more ui: business and admin

This commit is contained in:
Sebastian 2023-09-21 13:10:16 -03:00
parent 062939d9cc
commit 0b7bbed99d
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
19 changed files with 1196 additions and 906 deletions

View File

@ -33,12 +33,7 @@ export function Routing(): VNode {
const backend = useBackendContext();
if (backend.state.status === "loggedOut") {
return <BankFrame
account={undefined}
goToBusinessAccount={() => {
route("/business");
}}
>
return <BankFrame >
<Router history={history}>
<Route
path="/login"
@ -67,12 +62,7 @@ export function Routing(): VNode {
const { isUserAdministrator, username } = backend.state
return (
<BankFrame
account={backend.state.username}
goToBusinessAccount={() => {
route("/business");
}}
>
<BankFrame account={backend.state.username}>
<Router history={history}>
<Route
path="/test"
@ -121,6 +111,9 @@ export function Routing(): VNode {
onPendingOperationFound={(wopid) => {
route(`/operation/${wopid}`);
}}
goToBusinessAccount={() => {
route("/business");
}}
onRegister={() => {
route("/register");
}}

View File

@ -24,6 +24,6 @@ export function ShowInputErrorLabel({
isDirty: boolean;
}): VNode {
if (message && isDirty)
return <div style={{ marginTop: 8, color: "red" }}>{message}</div>;
return <Fragment />;
return <div class="text-base" style={{ color: "red" }}>{message}</div>;
return <div class="text-base" style={{ }}> </div>;
}

View File

@ -28,6 +28,7 @@ export interface Props {
onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
goToBusinessAccount: () => void;
}
export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
@ -51,10 +52,8 @@ export namespace State {
status: "ready";
error: undefined;
account: string,
payto: PaytoUriIBAN | PaytoUriTalerBank,
balance: AmountJson,
balanceIsDebit: boolean,
limit: AmountJson,
goToBusinessAccount: () => void;
}
export interface InvalidIban {

View File

@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js";
import { useAccountDetails } from "../../hooks/access.js";
import { Props, State } from "./index.js";
export function useComponentState({ account, onLoadNotOk }: Props): State {
export function useComponentState({ account, goToBusinessAccount }: Props): State {
const result = useAccountDetails(account);
const backend = useBackendContext();
const { i18n } = useTranslationContext();
@ -60,7 +60,6 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {
const payto = parsePaytoUri(data.paytoUri);
if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
console.log(payto)
return {
status: "invalid-iban",
error: result
@ -75,11 +74,9 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {
return {
status: "ready",
goToBusinessAccount,
error: undefined,
account,
balance,
balanceIsDebit,
limit,
payto
};
}

View File

@ -22,6 +22,7 @@ import { PaymentOptions } from "../PaymentOptions.js";
import { State } from "./index.js";
import { CopyButton } from "../../components/CopyButton.js";
import { bankUiSettings } from "../../settings.js";
import { useBusinessAccountDetails } from "../../hooks/circuit.js";
export function InvalidIbanView({ error }: State.InvalidIban) {
return (
@ -77,11 +78,35 @@ function ImportantMessage(): VNode {
}
export function ReadyView({ account, balance, balanceIsDebit, limit, payto }: State.Ready): VNode<{}> {
const { i18n } = useTranslationContext();
export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> {
return <Fragment>
<MaybeBusinessButton account={account} onClick={goToBusinessAccount} />
<PaymentOptions limit={limit} />
<Transactions account={account} />
</Fragment>;
}
function MaybeBusinessButton({
account,
onClick,
}: {
account: string;
onClick: () => void;
}): VNode {
const { i18n } = useTranslationContext();
const result = useBusinessAccountDetails(account);
if (!result.ok) return <Fragment />;
return (
<div class="w-full flex justify-end">
<button
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={(e) => {
e.preventDefault()
onClick()
}}
>
<i18n.Translate>Business Profile</i18n.Translate>
</button>
</div>
);
}

View File

@ -39,36 +39,12 @@ const versionText = VERSION
const logger = new Logger("BankFrame");
function MaybeBusinessButton({
account,
onClick,
}: {
account: string;
onClick: () => void;
}): VNode {
const { i18n } = useTranslationContext();
const result = useBusinessAccountDetails(account);
if (!result.ok) return <Fragment />;
return (
<a
href="#"
class="pure-button pure-button-primary"
onClick={(e) => {
e.preventDefault();
onClick();
}}
>{i18n.str`Business Profile`}</a>
);
}
export function BankFrame({
children,
goToBusinessAccount,
account,
}: {
account: string | undefined,
account?: string,
children: ComponentChildren;
goToBusinessAccount?: () => void;
}): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
@ -489,5 +465,9 @@ function AccountBalance({ account }: { account: string }): VNode {
const result = useAccountDetails(account);
if (!result.ok) return <div />
return <div>{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""} {Amounts.currencyOf(result.data.balance.amount)} {Amounts.stringifyValue(result.data.balance.amount)}</div>
return <div>
{Amounts.currencyOf(result.data.balance.amount)}
&nbsp;{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""}
{Amounts.stringifyValue(result.data.balance.amount)}
</div>
}

View File

@ -31,14 +31,11 @@ import {
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js";
import { useBackendContext } from "../context/backend.js";
import { getInitialBackendBaseURL } from "../hooks/backend.js";
import { useSettings } from "../hooks/settings.js";
import { AccountPage } from "./AccountPage/index.js";
import { AdminHome } from "./admin/Home.js";
import { LoginForm } from "./LoginForm.js";
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
import { error } from "console";
const logger = new Logger("AccountPage");
@ -56,10 +53,12 @@ export function HomePage({
onRegister,
account,
onPendingOperationFound,
goToBusinessAccount,
}: {
account: string,
onPendingOperationFound: (id: string) => void;
onRegister: () => void;
goToBusinessAccount: () => void;
}): VNode {
const [settings] = useSettings();
const { i18n } = useTranslationContext();
@ -72,6 +71,7 @@ export function HomePage({
return (
<AccountPage
account={account}
goToBusinessAccount={goToBusinessAccount}
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
/>
);

View File

@ -33,76 +33,79 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
// const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined);
return (<fieldset>
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Send money to</i18n.Translate>
</legend>
return (
<fieldset>
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Send money to</i18n.Translate>
</legend>
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
<input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => {
setTab("charge-wallet")
}} />
<span class="flex flex-1">
<span class="flex flex-col">
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900">
<i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
</span>
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
<input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => {
setTab("charge-wallet")
}} />
<span class="flex flex-1">
<span class="flex flex-col">
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900">
<i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
</span>
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
</span>
</span>
</span>
</span>
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
</svg>
</label>
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
</svg>
</label>
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
<input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => {
setTab("wire-transfer")
}} />
<span class="flex flex-1">
<span class="flex flex-col">
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
<i18n.Translate>another bank account</i18n.Translate>
</span>
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
<i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate>
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
<input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => {
setTab("wire-transfer")
}} />
<span class="flex flex-1">
<span class="flex flex-col">
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
<i18n.Translate>another bank account</i18n.Translate>
</span>
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
<i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate>
</span>
</span>
</span>
</span>
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
</svg>
</label>
</div>
{tab === "charge-wallet" && (
<WalletWithdrawForm
focus
limit={limit}
onSuccess={(id) => {
updateSettings("currentWithdrawalOperationId", id);
}}
onCancel={() => {
setTab(undefined)
}}
/>
)}
{tab === "wire-transfer" && (
<PaytoWireTransferForm
focus
limit={limit}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
}}
onCancel={() => {
setTab(undefined)
}}
/>
)}
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
</svg>
</label>
</div>
{tab === "charge-wallet" && (
<WalletWithdrawForm
focus
limit={limit}
onSuccess={(id) => {
updateSettings("currentWithdrawalOperationId", id);
}}
onCancel={() => {
setTab(undefined)
}}
/>
)}
{tab === "wire-transfer" && (
<PaytoWireTransferForm
focus
title={i18n.str`Transfer details`}
limit={limit}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
}}
onCancel={() => {
setTab(undefined)
}}
/>
)}
</fieldset>)
</fieldset>
)
}

View File

@ -44,10 +44,12 @@ const logger = new Logger("PaytoWireTransferForm");
export function PaytoWireTransferForm({
focus,
title,
onSuccess,
onCancel,
limit,
}: {
title: TranslatedString,
focus?: boolean;
onSuccess: () => void;
onCancel: (() => void) | undefined;
@ -158,7 +160,9 @@ export function PaytoWireTransferForm({
return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Transfer details</i18n.Translate></h2>
<h2 class="text-base font-semibold leading-7 text-gray-900">
{title}
</h2>
<div>
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 sm:gap-x-4">
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>

View File

@ -45,7 +45,7 @@ export function RegistrationPage({
return <RegistrationForm onComplete={onComplete} />;
}
export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
/**
* Collect and submit registration data.

View File

@ -1,5 +1,5 @@
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode,h } from "preact";
import { VNode, h } from "preact";
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
import { useState } from "preact/hooks";
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
@ -7,137 +7,161 @@ import { buildRequestErrorMessage } from "../utils.js";
import { AccountForm } from "./admin/AccountForm.js";
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>;
}
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);
}
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>
if (result.status === HttpStatusCode.NotFound) {
return <div>account not found</div>;
}
return onLoadNotOk(result);
}
async function doUpdate() {
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
)
}
}
}
}
return (
<div>
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
{update ?
<i18n.Translate>Update account</i18n.Translate>
:
<i18n.Translate>Account details</i18n.Translate>
}
</h2>
<div class="mt-4">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span class="text-sm text-black font-medium leading-6 " id="availability-label">
<i18n.Translate>change the account details</i18n.Translate>
</span>
</span>
<button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
onClick={() => {
setUpdate(!update)
}}>
<span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</div>
);
}
</div>
<AccountForm
template={result.data}
purpose={update ? "update" : "show"}
onChange={(a) => setSubmitAccount(a)}
>
</AccountForm>
<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();
doUpdate()
}}
/>
</div>
</div>
</div>
</p>
</div>
</div>
);
}

View File

@ -1,131 +1,181 @@
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
import { useState } from "preact/hooks";
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
import { VNode,h ,Fragment} from "preact";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
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>;
}
account,
onCancel,
onUpdateSuccess,
onLoadNotOk,
focus,
}: {
onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
onCancel: () => void;
focus?: boolean,
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>();
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
if (!result.ok) {
if (result.loading || result.type === ErrorType.TIMEOUT) {
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>
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,
});
async function doChangePassword() {
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)
}
}
}
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
<i18n.Translate>Update password for account "{account}"</i18n.Translate>
</h2>
</div>
);
}
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
onSubmit={e => {
e.preventDefault()
}}
>
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="password"
>
{i18n.str`New password`}
</label>
<div class="mt-2">
<input
ref={ref}
type="password"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="password"
id="password"
data-error={!!errors?.password && password !== undefined}
value={password ?? ""}
onChange={(e) => {
setPassword(e.currentTarget.value)
}}
// placeholder=""
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.password}
isDirty={password !== undefined}
/>
</div>
{/* <p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>user </i18n.Translate>
</p> */}
</div>
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="repeat"
>
{i18n.str`Type it again`}
</label>
<div class="mt-2">
<input
type="password"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="repeat"
id="repeat"
data-error={!!errors?.repeat && repeat !== undefined}
value={repeat ?? ""}
onChange={(e) => {
setRepeat(e.currentTarget.value)
}}
// placeholder=""
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.repeat}
isDirty={repeat !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>repeat the same password</i18n.Translate>
</p>
</div>
</div>
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
{onCancel ?
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
onClick={onCancel}
>
<i18n.Translate>Cancel</i18n.Translate>
</button>
: <div />
}
<button type="submit"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
disabled={!!errors}
onClick={(e) => {
e.preventDefault()
doChangePassword()
}}
>
<i18n.Translate>Change</i18n.Translate>
</button>
</div>
</form>
</div>
);
}

View File

@ -7,50 +7,30 @@ import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
export 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 { 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 (
<PaytoWireTransferForm
title={i18n.str`Make a wire transfer`}
limit={limit}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
}}
onCancel={undefined}
/>
);
}

View File

@ -1,9 +1,9 @@
import { VNode,h } from "preact";
import { ComponentChildren, VNode, h } from "preact";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
import { useState } from "preact/hooks";
import { useEffect, useRef, useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { parsePaytoUri } from "@gnu-taler/taler-util";
import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
const EMAIL_REGEX =
@ -19,201 +19,301 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
* @returns
*/
export 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
template,
purpose,
onChange,
focus,
children,
}: {
focus?: boolean,
children: ComponentChildren,
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();
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
function updateForm(newForm: typeof initial): void {
const parsed = !newForm.cashout_address
? undefined
: buildPayto("iban", newForm.cashout_address, undefined);;
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`
: !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`
: !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,
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>
);
}),
// 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);
}
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;
return (
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
onSubmit={e => {
e.preventDefault()
}}
>
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="username"
>
{i18n.str`Username`}
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
</label>
<div class="mt-2">
<input
ref={ref}
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="username"
id="username"
data-error={!!errors?.username && form.username !== undefined}
disabled={purpose !== "create"}
value={form.username ?? ""}
onChange={(e) => {
form.username = e.currentTarget.value;
updateForm(structuredClone(form));
}}
// placeholder=""
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.username}
isDirty={form.username !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>account identification in the bank</i18n.Translate>
</p>
</div>
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="name"
>
{i18n.str`Name`}
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
</label>
<div class="mt-2">
<input
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="name"
data-error={!!errors?.name && form.name !== undefined}
id="name"
disabled={purpose !== "create"}
value={form.name ?? ""}
onChange={(e) => {
form.name = e.currentTarget.value;
updateForm(structuredClone(form));
}}
// placeholder=""
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.name}
isDirty={form.name !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>name of the person owner the account</i18n.Translate>
</p>
</div>
{purpose !== "create" && (<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="internal-iban"
>
{i18n.str`Internal IBAN`}
</label>
<div class="mt-2">
<input
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="internal-iban"
id="internal-iban"
disabled={true}
value={form.iban ?? ""}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>international bank account number</i18n.Translate>
</p>
</div>)}
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="email"
>
{i18n.str`Email`}
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
</label>
<div class="mt-2">
<input
type="email"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="email"
id="email"
data-error={!!errors?.contact_data?.email && form.contact_data.email !== undefined}
disabled={purpose !== "create"}
value={form.contact_data.email ?? ""}
onChange={(e) => {
form.contact_data.email = e.currentTarget.value;
updateForm(structuredClone(form));
}}
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.contact_data?.email}
isDirty={form.contact_data.email !== undefined}
/>
</div>
</div>
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="phone"
>
{i18n.str`Phone`}
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
</label>
<div class="mt-2">
<input
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="phone"
id="phone"
disabled={purpose !== "create"}
value={form.contact_data.phone ?? ""}
data-error={!!errors?.contact_data?.phone && form.contact_data.phone !== undefined}
onChange={(e) => {
form.contact_data.phone = e.currentTarget.value;
updateForm(structuredClone(form));
}}
// placeholder=""
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.contact_data?.phone}
isDirty={form.contact_data.phone !== undefined}
/>
</div>
</div>
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="cashout"
>
{i18n.str`Cashout IBAN`}
{purpose !== "show" && <b style={{ color: "red" }}> *</b>}
</label>
<div class="mt-2">
<input
type="text"
data-error={!!errors?.cashout_address && form.cashout_address !== undefined}
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="cashout"
id="cashout"
disabled={purpose === "show"}
value={form.cashout_address ?? ""}
onChange={(e) => {
form.cashout_address = e.currentTarget.value;
updateForm(structuredClone(form));
}}
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.cashout_address}
isDirty={form.cashout_address !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>account number where the money is going to be sent when doing cashouts</i18n.Translate>
</p>
</div>
</div>
</div>
{children}
</form>
);
}
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;
}

View File

@ -9,10 +9,10 @@ interface Props {
onAction: (type: AccountAction, account: string) => void;
account: string | undefined;
onRegister: () => void;
onCreateAccount: () => void;
}
export function AccountList({ account, onAction, onRegister }: Props): VNode {
export function AccountList({ account, onAction, onCreateAccount, onRegister }: Props): VNode {
const result = useBusinessAccounts({ account });
const { i18n } = useTranslationContext();
@ -22,48 +22,60 @@ export function AccountList({ account, onAction, onRegister }: Props): VNode {
}
const { customers } = result.data;
return <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();
onAction("show-details", item.username)
}}
>
{item.username}
</a>
return <div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Accounts</i18n.Translate>
</h1>
<p class="mt-2 text-sm text-gray-700">
<i18n.Translate>A list of all business account in the bank.</i18n.Translate>
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<button type="button" class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={(e) => {
e.preventDefault()
onCreateAccount()
}}>
<i18n.Translate>Create account</i18n.Translate>
</button>
</div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
{!customers.length ? (
<div></div>
) : (
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">{i18n.str`Username`}</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Name`}</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Balance`}</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">{i18n.str`Actions`}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{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 class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
{item.username}
</td>
<td>{item.name}</td>
<td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{item.name}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{!balance ? (
i18n.str`unknown`
) : (
@ -77,9 +89,8 @@ export function AccountList({ account, onAction, onRegister }: Props): VNode {
</span>
)}
</td>
<td>
<a
href="#"
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<a href="#" class="text-indigo-600 hover:text-indigo-900"
onClick={(e) => {
e.preventDefault();
onAction("update-password", item.username)
@ -87,34 +98,71 @@ export function AccountList({ account, onAction, onRegister }: Props): VNode {
>
change password
</a>
&nbsp;
<a
href="#"
onClick={(e) => {
e.preventDefault();
onAction("show-cashout", item.username)
}}
<br/>
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
e.preventDefault();
onAction("show-cashout", item.username)
}}
>
cashouts
</a>
&nbsp;
<a
href="#"
onClick={(e) => {
e.preventDefault();
onAction("remove-account", item.username)
}}
<br/>
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
e.preventDefault();
onAction("remove-account", item.username)
}}
>
remove
</a>
</td>
</tr>
);
})}
</tbody>
</table>
})}
{/* <!-- More people... --> */}
</tbody>
</table>
)}
</div>
</article>
)}
</section>
</div>
</div>
</div>
// return <section
// id="main"
// style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
// >
// <article>
// <h2>{i18n.str`Accounts:`}</h2>
// <div class="results">
// <table class="pure-table pure-table-striped">
// <tbody>
// return (
// <tr key={idx}>
// <td>
// <a
// href="#"
// onClick={(e) => {
// e.preventDefault();
// onAction("show-details", item.username)
// }}
// >
// {item.username}
// </a>
// </td>
// <td>{item.name}</td>
// <td>
//
// </td>
// <td>
// </td>
// </tr>
// );
// })}
// </tbody>
// </table>
// </div>
// </article>
// )}
// </section>
}

