2023-02-08 21:41:19 +01:00
|
|
|
/*
|
|
|
|
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/>
|
|
|
|
*/
|
|
|
|
|
2023-02-17 20:23:37 +01:00
|
|
|
import {
|
|
|
|
Amounts,
|
2023-02-28 23:03:43 +01:00
|
|
|
HttpStatusCode,
|
2023-02-17 20:23:37 +01:00
|
|
|
parsePaytoUri,
|
|
|
|
TranslatedString,
|
|
|
|
} from "@gnu-taler/taler-util";
|
2023-02-08 21:41:19 +01:00
|
|
|
import {
|
|
|
|
HttpResponsePaginated,
|
|
|
|
RequestError,
|
|
|
|
useTranslationContext,
|
|
|
|
} from "@gnu-taler/web-util/lib/index.browser";
|
|
|
|
import { Fragment, h, VNode } from "preact";
|
|
|
|
import { useState } from "preact/hooks";
|
2023-02-17 20:23:37 +01:00
|
|
|
import { Cashouts } from "../components/Cashouts/index.js";
|
2023-02-08 21:41:19 +01:00
|
|
|
import { ErrorMessage, usePageContext } from "../context/pageState.js";
|
2023-02-17 20:23:37 +01:00
|
|
|
import { useAccountDetails } from "../hooks/access.js";
|
2023-02-08 21:41:19 +01:00
|
|
|
import {
|
2023-02-10 13:51:37 +01:00
|
|
|
useBusinessAccountDetails,
|
|
|
|
useBusinessAccounts,
|
2023-02-08 21:41:19 +01:00
|
|
|
useAdminAccountAPI,
|
|
|
|
} from "../hooks/circuit.js";
|
|
|
|
import {
|
2023-02-28 23:03:43 +01:00
|
|
|
buildRequestErrorMessage,
|
2023-02-08 21:41:19 +01:00
|
|
|
PartialButDefined,
|
2023-02-28 23:03:43 +01:00
|
|
|
RecursivePartial,
|
2023-02-08 21:41:19 +01:00
|
|
|
undefinedIfEmpty,
|
2023-03-05 19:21:12 +01:00
|
|
|
validateIBAN,
|
2023-02-08 21:41:19 +01:00
|
|
|
WithIntermediate,
|
|
|
|
} from "../utils.js";
|
2023-02-28 23:03:43 +01:00
|
|
|
import { ErrorBannerFloat } from "./BankFrame.js";
|
2023-02-20 14:18:02 +01:00
|
|
|
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
2023-02-08 21:41:19 +01:00
|
|
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
|
|
|
|
|
|
|
const charset =
|
|
|
|
"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
|
|
const upperIdx = charset.indexOf("A");
|
|
|
|
|
|
|
|
function randomPassword(): string {
|
|
|
|
const random = Array.from({ length: 16 }).map(() => {
|
|
|
|
return charset.charCodeAt(Math.random() * charset.length);
|
|
|
|
});
|
|
|
|
// first char can't be upper
|
|
|
|
const charIdx = charset.indexOf(String.fromCharCode(random[0]));
|
|
|
|
random[0] =
|
|
|
|
charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
|
|
|
|
return String.fromCharCode(...random);
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Props {
|
2023-02-25 23:43:45 +01:00
|
|
|
onLoadNotOk: <T>(
|
|
|
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
|
|
|
) => VNode;
|
2023-02-08 21:41:19 +01:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Query account information and show QR code if there is pending withdrawal
|
|
|
|
*/
|
|
|
|
export function AdminPage({ onLoadNotOk }: Props): VNode {
|
|
|
|
const [account, setAccount] = useState<string | undefined>();
|
|
|
|
const [showDetails, setShowDetails] = useState<string | undefined>();
|
2023-02-17 20:23:37 +01:00
|
|
|
const [showCashouts, setShowCashouts] = useState<string | undefined>();
|
2023-02-08 21:41:19 +01:00
|
|
|
const [updatePassword, setUpdatePassword] = useState<string | undefined>();
|
2023-02-17 20:23:37 +01:00
|
|
|
const [removeAccount, setRemoveAccount] = useState<string | undefined>();
|
2023-02-20 14:18:02 +01:00
|
|
|
const [showCashoutDetails, setShowCashoutDetails] = useState<
|
|
|
|
string | undefined
|
|
|
|
>();
|
2023-02-17 20:23:37 +01:00
|
|
|
|
2023-02-08 21:41:19 +01:00
|
|
|
const [createAccount, setCreateAccount] = useState(false);
|
|
|
|
const { pageStateSetter } = usePageContext();
|
|
|
|
|
|
|
|
function showInfoMessage(info: TranslatedString): void {
|
|
|
|
pageStateSetter((prev) => ({
|
|
|
|
...prev,
|
|
|
|
info,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2023-02-10 13:51:37 +01:00
|
|
|
const result = useBusinessAccounts({ account });
|
2023-02-08 21:41:19 +01:00
|
|
|
const { i18n } = useTranslationContext();
|
|
|
|
|
|
|
|
if (result.loading) return <div />;
|
|
|
|
if (!result.ok) {
|
|
|
|
return onLoadNotOk(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { customers } = result.data;
|
|
|
|
|
2023-02-20 14:18:02 +01:00
|
|
|
if (showCashoutDetails) {
|
|
|
|
return (
|
|
|
|
<ShowCashoutDetails
|
|
|
|
id={showCashoutDetails}
|
|
|
|
onLoadNotOk={onLoadNotOk}
|
|
|
|
onCancel={() => {
|
|
|
|
setShowCashoutDetails(undefined);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-17 20:23:37 +01:00
|
|
|
if (showCashouts) {
|
|
|
|
return (
|
|
|
|
<div>
|
2023-02-25 23:43:45 +01:00
|
|
|
<div>
|
|
|
|
<h1 class="nav welcome-text">
|
|
|
|
<i18n.Translate>Cashout for account {showCashouts}</i18n.Translate>
|
|
|
|
</h1>
|
|
|
|
</div>
|
2023-02-20 14:18:02 +01:00
|
|
|
<Cashouts
|
|
|
|
account={showCashouts}
|
|
|
|
onSelected={(id) => {
|
|
|
|
setShowCashouts(id);
|
|
|
|
setShowCashouts(undefined);
|
|
|
|
}}
|
|
|
|
/>
|
2023-02-25 23:43:45 +01:00
|
|
|
<p>
|
|
|
|
<input
|
|
|
|
class="pure-button"
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Close`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
setShowCashouts(undefined);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</p>
|
2023-02-17 20:23:37 +01:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-08 21:41:19 +01:00
|
|
|
if (showDetails) {
|
|
|
|
return (
|
|
|
|
<ShowAccountDetails
|
|
|
|
account={showDetails}
|
|
|
|
onLoadNotOk={onLoadNotOk}
|
2023-02-10 13:51:37 +01:00
|
|
|
onChangePassword={() => {
|
|
|
|
setUpdatePassword(showDetails);
|
|
|
|
setShowDetails(undefined);
|
|
|
|
}}
|
2023-02-08 21:41:19 +01:00
|
|
|
onUpdateSuccess={() => {
|
|
|
|
showInfoMessage(i18n.str`Account updated`);
|
|
|
|
setShowDetails(undefined);
|
|
|
|
}}
|
|
|
|
onClear={() => {
|
|
|
|
setShowDetails(undefined);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2023-02-17 20:23:37 +01:00
|
|
|
if (removeAccount) {
|
|
|
|
return (
|
|
|
|
<RemoveAccount
|
|
|
|
account={removeAccount}
|
|
|
|
onLoadNotOk={onLoadNotOk}
|
|
|
|
onUpdateSuccess={() => {
|
|
|
|
showInfoMessage(i18n.str`Account removed`);
|
|
|
|
setRemoveAccount(undefined);
|
|
|
|
}}
|
|
|
|
onClear={() => {
|
|
|
|
setRemoveAccount(undefined);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2023-02-08 21:41:19 +01:00
|
|
|
if (updatePassword) {
|
|
|
|
return (
|
|
|
|
<UpdateAccountPassword
|
|
|
|
account={updatePassword}
|
|
|
|
onLoadNotOk={onLoadNotOk}
|
|
|
|
onUpdateSuccess={() => {
|
|
|
|
showInfoMessage(i18n.str`Password changed`);
|
|
|
|
setUpdatePassword(undefined);
|
|
|
|
}}
|
|
|
|
onClear={() => {
|
|
|
|
setUpdatePassword(undefined);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (createAccount) {
|
|
|
|
return (
|
|
|
|
<CreateNewAccount
|
|
|
|
onClose={() => setCreateAccount(false)}
|
|
|
|
onCreateSuccess={(password) => {
|
|
|
|
showInfoMessage(
|
2023-02-25 23:43:45 +01:00
|
|
|
i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
|
2023-02-08 21:41:19 +01:00
|
|
|
);
|
|
|
|
setCreateAccount(false);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<Fragment>
|
|
|
|
<div>
|
|
|
|
<h1 class="nav welcome-text">
|
|
|
|
<i18n.Translate>Admin panel</i18n.Translate>
|
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<p>
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
<div></div>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
class="pure-button pure-button-primary content"
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Create account`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
setCreateAccount(true);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</p>
|
|
|
|
|
2023-03-06 19:12:43 +01:00
|
|
|
<section
|
|
|
|
id="main"
|
|
|
|
style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
|
|
|
|
>
|
2023-03-05 19:21:12 +01:00
|
|
|
{!customers.length ? (
|
|
|
|
<div></div>
|
|
|
|
) : (
|
|
|
|
<article>
|
|
|
|
<h2>{i18n.str`Accounts:`}</h2>
|
|
|
|
<div class="results">
|
|
|
|
<table class="pure-table pure-table-striped">
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>{i18n.str`Username`}</th>
|
|
|
|
<th>{i18n.str`Name`}</th>
|
2023-03-06 19:12:43 +01:00
|
|
|
<th>{i18n.str`Balance`}</th>
|
|
|
|
<th>{i18n.str`Actions`}</th>
|
2023-03-05 19:21:12 +01:00
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
{customers.map((item, idx) => {
|
2023-03-06 19:12:43 +01:00
|
|
|
const balance = !item.balance
|
|
|
|
? undefined
|
|
|
|
: Amounts.parse(item.balance.amount);
|
|
|
|
const balanceIsDebit =
|
|
|
|
item.balance &&
|
|
|
|
item.balance.credit_debit_indicator == "debit";
|
2023-03-05 19:21:12 +01:00
|
|
|
return (
|
|
|
|
<tr key={idx}>
|
|
|
|
<td>
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
setShowDetails(item.username);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{item.username}
|
|
|
|
</a>
|
|
|
|
</td>
|
|
|
|
<td>{item.name}</td>
|
2023-03-06 19:12:43 +01:00
|
|
|
<td>
|
|
|
|
{!balance ? (
|
|
|
|
i18n.str`unknown`
|
|
|
|
) : (
|
|
|
|
<span class="amount">
|
|
|
|
{balanceIsDebit ? <b>-</b> : null}
|
|
|
|
<span class="value">{`${Amounts.stringifyValue(
|
|
|
|
balance,
|
|
|
|
)}`}</span>
|
|
|
|
|
|
|
|
<span class="currency">{`${balance.currency}`}</span>
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</td>
|
2023-03-05 19:21:12 +01:00
|
|
|
<td>
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
setUpdatePassword(item.username);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
change password
|
|
|
|
</a>
|
2023-03-06 19:12:43 +01:00
|
|
|
|
2023-03-05 19:21:12 +01:00
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
setShowCashouts(item.username);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
cashouts
|
|
|
|
</a>
|
2023-03-06 19:12:43 +01:00
|
|
|
|
2023-03-05 19:21:12 +01:00
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
setRemoveAccount(item.username);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
remove
|
|
|
|
</a>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
</article>
|
|
|
|
)}
|
2023-02-08 21:41:19 +01:00
|
|
|
</section>
|
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
|
|
|
|
const EMAIL_REGEX =
|
|
|
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
|
|
const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
|
|
|
|
|
|
|
|
function initializeFromTemplate(
|
|
|
|
account: SandboxBackend.Circuit.CircuitAccountData | undefined,
|
|
|
|
): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
|
|
|
|
const emptyAccount = {
|
|
|
|
cashout_address: undefined,
|
|
|
|
iban: undefined,
|
|
|
|
name: undefined,
|
|
|
|
username: undefined,
|
|
|
|
contact_data: undefined,
|
|
|
|
};
|
|
|
|
const emptyContact = {
|
|
|
|
email: undefined,
|
|
|
|
phone: undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
|
|
|
|
structuredClone(account) ?? emptyAccount;
|
|
|
|
if (typeof initial.contact_data === "undefined") {
|
|
|
|
initial.contact_data = emptyContact;
|
|
|
|
}
|
|
|
|
initial.contact_data.email;
|
|
|
|
return initial as any;
|
|
|
|
}
|
|
|
|
|
2023-02-10 13:51:37 +01:00
|
|
|
export function UpdateAccountPassword({
|
2023-02-08 21:41:19 +01:00
|
|
|
account,
|
|
|
|
onClear,
|
|
|
|
onUpdateSuccess,
|
|
|
|
onLoadNotOk,
|
|
|
|
}: {
|
2023-02-25 23:43:45 +01:00
|
|
|
onLoadNotOk: <T>(
|
|
|
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
|
|
|
) => VNode;
|
2023-02-08 21:41:19 +01:00
|
|
|
onClear: () => void;
|
|
|
|
onUpdateSuccess: () => void;
|
|
|
|
account: string;
|
|
|
|
}): VNode {
|
|
|
|
const { i18n } = useTranslationContext();
|
2023-02-10 13:51:37 +01:00
|
|
|
const result = useBusinessAccountDetails(account);
|
2023-02-08 21:41:19 +01:00
|
|
|
const { changePassword } = useAdminAccountAPI();
|
|
|
|
const [password, setPassword] = useState<string | undefined>();
|
|
|
|
const [repeat, setRepeat] = useState<string | undefined>();
|
|
|
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
|
|
|
|
|
|
if (result.clientError) {
|
|
|
|
if (result.isNotfound) return <div>account not found</div>;
|
|
|
|
}
|
|
|
|
if (!result.ok) {
|
|
|
|
return onLoadNotOk(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
const errors = undefinedIfEmpty({
|
|
|
|
password: !password ? i18n.str`required` : undefined,
|
|
|
|
repeat: !repeat
|
|
|
|
? i18n.str`required`
|
|
|
|
: password !== repeat
|
|
|
|
? i18n.str`password doesn't match`
|
|
|
|
: undefined,
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h1 class="nav welcome-text">
|
2023-02-10 13:51:37 +01:00
|
|
|
<i18n.Translate>Update password for {account}</i18n.Translate>
|
2023-02-08 21:41:19 +01:00
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
{error && (
|
2023-02-28 23:03:43 +01:00
|
|
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
2023-02-08 21:41:19 +01:00
|
|
|
)}
|
|
|
|
|
2023-03-06 19:12:43 +01:00
|
|
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
|
|
|
<form class="pure-form">
|
|
|
|
<fieldset>
|
|
|
|
<label>{i18n.str`Password`}</label>
|
2023-02-08 21:41:19 +01:00
|
|
|
<input
|
2023-03-06 19:12:43 +01:00
|
|
|
type="password"
|
|
|
|
value={password ?? ""}
|
|
|
|
onChange={(e) => {
|
|
|
|
setPassword(e.currentTarget.value);
|
2023-02-08 21:41:19 +01:00
|
|
|
}}
|
|
|
|
/>
|
2023-03-06 19:12:43 +01:00
|
|
|
<ShowInputErrorLabel
|
|
|
|
message={errors?.password}
|
|
|
|
isDirty={password !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
|
|
<label>{i18n.str`Repeast password`}</label>
|
2023-02-08 21:41:19 +01:00
|
|
|
<input
|
2023-03-06 19:12:43 +01:00
|
|
|
type="password"
|
|
|
|
value={repeat ?? ""}
|
|
|
|
onChange={(e) => {
|
|
|
|
setRepeat(e.currentTarget.value);
|
2023-02-08 21:41:19 +01:00
|
|
|
}}
|
|
|
|
/>
|
2023-03-06 19:12:43 +01:00
|
|
|
<ShowInputErrorLabel
|
|
|
|
message={errors?.repeat}
|
|
|
|
isDirty={repeat !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
|
|
|
</form>
|
|
|
|
<p>
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
class="pure-button"
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Close`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
onClear();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
id="select-exchange"
|
|
|
|
class="pure-button pure-button-primary content"
|
|
|
|
disabled={!!errors}
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Confirm`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
if (!!errors || !password) return;
|
|
|
|
try {
|
|
|
|
const r = await changePassword(account, {
|
|
|
|
new_password: password,
|
|
|
|
});
|
|
|
|
onUpdateSuccess();
|
|
|
|
} catch (error) {
|
|
|
|
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),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-02-08 21:41:19 +01:00
|
|
|
</div>
|
2023-03-06 19:12:43 +01:00
|
|
|
</p>
|
|
|
|
</div>
|
2023-02-08 21:41:19 +01:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function CreateNewAccount({
|
|
|
|
onClose,
|
|
|
|
onCreateSuccess,
|
|
|
|
}: {
|
|
|
|
onClose: () => void;
|
|
|
|
onCreateSuccess: (password: string) => void;
|
|
|
|
}): VNode {
|
|
|
|
const { i18n } = useTranslationContext();
|
|
|
|
const { createAccount } = useAdminAccountAPI();
|
|
|
|
const [submitAccount, setSubmitAccount] = useState<
|
|
|
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
|
|
|
>();
|
|
|
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h1 class="nav welcome-text">
|
2023-02-10 13:51:37 +01:00
|
|
|
<i18n.Translate>New account</i18n.Translate>
|
2023-02-08 21:41:19 +01:00
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
{error && (
|
2023-02-28 23:03:43 +01:00
|
|
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
2023-02-08 21:41:19 +01:00
|
|
|
)}
|
|
|
|
|
2023-03-06 19:12:43 +01:00
|
|
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
|
|
|
<AccountForm
|
|
|
|
template={undefined}
|
|
|
|
purpose="create"
|
|
|
|
onChange={(a) => {
|
|
|
|
console.log(a);
|
|
|
|
setSubmitAccount(a);
|
|
|
|
}}
|
|
|
|
/>
|
2023-02-08 21:41:19 +01:00
|
|
|
|
2023-03-06 19:12:43 +01:00
|
|
|
<p>
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
class="pure-button"
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Close`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
onClose();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
id="select-exchange"
|
|
|
|
class="pure-button pure-button-primary content"
|
|
|
|
disabled={!submitAccount}
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Confirm`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
2023-02-08 21:41:19 +01:00
|
|
|
|
2023-03-06 19:12:43 +01:00
|
|
|
if (!submitAccount) return;
|
|
|
|
try {
|
|
|
|
const account: SandboxBackend.Circuit.CircuitAccountRequest =
|
|
|
|
{
|
|
|
|
cashout_address: submitAccount.cashout_address,
|
|
|
|
contact_data: submitAccount.contact_data,
|
|
|
|
internal_iban: submitAccount.iban,
|
|
|
|
name: submitAccount.name,
|
|
|
|
username: submitAccount.username,
|
|
|
|
password: randomPassword(),
|
|
|
|
};
|
|
|
|
|
|
|
|
await createAccount(account);
|
|
|
|
onCreateSuccess(account.password);
|
|
|
|
} catch (error) {
|
|
|
|
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),
|
|
|
|
});
|
|
|
|
}
|
2023-02-28 23:03:43 +01:00
|
|
|
}
|
2023-03-06 19:12:43 +01:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-02-08 21:41:19 +01:00
|
|
|
</div>
|
2023-03-06 19:12:43 +01:00
|
|
|
</p>
|
|
|
|
</div>
|
2023-02-08 21:41:19 +01:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-10 13:51:37 +01:00
|
|
|
export function ShowAccountDetails({
|
2023-02-08 21:41:19 +01:00
|
|
|
account,
|
|
|
|
onClear,
|
|
|
|
onUpdateSuccess,
|
|
|
|
onLoadNotOk,
|
2023-02-10 13:51:37 +01:00
|
|
|
onChangePassword,
|
2023-02-08 21:41:19 +01:00
|
|
|
}: {
|
2023-02-25 23:43:45 +01:00
|
|
|
onLoadNotOk: <T>(
|
|
|
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
|
|
|
) => VNode;
|
2023-02-10 13:51:37 +01:00
|
|
|
onClear?: () => void;
|
|
|
|
onChangePassword: () => void;
|
2023-02-08 21:41:19 +01:00
|
|
|
onUpdateSuccess: () => void;
|
|
|
|
account: string;
|
|
|
|
}): VNode {
|
|
|
|
const { i18n } = useTranslationContext();
|
2023-02-10 13:51:37 +01:00
|
|
|
const result = useBusinessAccountDetails(account);
|
2023-02-08 21:41:19 +01:00
|
|
|
const { updateAccount } = useAdminAccountAPI();
|
|
|
|
const [update, setUpdate] = useState(false);
|
|
|
|
const [submitAccount, setSubmitAccount] = useState<
|
|
|
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
|
|
|
>();
|
|
|
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
|
|
|
|
|
|
if (result.clientError) {
|
|
|
|
if (result.isNotfound) return <div>account not found</div>;
|
|
|
|
}
|
|
|
|
if (!result.ok) {
|
|
|
|
return onLoadNotOk(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h1 class="nav welcome-text">
|
2023-02-10 13:51:37 +01:00
|
|
|
<i18n.Translate>Business account details</i18n.Translate>
|
2023-02-08 21:41:19 +01:00
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
{error && (
|
2023-02-28 23:03:43 +01:00
|
|
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
2023-02-08 21:41:19 +01:00
|
|
|
)}
|
2023-03-06 19:12:43 +01:00
|
|
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
|
|
|
<AccountForm
|
|
|
|
template={result.data}
|
|
|
|
purpose={update ? "update" : "show"}
|
|
|
|
onChange={(a) => setSubmitAccount(a)}
|
|
|
|
/>
|
2023-02-08 21:41:19 +01:00
|
|
|
|
2023-03-06 19:12:43 +01:00
|
|
|
<p>
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
2023-02-10 13:51:37 +01:00
|
|
|
<div>
|
2023-03-06 19:12:43 +01:00
|
|
|
{onClear ? (
|
|
|
|
<input
|
|
|
|
class="pure-button"
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Close`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
onClear();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
) : undefined}
|
2023-02-10 13:51:37 +01:00
|
|
|
</div>
|
2023-03-06 19:12:43 +01:00
|
|
|
<div style={{ display: "flex" }}>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
id="select-exchange"
|
|
|
|
class="pure-button pure-button-primary content"
|
|
|
|
disabled={update && !submitAccount}
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Change password`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
onChangePassword();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
id="select-exchange"
|
|
|
|
class="pure-button pure-button-primary content"
|
|
|
|
disabled={update && !submitAccount}
|
|
|
|
type="submit"
|
|
|
|
value={update ? i18n.str`Confirm` : i18n.str`Update`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
if (!update) {
|
|
|
|
setUpdate(true);
|
|
|
|
} else {
|
|
|
|
if (!submitAccount) return;
|
|
|
|
try {
|
|
|
|
await updateAccount(account, {
|
|
|
|
cashout_address: submitAccount.cashout_address,
|
|
|
|
contact_data: submitAccount.contact_data,
|
2023-02-28 23:03:43 +01:00
|
|
|
});
|
2023-03-06 19:12:43 +01:00
|
|
|
onUpdateSuccess();
|
|
|
|
} catch (error) {
|
|
|
|
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),
|
|
|
|
});
|
|
|
|
}
|
2023-02-28 23:03:43 +01:00
|
|
|
}
|
2023-02-10 13:51:37 +01:00
|
|
|
}
|
2023-03-06 19:12:43 +01:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-02-10 13:51:37 +01:00
|
|
|
</div>
|
2023-02-08 21:41:19 +01:00
|
|
|
</div>
|
2023-03-06 19:12:43 +01:00
|
|
|
</p>
|
|
|
|
</div>
|
2023-02-08 21:41:19 +01:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-17 20:23:37 +01:00
|
|
|
function RemoveAccount({
|
|
|
|
account,
|
|
|
|
onClear,
|
|
|
|
onUpdateSuccess,
|
|
|
|
onLoadNotOk,
|
|
|
|
}: {
|
2023-02-25 23:43:45 +01:00
|
|
|
onLoadNotOk: <T>(
|
|
|
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
|
|
|
) => VNode;
|
2023-02-17 20:23:37 +01:00
|
|
|
onClear: () => void;
|
|
|
|
onUpdateSuccess: () => void;
|
|
|
|
account: string;
|
|
|
|
}): VNode {
|
|
|
|
const { i18n } = useTranslationContext();
|
|
|
|
const result = useAccountDetails(account);
|
|
|
|
const { deleteAccount } = useAdminAccountAPI();
|
|
|
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
|
|
|
|
|
|
if (result.clientError) {
|
|
|
|
if (result.isNotfound) return <div>account not found</div>;
|
|
|
|
}
|
|
|
|
if (!result.ok) {
|
|
|
|
return onLoadNotOk(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
const balance = Amounts.parse(result.data.balance.amount);
|
|
|
|
if (!balance) {
|
|
|
|
return <div>there was an error reading the balance</div>;
|
|
|
|
}
|
|
|
|
const isBalanceEmpty = Amounts.isZero(balance);
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h1 class="nav welcome-text">
|
|
|
|
<i18n.Translate>Remove account: {account}</i18n.Translate>
|
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
{!isBalanceEmpty && (
|
2023-02-28 23:03:43 +01:00
|
|
|
<ErrorBannerFloat
|
2023-02-17 20:23:37 +01:00
|
|
|
error={{
|
|
|
|
title: i18n.str`Can't delete the account`,
|
|
|
|
description: i18n.str`Balance is not empty`,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{error && (
|
2023-02-28 23:03:43 +01:00
|
|
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
2023-02-17 20:23:37 +01:00
|
|
|
)}
|
|
|
|
|
|
|
|
<p>
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
class="pure-button"
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Cancel`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
onClear();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
id="select-exchange"
|
|
|
|
class="pure-button pure-button-primary content"
|
|
|
|
disabled={!isBalanceEmpty}
|
|
|
|
type="submit"
|
|
|
|
value={i18n.str`Confirm`}
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
try {
|
|
|
|
const r = await deleteAccount(account);
|
|
|
|
onUpdateSuccess();
|
|
|
|
} catch (error) {
|
2023-02-28 23:03:43 +01:00
|
|
|
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),
|
|
|
|
});
|
|
|
|
}
|
2023-02-17 20:23:37 +01:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2023-02-08 21:41:19 +01:00
|
|
|
/**
|
|
|
|
* Create valid account object to update or create
|
|
|
|
* Take template as initial values for the form
|
|
|
|
* Purpose indicate if all field al read only (show), part of them (update)
|
|
|
|
* or none (create)
|
|
|
|
* @param param0
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
function AccountForm({
|
|
|
|
template,
|
|
|
|
purpose,
|
|
|
|
onChange,
|
|
|
|
}: {
|
|
|
|
template: SandboxBackend.Circuit.CircuitAccountData | undefined;
|
|
|
|
onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
|
|
|
|
purpose: "create" | "update" | "show";
|
|
|
|
}): VNode {
|
|
|
|
const initial = initializeFromTemplate(template);
|
|
|
|
const [form, setForm] = useState(initial);
|
2023-02-28 23:03:43 +01:00
|
|
|
const [errors, setErrors] = useState<
|
|
|
|
RecursivePartial<typeof initial> | undefined
|
|
|
|
>(undefined);
|
2023-02-08 21:41:19 +01:00
|
|
|
const { i18n } = useTranslationContext();
|
|
|
|
|
|
|
|
function updateForm(newForm: typeof initial): void {
|
|
|
|
const parsed = !newForm.cashout_address
|
|
|
|
? undefined
|
|
|
|
: parsePaytoUri(newForm.cashout_address);
|
|
|
|
|
2023-02-28 23:03:43 +01:00
|
|
|
const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
|
2023-02-08 21:41:19 +01:00
|
|
|
cashout_address: !newForm.cashout_address
|
|
|
|
? i18n.str`required`
|
|
|
|
: !parsed
|
|
|
|
? i18n.str`does not follow the pattern`
|
|
|
|
: !parsed.isKnown || parsed.targetType !== "iban"
|
|
|
|
? i18n.str`only "IBAN" target are supported`
|
|
|
|
: !IBAN_REGEX.test(parsed.iban)
|
|
|
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
2023-03-05 19:21:12 +01:00
|
|
|
: validateIBAN(parsed.iban, i18n),
|
2023-02-28 23:03:43 +01:00
|
|
|
contact_data: undefinedIfEmpty({
|
|
|
|
email: !newForm.contact_data?.email
|
2023-03-05 19:21:12 +01:00
|
|
|
? i18n.str`required`
|
2023-02-08 21:41:19 +01:00
|
|
|
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
|
|
|
? i18n.str`it should be an email`
|
|
|
|
: undefined,
|
2023-02-28 23:03:43 +01:00
|
|
|
phone: !newForm.contact_data?.phone
|
2023-03-05 19:21:12 +01:00
|
|
|
? i18n.str`required`
|
2023-02-08 21:41:19 +01:00
|
|
|
: !newForm.contact_data.phone.startsWith("+")
|
|
|
|
? i18n.str`should start with +`
|
|
|
|
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
|
|
|
? i18n.str`phone number can't have other than numbers`
|
|
|
|
: undefined,
|
2023-02-28 23:03:43 +01:00
|
|
|
}),
|
2023-02-08 21:41:19 +01:00
|
|
|
iban: !newForm.iban
|
2023-03-05 19:21:12 +01:00
|
|
|
? undefined //optional field
|
2023-02-08 21:41:19 +01:00
|
|
|
: !IBAN_REGEX.test(newForm.iban)
|
|
|
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
2023-03-05 19:21:12 +01:00
|
|
|
: validateIBAN(newForm.iban, i18n),
|
2023-02-08 21:41:19 +01:00
|
|
|
name: !newForm.name ? i18n.str`required` : undefined,
|
|
|
|
username: !newForm.username ? i18n.str`required` : undefined,
|
|
|
|
});
|
2023-02-28 23:03:43 +01:00
|
|
|
setErrors(errors);
|
2023-02-08 21:41:19 +01:00
|
|
|
setForm(newForm);
|
2023-02-28 23:03:43 +01:00
|
|
|
onChange(errors === undefined ? (newForm as any) : undefined);
|
2023-02-08 21:41:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<form class="pure-form">
|
|
|
|
<fieldset>
|
2023-03-05 19:21:12 +01:00
|
|
|
<label for="username">
|
|
|
|
{i18n.str`Username`}
|
|
|
|
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
|
|
|
</label>
|
2023-02-08 21:41:19 +01:00
|
|
|
<input
|
|
|
|
name="username"
|
|
|
|
type="text"
|
|
|
|
disabled={purpose !== "create"}
|
|
|
|
value={form.username}
|
|
|
|
onChange={(e) => {
|
|
|
|
form.username = e.currentTarget.value;
|
|
|
|
updateForm(structuredClone(form));
|
|
|
|
}}
|
2023-03-05 19:21:12 +01:00
|
|
|
/>{" "}
|
2023-02-08 21:41:19 +01:00
|
|
|
<ShowInputErrorLabel
|
|
|
|
message={errors?.username}
|
|
|
|
isDirty={form.username !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
|
|
|
<fieldset>
|
2023-03-05 19:21:12 +01:00
|
|
|
<label>
|
|
|
|
{i18n.str`Name`}
|
|
|
|
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
|
|
|
</label>
|
2023-02-08 21:41:19 +01:00
|
|
|
<input
|
|
|
|
disabled={purpose !== "create"}
|
|
|
|
value={form.name ?? ""}
|
|
|
|
onChange={(e) => {
|
|
|
|
form.name = e.currentTarget.value;
|
|
|
|
updateForm(structuredClone(form));
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<ShowInputErrorLabel
|
|
|
|
message={errors?.name}
|
|
|
|
isDirty={form.name !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
2023-03-05 19:21:12 +01:00
|
|
|
{purpose !== "create" && (
|
|
|
|
<fieldset>
|
|
|
|
<label>{i18n.str`Internal IBAN`}</label>
|
|
|
|
<input
|
|
|
|
disabled={true}
|
|
|
|
value={form.iban ?? ""}
|
|
|
|
onChange={(e) => {
|
|
|
|
form.iban = e.currentTarget.value;
|
|
|
|
updateForm(structuredClone(form));
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<ShowInputErrorLabel
|
|
|
|
message={errors?.iban}
|
|
|
|
isDirty={form.iban !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
|
|
|
)}
|
2023-02-08 21:41:19 +01:00
|
|
|
<fieldset>
|
2023-03-05 19:21:12 +01:00
|
|
|
<label>
|
|
|
|
{i18n.str`Email`}
|
|
|
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
|
|
|
</label>
|
2023-02-08 21:41:19 +01:00
|
|
|
<input
|
|
|
|
disabled={purpose === "show"}
|
|
|
|
value={form.contact_data.email ?? ""}
|
|
|
|
onChange={(e) => {
|
|
|
|
form.contact_data.email = e.currentTarget.value;
|
|
|
|
updateForm(structuredClone(form));
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<ShowInputErrorLabel
|
2023-02-28 23:03:43 +01:00
|
|
|
message={errors?.contact_data?.email}
|
2023-02-08 21:41:19 +01:00
|
|
|
isDirty={form.contact_data.email !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
|
|
|
<fieldset>
|
2023-03-05 19:21:12 +01:00
|
|
|
<label>
|
|
|
|
{i18n.str`Phone`}
|
|
|
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
|
|
|
</label>
|
2023-02-08 21:41:19 +01:00
|
|
|
<input
|
|
|
|
disabled={purpose === "show"}
|
|
|
|
value={form.contact_data.phone ?? ""}
|
|
|
|
onChange={(e) => {
|
|
|
|
form.contact_data.phone = e.currentTarget.value;
|
|
|
|
updateForm(structuredClone(form));
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<ShowInputErrorLabel
|
2023-02-28 23:03:43 +01:00
|
|
|
message={errors?.contact_data?.phone}
|
2023-02-08 21:41:19 +01:00
|
|
|
isDirty={form.contact_data?.phone !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
|
|
|
<fieldset>
|
2023-03-05 19:21:12 +01:00
|
|
|
<label>
|
|
|
|
{i18n.str`Cashout address`}
|
|
|
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
|
|
|
</label>
|
2023-02-08 21:41:19 +01:00
|
|
|
<input
|
|
|
|
disabled={purpose === "show"}
|
2023-03-05 19:21:12 +01:00
|
|
|
value={(form.cashout_address ?? "").substring("payto://iban/".length)}
|
2023-02-08 21:41:19 +01:00
|
|
|
onChange={(e) => {
|
2023-03-05 19:21:12 +01:00
|
|
|
form.cashout_address = "payto://iban/" + e.currentTarget.value;
|
2023-02-08 21:41:19 +01:00
|
|
|
updateForm(structuredClone(form));
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<ShowInputErrorLabel
|
|
|
|
message={errors?.cashout_address}
|
|
|
|
isDirty={form.cashout_address !== undefined}
|
|
|
|
/>
|
|
|
|
</fieldset>
|
|
|
|
</form>
|
|
|
|
);
|
|
|
|
}
|