891 lines
29 KiB
TypeScript
891 lines
29 KiB
TypeScript
/*
|
|
This file is part of GNU Taler
|
|
(C) 2022 Taler Systems S.A.
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
terms of the GNU General Public License as published by the Free Software
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
import {
|
|
AmountJson,
|
|
Amounts,
|
|
HttpStatusCode,
|
|
TranslatedString,
|
|
} from "@gnu-taler/taler-util";
|
|
import {
|
|
ErrorType,
|
|
HttpResponsePaginated,
|
|
RequestError,
|
|
useTranslationContext,
|
|
} from "@gnu-taler/web-util/lib/index.browser";
|
|
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, usePageContext } from "../context/pageState.js";
|
|
import { useAccountDetails } from "../hooks/access.js";
|
|
import {
|
|
useCashoutDetails,
|
|
useCircuitAccountAPI,
|
|
useRatiosAndFeeConfig,
|
|
} from "../hooks/circuit.js";
|
|
import { TanChannel, undefinedIfEmpty } from "../utils.js";
|
|
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
|
import { ErrorBanner } from "./BankFrame.js";
|
|
import { LoginForm } from "./LoginForm.js";
|
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
onRegister: () => void;
|
|
onLoadNotOk: <T>(
|
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
|
) => VNode;
|
|
}
|
|
export function BusinessAccount({
|
|
onClose,
|
|
onLoadNotOk,
|
|
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} />;
|
|
}
|
|
|
|
if (newCashout) {
|
|
return (
|
|
<CreateCashout
|
|
account={backend.state.username}
|
|
onLoadNotOk={onLoadNotOk}
|
|
onCancel={() => {
|
|
setNewcashout(false);
|
|
}}
|
|
onComplete={(id) => {
|
|
showInfoMessage(
|
|
i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`,
|
|
);
|
|
setNewcashout(false);
|
|
setShowCashoutDetails(id);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
if (showCashoutDetails) {
|
|
return (
|
|
<ShowCashoutDetails
|
|
id={showCashoutDetails}
|
|
onLoadNotOk={onLoadNotOk}
|
|
onCancel={() => {
|
|
setShowCashoutDetails(undefined);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
if (updatePassword) {
|
|
return (
|
|
<UpdateAccountPassword
|
|
account={backend.state.username}
|
|
onLoadNotOk={onLoadNotOk}
|
|
onUpdateSuccess={() => {
|
|
showInfoMessage(i18n.str`Password changed`);
|
|
setUpdatePassword(false);
|
|
}}
|
|
onClear={() => {
|
|
setUpdatePassword(false);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div>
|
|
<ShowAccountDetails
|
|
account={backend.state.username}
|
|
onLoadNotOk={onLoadNotOk}
|
|
onUpdateSuccess={() => {
|
|
showInfoMessage(i18n.str`Account updated`);
|
|
}}
|
|
onChangePassword={() => {
|
|
setUpdatePassword(true);
|
|
}}
|
|
onClear={onClose}
|
|
/>
|
|
<section style={{ marginTop: "2em" }}>
|
|
<div class="active">
|
|
<h3>{i18n.str`Latest cashouts`}</h3>
|
|
<Cashouts
|
|
account={backend.state.username}
|
|
onSelected={(id) => {
|
|
setShowCashoutDetails(id);
|
|
}}
|
|
/>
|
|
</div>
|
|
<br />
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
<div />
|
|
<input
|
|
class="pure-button pure-button-primary content"
|
|
type="submit"
|
|
value={i18n.str`New cashout`}
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
setNewcashout(true);
|
|
}}
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface PropsCashout {
|
|
account: string;
|
|
onComplete: (id: string) => void;
|
|
onCancel: () => void;
|
|
onLoadNotOk: <T>(
|
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
|
) => VNode;
|
|
}
|
|
|
|
type FormType = {
|
|
isDebit: boolean;
|
|
amount: string;
|
|
subject: string;
|
|
channel: TanChannel;
|
|
};
|
|
type ErrorFrom<T> = {
|
|
[P in keyof T]+?: string;
|
|
};
|
|
|
|
function CreateCashout({
|
|
account,
|
|
onComplete,
|
|
onCancel,
|
|
onLoadNotOk,
|
|
}: PropsCashout): VNode {
|
|
const { i18n } = useTranslationContext();
|
|
const ratiosResult = useRatiosAndFeeConfig();
|
|
const result = useAccountDetails(account);
|
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
|
|
const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
|
|
|
|
const { createCashout } = useCircuitAccountAPI();
|
|
if (!result.ok) return onLoadNotOk(result);
|
|
if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
|
|
const config = ratiosResult.data;
|
|
const maybeBalance = Amounts.parse(result.data.balance.amount);
|
|
if (!maybeBalance) return <div>error</div>;
|
|
const balance = maybeBalance;
|
|
const zero = Amounts.zeroOfCurrency(balance.currency);
|
|
|
|
const sellRate = config.ratios_and_fees.sell_at_ratio;
|
|
const sellFee = !config.ratios_and_fees.sell_out_fee
|
|
? zero
|
|
: Amounts.fromFloat(config.ratios_and_fees.sell_out_fee, balance.currency);
|
|
const fiatCurrency = config.ratios_and_fees.fiat_currency;
|
|
|
|
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_debit = !amount
|
|
? zero
|
|
: form.isDebit
|
|
? amount
|
|
: truncate(Amounts.divide(Amounts.add(amount, sellFee).amount, sellRate));
|
|
const credit_before_fee = !amount
|
|
? zero
|
|
: form.isDebit
|
|
? truncate(Amounts.divide(amount, 1 / sellRate))
|
|
: Amounts.add(amount, sellFee).amount;
|
|
|
|
const __amount_credit = Amounts.sub(credit_before_fee, sellFee).amount;
|
|
const amount_credit = Amounts.parseOrThrow(
|
|
`${fiatCurrency}:${Amounts.stringifyValue(__amount_credit)}`,
|
|
);
|
|
|
|
const balanceAfter = Amounts.sub(balance, amount_debit).amount;
|
|
|
|
function updateForm(newForm: typeof form): void {
|
|
setForm(newForm);
|
|
}
|
|
const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
|
|
amount: !form.amount
|
|
? i18n.str`required`
|
|
: !amount
|
|
? i18n.str`could not be parsed`
|
|
: Amounts.cmp(balance, amount_debit) === -1
|
|
? i18n.str`balance is not enough`
|
|
: Amounts.cmp(credit_before_fee, sellFee) === -1
|
|
? i18n.str`amount is not enough`
|
|
: Amounts.isZero(amount_credit)
|
|
? i18n.str`amount is not enough`
|
|
: undefined,
|
|
channel: !form.channel ? i18n.str`required` : undefined,
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
{error && (
|
|
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
|
)}
|
|
<h1>New cashout</h1>
|
|
<form class="pure-form">
|
|
<fieldset>
|
|
<label>{i18n.str`Subject`}</label>
|
|
<input
|
|
value={form.subject ?? ""}
|
|
onChange={(e) => {
|
|
form.subject = e.currentTarget.value;
|
|
updateForm(structuredClone(form));
|
|
}}
|
|
/>
|
|
<ShowInputErrorLabel
|
|
message={errors?.subject}
|
|
isDirty={form.subject !== undefined}
|
|
/>
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>
|
|
{form.isDebit
|
|
? i18n.str`Amount to send`
|
|
: i18n.str`Amount to receive`}
|
|
</label>
|
|
<div style={{ width: "max-content" }}>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
class="currency-indicator"
|
|
size={
|
|
!form.isDebit ? fiatCurrency.length : balance.currency.length
|
|
}
|
|
maxLength={
|
|
!form.isDebit ? fiatCurrency.length : balance.currency.length
|
|
}
|
|
tabIndex={-1}
|
|
value={!form.isDebit ? fiatCurrency : balance.currency}
|
|
/>
|
|
|
|
<input
|
|
type="number"
|
|
// ref={ref}
|
|
id="withdraw-amount"
|
|
name="withdraw-amount"
|
|
value={form.amount ?? ""}
|
|
onChange={(e): void => {
|
|
form.amount = e.currentTarget.value;
|
|
updateForm(structuredClone(form));
|
|
}}
|
|
/>
|
|
|
|
<label class="toggle">
|
|
<input
|
|
class="toggle-checkbox"
|
|
type="checkbox"
|
|
onChange={(e): void => {
|
|
form.isDebit = !form.isDebit;
|
|
updateForm(structuredClone(form));
|
|
}}
|
|
/>
|
|
<div class="toggle-switch"></div>
|
|
</label>
|
|
</div>
|
|
<ShowInputErrorLabel
|
|
message={errors?.amount}
|
|
isDirty={form.amount !== undefined}
|
|
/>
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>{i18n.str`Conversion rate`}</label>
|
|
<input value={sellRate} disabled />
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>{i18n.str`Balance now`}</label>
|
|
<div style={{ width: "max-content" }}>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
class="currency-indicator"
|
|
size={balance.currency.length}
|
|
maxLength={balance.currency.length}
|
|
tabIndex={-1}
|
|
value={balance.currency}
|
|
/>
|
|
|
|
<input
|
|
type="number"
|
|
id="withdraw-amount"
|
|
disabled
|
|
name="withdraw-amount"
|
|
value={Amounts.stringifyValue(balance)}
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
<fieldset>
|
|
<label
|
|
style={{ fontWeight: "bold", color: "red" }}
|
|
>{i18n.str`Total cost`}</label>
|
|
<div style={{ width: "max-content" }}>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
class="currency-indicator"
|
|
size={balance.currency.length}
|
|
maxLength={balance.currency.length}
|
|
tabIndex={-1}
|
|
value={balance.currency}
|
|
/>
|
|
|
|
<input
|
|
type="number"
|
|
// ref={ref}
|
|
id="withdraw-amount"
|
|
disabled
|
|
name="withdraw-amount"
|
|
value={amount_debit ? Amounts.stringifyValue(amount_debit) : ""}
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>{i18n.str`Balance after`}</label>
|
|
<div style={{ width: "max-content" }}>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
class="currency-indicator"
|
|
size={balance.currency.length}
|
|
maxLength={balance.currency.length}
|
|
tabIndex={-1}
|
|
value={balance.currency}
|
|
/>
|
|
|
|
<input
|
|
type="number"
|
|
// ref={ref}
|
|
id="withdraw-amount"
|
|
disabled
|
|
name="withdraw-amount"
|
|
value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
|
|
/>
|
|
</div>
|
|
</fieldset>{" "}
|
|
{Amounts.isZero(sellFee) ? undefined : (
|
|
<Fragment>
|
|
<fieldset>
|
|
<label>{i18n.str`Amount after conversion`}</label>
|
|
<div style={{ width: "max-content" }}>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
class="currency-indicator"
|
|
size={fiatCurrency.length}
|
|
maxLength={fiatCurrency.length}
|
|
tabIndex={-1}
|
|
value={fiatCurrency}
|
|
/>
|
|
|
|
<input
|
|
// type="number"
|
|
style={{ color: "black" }}
|
|
disabled
|
|
value={Amounts.stringifyValue(credit_before_fee)}
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<label>{i18n.str`Cashout fee`}</label>
|
|
<div style={{ width: "max-content" }}>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
class="currency-indicator"
|
|
size={fiatCurrency.length}
|
|
maxLength={fiatCurrency.length}
|
|
tabIndex={-1}
|
|
value={fiatCurrency}
|
|
/>
|
|
|
|
<input
|
|
// type="number"
|
|
style={{ color: "black" }}
|
|
disabled
|
|
value={Amounts.stringifyValue(sellFee)}
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
</Fragment>
|
|
)}
|
|
<fieldset>
|
|
<label
|
|
style={{ fontWeight: "bold", color: "green" }}
|
|
>{i18n.str`Total cashout transfer`}</label>
|
|
<div style={{ width: "max-content" }}>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
class="currency-indicator"
|
|
size={fiatCurrency.length}
|
|
maxLength={fiatCurrency.length}
|
|
tabIndex={-1}
|
|
value={fiatCurrency}
|
|
/>
|
|
|
|
<input
|
|
type="number"
|
|
// ref={ref}
|
|
id="withdraw-amount"
|
|
disabled
|
|
name="withdraw-amount"
|
|
value={amount_credit ? Amounts.stringifyValue(amount_credit) : ""}
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>{i18n.str`Confirmation channel`}</label>
|
|
|
|
<div class="channel">
|
|
<input
|
|
class={
|
|
"pure-button content " +
|
|
(form.channel === TanChannel.EMAIL
|
|
? "pure-button-primary"
|
|
: "pure-button-secondary")
|
|
}
|
|
type="submit"
|
|
value={i18n.str`Email`}
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
form.channel = TanChannel.EMAIL;
|
|
updateForm(structuredClone(form));
|
|
}}
|
|
/>
|
|
<input
|
|
class={
|
|
"pure-button content " +
|
|
(form.channel === TanChannel.SMS
|
|
? "pure-button-primary"
|
|
: "pure-button-secondary")
|
|
}
|
|
type="submit"
|
|
value={i18n.str`SMS`}
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
form.channel = TanChannel.SMS;
|
|
updateForm(structuredClone(form));
|
|
}}
|
|
/>
|
|
<input
|
|
class={
|
|
"pure-button content " +
|
|
(form.channel === TanChannel.FILE
|
|
? "pure-button-primary"
|
|
: "pure-button-secondary")
|
|
}
|
|
type="submit"
|
|
value={i18n.str`FILE`}
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
form.channel = TanChannel.FILE;
|
|
updateForm(structuredClone(form));
|
|
}}
|
|
/>
|
|
</div>
|
|
<ShowInputErrorLabel
|
|
message={errors?.channel}
|
|
isDirty={form.channel !== undefined}
|
|
/>
|
|
</fieldset>
|
|
<br />
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
<button
|
|
class="pure-button pure-button-secondary btn-cancel"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
onCancel();
|
|
}}
|
|
>
|
|
{i18n.str`Cancel`}
|
|
</button>
|
|
|
|
<button
|
|
class="pure-button pure-button-primary btn-register"
|
|
type="submit"
|
|
disabled={!!errors}
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (errors) return;
|
|
try {
|
|
const res = await createCashout({
|
|
amount_credit: Amounts.stringify(amount_credit),
|
|
amount_debit: Amounts.stringify(amount_debit),
|
|
subject: form.subject,
|
|
tan_channel: form.channel,
|
|
});
|
|
onComplete(res.data.uuid);
|
|
} catch (error) {
|
|
if (error instanceof RequestError) {
|
|
const e = error as RequestError<SandboxBackend.SandboxError>;
|
|
switch (e.cause.type) {
|
|
case ErrorType.TIMEOUT: {
|
|
saveError({
|
|
title: i18n.str`Request timeout, try again later.`,
|
|
});
|
|
break;
|
|
}
|
|
case ErrorType.CLIENT: {
|
|
const errorData = e.cause.error;
|
|
|
|
if (
|
|
e.cause.status === HttpStatusCode.PreconditionFailed
|
|
) {
|
|
saveError({
|
|
title: i18n.str`The account does not have sufficient funds`,
|
|
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 {
|
|
saveError({
|
|
title: i18n.str`New cashout gave response error`,
|
|
description: errorData.error.description,
|
|
debug: JSON.stringify(error.info),
|
|
});
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{i18n.str`Create`}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ShowCashoutProps {
|
|
id: string;
|
|
onCancel: () => void;
|
|
onLoadNotOk: <T>(
|
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
|
) => VNode;
|
|
}
|
|
export function ShowCashoutDetails({
|
|
id,
|
|
onCancel,
|
|
onLoadNotOk,
|
|
}: ShowCashoutProps): VNode {
|
|
const { i18n } = useTranslationContext();
|
|
const result = useCashoutDetails(id);
|
|
const { abortCashout, confirmCashout } = useCircuitAccountAPI();
|
|
const [code, setCode] = useState<string | undefined>(undefined);
|
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
if (!result.ok) return onLoadNotOk(result);
|
|
const errors = undefinedIfEmpty({
|
|
code: !code ? i18n.str`required` : undefined,
|
|
});
|
|
const isPending = String(result.data.status).toUpperCase() === "PENDING";
|
|
return (
|
|
<div>
|
|
<h1>Cashout details {id}</h1>
|
|
{error && (
|
|
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
|
)}
|
|
<form class="pure-form">
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Subject</i18n.Translate>
|
|
</label>
|
|
<input readOnly value={result.data.subject} />
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Created</i18n.Translate>
|
|
</label>
|
|
<input readOnly value={result.data.creation_time ?? ""} />
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Confirmed</i18n.Translate>
|
|
</label>
|
|
<input readOnly value={result.data.confirmation_time ?? ""} />
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Debited</i18n.Translate>
|
|
</label>
|
|
<input readOnly value={result.data.amount_debit} />
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Credit</i18n.Translate>
|
|
</label>
|
|
<input readOnly value={result.data.amount_credit} />
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Status</i18n.Translate>
|
|
</label>
|
|
<input readOnly value={result.data.status} />
|
|
</fieldset>
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Destination</i18n.Translate>
|
|
</label>
|
|
<input readOnly value={result.data.cashout_address} />
|
|
</fieldset>
|
|
{isPending ? (
|
|
<fieldset>
|
|
<label>
|
|
<i18n.Translate>Code</i18n.Translate>
|
|
</label>
|
|
<input
|
|
value={code ?? ""}
|
|
onChange={(e) => {
|
|
setCode(e.currentTarget.value);
|
|
}}
|
|
/>
|
|
<ShowInputErrorLabel
|
|
message={errors?.code}
|
|
isDirty={code !== undefined}
|
|
/>
|
|
</fieldset>
|
|
) : undefined}
|
|
</form>
|
|
<br />
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
<button
|
|
class="pure-button pure-button-secondary btn-cancel"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
onCancel();
|
|
}}
|
|
>
|
|
{i18n.str`Back`}
|
|
</button>
|
|
{isPending ? (
|
|
<div>
|
|
<button
|
|
type="submit"
|
|
class="pure-button pure-button-primary button-error"
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const rest = await abortCashout(id);
|
|
onCancel();
|
|
} catch (error) {
|
|
if (error instanceof RequestError) {
|
|
const e =
|
|
error as RequestError<SandboxBackend.SandboxError>;
|
|
switch (e.cause.type) {
|
|
case ErrorType.TIMEOUT: {
|
|
saveError({
|
|
title: i18n.str`Request timeout, try again later.`,
|
|
});
|
|
break;
|
|
}
|
|
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 {
|
|
saveError({
|
|
title: i18n.str`Aborting cashout gave response error`,
|
|
description: errorData.error.description,
|
|
debug: JSON.stringify(error.info),
|
|
});
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{i18n.str`Abort`}
|
|
</button>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={!code}
|
|
class="pure-button pure-button-primary "
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
if (!code) return;
|
|
const rest = await confirmCashout(id, {
|
|
tan: code,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof RequestError) {
|
|
const e =
|
|
error as RequestError<SandboxBackend.SandboxError>;
|
|
switch (e.cause.type) {
|
|
case ErrorType.TIMEOUT: {
|
|
saveError({
|
|
title: i18n.str`Request timeout, try again later.`,
|
|
});
|
|
break;
|
|
}
|
|
case ErrorType.CLIENT: {
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{i18n.str`Confirm`}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function assertUnreachable(x: never): never {
|
|
throw new Error("Didn't expect to get here");
|
|
}
|