View File

@ -8,100 +8,94 @@ import { getRandomPassword } from "../rnd.js";
import { AccountForm } from "./AccountForm.js";
export function CreateNewAccount({
onClose,
onCreateSuccess,
onCancel,
onCreateSuccess,
}: {
onClose: () => void;
onCreateSuccess: (password: string) => void;
onCancel: () => 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>
const { i18n } = useTranslationContext();
const { createAccount } = useAdminAccountAPI();
const [submitAccount, setSubmitAccount] = useState<
SandboxBackend.Circuit.CircuitAccountData | undefined
>();
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
<AccountForm
template={undefined}
purpose="create"
onChange={(a) => {
setSubmitAccount(a);
}}
/>
async function doCreate() {
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: getRandomPassword(),
};
<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();
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`Server replied that 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
)
}
}
}
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: getRandomPassword(),
};
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>
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
<i18n.Translate>New business account</i18n.Translate>
</h2>
</div>
<AccountForm
template={undefined}
purpose="create"
onChange={(a) => {
setSubmitAccount(a);
}}
>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
{onCancel ?
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
onClick={onCancel}
>
<i18n.Translate>Cancel</i18n.Translate>
</button>
: <div />
}
<button type="submit"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
disabled={!submitAccount}
onClick={(e) => {
e.preventDefault()
doCreate()
}}
>
<i18n.Translate>Create</i18n.Translate>
</button>
</div>
);
</AccountForm>
</div>
);
}

View File

@ -17,17 +17,20 @@ import { RemoveAccount } from "./RemoveAccount.js";
interface Props {
onRegister: () => void;
}
export type AccountAction = "show-details" |
"show-cashout" |
"update-password" |
"remove-account" |
export type AccountAction = "show-details" |
"show-cashout" |
"update-password" |
"remove-account" |
"show-cashouts-details";
export function AdminHome({ onRegister }: Props): VNode {
const [action, setAction] = useState<{
type: AccountAction,
account: string
}>()
} | undefined>({
type:"remove-account",
account:"gnunet-at-sandbox"
})
const [createAccount, setCreateAccount] = useState(false);
@ -78,7 +81,7 @@ export function AdminHome({ onRegister }: Props): VNode {
notifyInfo(i18n.str`Password changed`);
setAction(undefined);
}}
onClear={() => {
onCancel={() => {
setAction(undefined);
}}
/>
@ -89,7 +92,7 @@ export function AdminHome({ onRegister }: Props): VNode {
notifyInfo(i18n.str`Account removed`);
setAction(undefined);
}}
onClear={() => {
onCancel={() => {
setAction(undefined);
}}
/>
@ -116,7 +119,7 @@ export function AdminHome({ onRegister }: Props): VNode {
if (createAccount) {
return (
<CreateNewAccount
onClose={() => setCreateAccount(false)}
onCancel={() => setCreateAccount(false)}
onCreateSuccess={(password) => {
notifyInfo(
i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
@ -129,34 +132,18 @@ export function AdminHome({ onRegister }: Props): VNode {
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>
<AccountList
onCreateAccount={() => {
setCreateAccount(true);
}}
account={undefined}
onAction={(type, account) => setAction({ account, type })}
onRegister={onRegister}
/>
<AdminAccount onRegister={onRegister} />
<AccountList account={undefined} onAction={(type,account) => setAction({account, type})} onRegister={onRegister}/>
</Fragment>
);
}

View File

@ -1,112 +1,218 @@
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode,h,Fragment } from "preact";
import { VNode, h, Fragment } from "preact";
import { useAccountDetails } from "../../hooks/access.js";
import { useAdminAccountAPI } from "../../hooks/circuit.js";
import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
import { buildRequestErrorMessage } from "../../utils.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
export 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>;
}
account,
onCancel,
onUpdateSuccess,
onLoadNotOk,
focus,
}: {
onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
focus?: boolean;
onCancel: () => void;
onUpdateSuccess: () => void;
account: string;
}): VNode {
const { i18n } = useTranslationContext();
const result = useAccountDetails(account);
const [accountName, setAccountName] = useState<string | undefined>()
const { deleteAccount } = useAdminAccountAPI();
if (!result.ok) {
if (result.loading || result.type === ErrorType.TIMEOUT) {
return onLoadNotOk(result);
}
const balance = Amounts.parse(result.data.balance.amount);
if (!balance) {
return <div>there was an error reading the balance</div>;
if (result.status === HttpStatusCode.NotFound) {
return <div>account not found</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);
}
}
}}
/>
return onLoadNotOk(result);
}
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
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);
if (!isBalanceEmpty) {
return <div>
<div class="rounded-md bg-yellow-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">
<i18n.Translate>Can't delete the account</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<i18n.Translate>The account can be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate>
</p>
</div>
</div>
</p>
</div>
</div>
);
<div class="mt-2 flex justify-end">
<button type="button" class="rounded-md ring-1 ring-gray-400 bg-white px-3 py-2 text-sm font-semibold shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
onClick={() => {
onCancel()
}}>
<i18n.Translate>Go back</i18n.Translate>
</button>
</div>
</div>
}
async function doRemove() {
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);
}
}
}
const errors = undefinedIfEmpty({
accountName: !accountName
? i18n.str`required`
: account !== accountName
? i18n.str`name doesn't match`
: undefined,
});
return (
<div>
<div class="rounded-md bg-yellow-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-bold text-yellow-800">
<i18n.Translate>You are going to remove the account</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<i18n.Translate>This step can't be undone.</i18n.Translate>
</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
<i18n.Translate>Deleting account "{account}"</i18n.Translate>
</h2>
</div>
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
onSubmit={e => {
e.preventDefault()
}}
>
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="password"
>
{i18n.str`Verification`}
</label>
<div class="mt-2">
<input
ref={ref}
type="text"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="password"
id="password"
data-error={!!errors?.accountName && accountName !== undefined}
value={accountName ?? ""}
onChange={(e) => {
setAccountName(e.currentTarget.value)
}}
placeholder={account}
autocomplete="off"
/>
<ShowInputErrorLabel
message={errors?.accountName}
isDirty={accountName !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>enter the account name that is going to be deleted</i18n.Translate>
</p>
</div>
</div>
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
{onCancel ?
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
onClick={onCancel}
>
<i18n.Translate>Cancel</i18n.Translate>
</button>
: <div />
}
<button type="submit"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
disabled={!!errors}
onClick={(e) => {
e.preventDefault()
doRemove()
}}
>
<i18n.Translate>Delete</i18n.Translate>
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -109,7 +109,7 @@ export function BusinessAccount({
notifyInfo(i18n.str`Password changed`);
setUpdatePassword(false);
}}
onClear={() => {
onCancel={() => {
setUpdatePassword(false);
}}
/>