summaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/AdminPage.tsx
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-09-21 10:31:10 -0300
committerSebastian <sebasjm@gmail.com>2023-09-25 14:50:41 -0300
commit062939d9cc016a186a282f7a48492c3e01cd740c (patch)
treea52c93ef1179ece9d8621731d4a34fc654f18713 /packages/demobank-ui/src/pages/AdminPage.tsx
parentb3c747151bb3f50d28bf6205cafa4b7dd6ae2b1c (diff)
downloadwallet-core-062939d9cc016a186a282f7a48492c3e01cd740c.tar.gz
wallet-core-062939d9cc016a186a282f7a48492c3e01cd740c.tar.bz2
wallet-core-062939d9cc016a186a282f7a48492c3e01cd740c.zip
admin refactor
Diffstat (limited to 'packages/demobank-ui/src/pages/AdminPage.tsx')
-rw-r--r--packages/demobank-ui/src/pages/AdminPage.tsx1042
1 files changed, 0 insertions, 1042 deletions
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
deleted file mode 100644
index 18462bdc3..000000000
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ /dev/null
@@ -1,1042 +0,0 @@
-/*
- 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 { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpResponsePaginated,
- RequestError,
- notify,
- notifyError,
- notifyInfo,
- useTranslationContext,
-} from "@gnu-taler/web-util/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 { useAccountDetails } from "../hooks/access.js";
-import {
- useAdminAccountAPI,
- useBusinessAccountDetails,
- useBusinessAccounts,
-} from "../hooks/circuit.js";
-import {
- buildRequestErrorMessage,
- PartialButDefined,
- RecursivePartial,
- undefinedIfEmpty,
- validateIBAN,
- WithIntermediate,
-} from "../utils.js";
-import { ShowCashoutDetails } from "./BusinessAccount.js";
-import { handleNotOkResult } from "./HomePage.js";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { ShowInputErrorLabel } from "../components/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 {
- onRegister: () => void;
-}
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-export function AdminPage({ onRegister }: Props): VNode {
- const [account, setAccount] = useState<string | undefined>();
- const [showDetails, setShowDetails] = useState<string | undefined>();
- const [showCashouts, setShowCashouts] = useState<string | undefined>();
- const [updatePassword, setUpdatePassword] = useState<string | undefined>();
- const [removeAccount, setRemoveAccount] = useState<string | undefined>();
- const [showCashoutDetails, setShowCashoutDetails] = useState<
- string | undefined
- >();
-
- const [createAccount, setCreateAccount] = useState(false);
-
- const result = useBusinessAccounts({ account });
- const { i18n } = useTranslationContext();
-
- if (result.loading) return <div />;
- if (!result.ok) {
- return handleNotOkResult(i18n, onRegister)(result);
- }
-
- const { customers } = result.data;
-
- if (showCashoutDetails) {
- return (
- <ShowCashoutDetails
- id={showCashoutDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onCancel={() => {
- setShowCashoutDetails(undefined);
- }}
- />
- );
- }
-
- if (showCashouts) {
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Cashout for account {showCashouts}</i18n.Translate>
- </h1>
- </div>
- <Cashouts
- account={showCashouts}
- onSelected={(id) => {
- setShowCashouts(id);
- setShowCashouts(undefined);
- }}
- />
- <p>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- setShowCashouts(undefined);
- }}
- />
- </p>
- </div>
- );
- }
-
- if (showDetails) {
- return (
- <ShowAccountDetails
- account={showDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onChangePassword={() => {
- setUpdatePassword(showDetails);
- setShowDetails(undefined);
- }}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- setShowDetails(undefined);
- }}
- onClear={() => {
- setShowDetails(undefined);
- }}
- />
- );
- }
- if (removeAccount) {
- return (
- <RemoveAccount
- account={removeAccount}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account removed`);
- setRemoveAccount(undefined);
- }}
- onClear={() => {
- setRemoveAccount(undefined);
- }}
- />
- );
- }
- if (updatePassword) {
- return (
- <UpdateAccountPassword
- account={updatePassword}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Password changed`);
- setUpdatePassword(undefined);
- }}
- onClear={() => {
- setUpdatePassword(undefined);
- }}
- />
- );
- }
- if (createAccount) {
- return (
- <CreateNewAccount
- onClose={() => setCreateAccount(false)}
- onCreateSuccess={(password) => {
- notifyInfo(
- i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
- );
- 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>
-
- <AdminAccount onRegister={onRegister} />
- <section
- id="main"
- style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
- >
- {!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>
- <th>{i18n.str`Balance`}</th>
- <th>{i18n.str`Actions`}</th>
- </tr>
- </thead>
- <tbody>
- {customers.map((item, idx) => {
- const balance = !item.balance
- ? undefined
- : Amounts.parse(item.balance.amount);
- const balanceIsDebit =
- item.balance &&
- item.balance.credit_debit_indicator == "debit";
- return (
- <tr key={idx}>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowDetails(item.username);
- }}
- >
- {item.username}
- </a>
- </td>
- <td>{item.name}</td>
- <td>
- {!balance ? (
- i18n.str`unknown`
- ) : (
- <span class="amount">
- {balanceIsDebit ? <b>-</b> : null}
- <span class="value">{`${Amounts.stringifyValue(
- balance,
- )}`}</span>
- &nbsp;
- <span class="currency">{`${balance.currency}`}</span>
- </span>
- )}
- </td>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setUpdatePassword(item.username);
- }}
- >
- change password
- </a>
- &nbsp;
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowCashouts(item.username);
- }}
- >
- cashouts
- </a>
- &nbsp;
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setRemoveAccount(item.username);
- }}
- >
- remove
- </a>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </article>
- )}
- </section>
- </Fragment>
- );
-}
-
-function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
- const { i18n } = useTranslationContext();
- const r = useBackendContext();
- const account = r.state.status === "loggedIn" ? r.state.username : "admin";
- const result = useAccountDetails(account);
-
- if (!result.ok) {
- return handleNotOkResult(i18n, onRegister)(result);
- }
- const { data } = result;
- const balance = Amounts.parseOrThrow(data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
- const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
- if (!balance) return <Fragment />;
- return (
- <Fragment>
- <section id="assets">
- <div class="asset-summary">
- <h2>{i18n.str`Bank account balance`}</h2>
- {!balance ? (
- <div class="large-amount" style={{ color: "gray" }}>
- Waiting server response...
- </div>
- ) : (
- <div class="large-amount amount">
- {balanceIsDebit ? <b>-</b> : null}
- <span class="value">{`${Amounts.stringifyValue(balance)}`}</span>
- &nbsp;
- <span class="currency">{`${balance.currency}`}</span>
- </div>
- )}
- </div>
- </section>
- <PaytoWireTransferForm
- focus
- limit={limit}
- onSuccess={() => {
- notifyInfo(i18n.str`Wire transfer created!`);
- }}
- onCancel={undefined}
- />
- </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;
-}
-
-export function UpdateAccountPassword({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
-}: {
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
- onClear: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- const { changePassword } = useAdminAccountAPI();
- const [password, setPassword] = useState<string | undefined>();
- const [repeat, setRepeat] = useState<string | undefined>();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
- }
- return onLoadNotOk(result);
- }
-
- 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">
- <i18n.Translate>Update password for {account}</i18n.Translate>
- </h1>
- </div>
-
- <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
- <form class="pure-form">
- <fieldset>
- <label>{i18n.str`Password`}</label>
- <input
- type="password"
- value={password ?? ""}
- onChange={(e) => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>{i18n.str`Repeat password`}</label>
- <input
- type="password"
- value={repeat ?? ""}
- onChange={(e) => {
- setRepeat(e.currentTarget.value);
- }}
- />
- <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) {
- notify(buildRequestErrorMessage(i18n, error.cause));
- } else {
- notifyError(i18n.str`Operation failed, please report`, (error instanceof Error
- ? error.message
- : JSON.stringify(error)) as TranslatedString)
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- </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
- >();
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>New account</i18n.Translate>
- </h1>
- </div>
-
- <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
- <AccountForm
- template={undefined}
- purpose="create"
- onChange={(a) => {
- setSubmitAccount(a);
- }}
- />
-
- <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();
-
- 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) {
- notify(
- 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 {
- notifyError(
- i18n.str`Operation failed, please report`,
- (error instanceof Error
- ? error.message
- : JSON.stringify(error)) as TranslatedString
- )
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-export function ShowAccountDetails({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
- onChangePassword,
-}: {
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
- onClear?: () => void;
- onChangePassword: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- const { updateAccount } = useAdminAccountAPI();
- const [update, setUpdate] = useState(false);
- const [submitAccount, setSubmitAccount] = useState<
- SandboxBackend.Circuit.CircuitAccountData | undefined
- >();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
- }
- return onLoadNotOk(result);
- }
-
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Business account details</i18n.Translate>
- </h1>
- </div>
- <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
- <AccountForm
- template={result.data}
- purpose={update ? "update" : "show"}
- onChange={(a) => setSubmitAccount(a)}
- />
-
- <p class="buttons-account">
- <div
- style={{
- display: "flex",
- justifyContent: "space-between",
- flexFlow: "wrap-reverse",
- }}
- >
- <div>
- {onClear ? (
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- ) : undefined}
- </div>
- <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,
- });
- onUpdateSuccess();
- } catch (error) {
- if (error instanceof RequestError) {
- notify(
- 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 {
- notifyError(
- i18n.str`Operation failed, please report`,
- (error instanceof Error
- ? error.message
- : JSON.stringify(error)) as TranslatedString
- )
- }
- }
- }
- }}
- />
- </div>
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-function RemoveAccount({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
-}: {
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
- onClear: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useAccountDetails(account);
- const { deleteAccount } = useAdminAccountAPI();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
- }
- return onLoadNotOk(result);
- }
-
- 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>
- {/* {FXME: SHOW WARNING} */}
- {/* {!isBalanceEmpty && (
- <ErrorBannerFloat
- error={{
- title: i18n.str`Can't delete the account`,
- description: i18n.str`Balance is not empty`,
- }}
- onClear={() => saveError(undefined)}
- />
- )} */}
-
- <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) {
- if (error instanceof RequestError) {
- notify(
- 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 {
- notifyError(i18n.str`Operation failed, please report`,
- (error instanceof Error
- ? error.message
- : JSON.stringify(error)) as TranslatedString);
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- );
-}
-/**
- * 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);
- const [errors, setErrors] = useState<
- RecursivePartial<typeof initial> | undefined
- >(undefined);
- const { i18n } = useTranslationContext();
-
- function updateForm(newForm: typeof initial): void {
- const parsed = !newForm.cashout_address
- ? undefined
- : parsePaytoUri(newForm.cashout_address);
-
- const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
- 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`
- : validateIBAN(parsed.iban, i18n),
- contact_data: undefinedIfEmpty({
- email: !newForm.contact_data?.email
- ? i18n.str`required`
- : !EMAIL_REGEX.test(newForm.contact_data.email)
- ? i18n.str`it should be an email`
- : undefined,
- phone: !newForm.contact_data?.phone
- ? i18n.str`required`
- : !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,
- }),
- iban: !newForm.iban
- ? undefined //optional field
- : !IBAN_REGEX.test(newForm.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(newForm.iban, i18n),
- name: !newForm.name ? i18n.str`required` : undefined,
- username: !newForm.username ? i18n.str`required` : undefined,
- });
- setErrors(errors);
- setForm(newForm);
- onChange(errors === undefined ? (newForm as any) : undefined);
- }
-
- return (
- <form class="pure-form">
- <fieldset>
- <label for="username">
- {i18n.str`Username`}
- {purpose === "create" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- name="username"
- type="text"
- disabled={purpose !== "create"}
- value={form.username}
- onChange={(e) => {
- form.username = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />{" "}
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={form.username !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>
- {i18n.str`Name`}
- {purpose === "create" && <b style={{ color: "red" }}>*</b>}
- </label>
- <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>
- {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>
- )}
- <fieldset>
- <label>
- {i18n.str`Email`}
- {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- disabled={purpose === "show"}
- value={form.contact_data.email ?? ""}
- onChange={(e) => {
- form.contact_data.email = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.contact_data?.email}
- isDirty={form.contact_data.email !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>
- {i18n.str`Phone`}
- {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- disabled={purpose === "show"}
- value={form.contact_data.phone ?? ""}
- onChange={(e) => {
- form.contact_data.phone = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.contact_data?.phone}
- isDirty={form.contact_data?.phone !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>
- {i18n.str`Cashout address`}
- {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- disabled={purpose === "show"}
- value={(form.cashout_address ?? "").substring("payto://iban/".length)}
- onChange={(e) => {
- form.cashout_address = "payto://iban/" + e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.cashout_address}
- isDirty={form.cashout_address !== undefined}
- />
- </fieldset>
- </form>
- );
-}