aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-09-21 13:10:16 -0300
committerSebastian <sebasjm@gmail.com>2023-09-25 14:50:41 -0300
commit0b7bbed99d155ba030a1328e357ab6751bdbb835 (patch)
tree7ef5e136aec9c1253f55295a1b20b66043a924f6
parent062939d9cc016a186a282f7a48492c3e01cd740c (diff)
downloadwallet-core-0b7bbed99d155ba030a1328e357ab6751bdbb835.tar.xz
more ui: business and admin
-rw-r--r--packages/demobank-ui/src/components/Routing.tsx17
-rw-r--r--packages/demobank-ui/src/components/ShowInputErrorLabel.tsx4
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/index.ts5
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/state.ts7
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx29
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx32
-rw-r--r--packages/demobank-ui/src/pages/HomePage.tsx6
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx131
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx6
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx2
-rw-r--r--packages/demobank-ui/src/pages/ShowAccountDetails.tsx278
-rw-r--r--packages/demobank-ui/src/pages/UpdateAccountPassword.tsx278
-rw-r--r--packages/demobank-ui/src/pages/admin/Account.tsx72
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx494
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx182
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx174
-rw-r--r--packages/demobank-ui/src/pages/admin/Home.tsx51
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx302
-rw-r--r--packages/demobank-ui/src/pages/business/Home.tsx2
19 files changed, 1181 insertions, 891 deletions
diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx
index ef11af76e..b8e39948b 100644
--- a/packages/demobank-ui/src/components/Routing.tsx
+++ b/packages/demobank-ui/src/components/Routing.tsx
@@ -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");
}}
diff --git a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
index dacffe20a..c5840cad9 100644
--- a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
+++ b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
@@ -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>;
}
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts
index ed6945f84..128a6d30f 100644
--- a/packages/demobank-ui/src/pages/AccountPage/index.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/index.ts
@@ -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 {
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
index 2249b743e..a57e19901 100644
--- a/packages/demobank-ui/src/pages/AccountPage/state.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -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
};
}
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index f2cbbba6c..abd14848f 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -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>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index 80a8224d4..4b23686d6 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -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>
}
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx
index a911f347c..40cc147a6 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -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)}
/>
);
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index c82c1b28d..5cb09a5d4 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -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>
+ )
}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index cdaf363e3..af6f7caee 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -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")}>
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index b912b9060..f972e0380 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -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.
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
index 91b50b84c..6acf0361e 100644
--- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -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>
+ 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 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>
+
+ </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>
- {onClear ? (
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- ) : undefined}
+ <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 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>
+ <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>
- </p>
- </div>
+ </div>
+ </p>
</div>
- );
- }
- \ No newline at end of file
+ </div>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
index 084a5b643..d19c411f3 100644
--- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -1,131 +1,181 @@
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
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 { 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 { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.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>
+ 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
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
+ 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>
- <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
- 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)
- }
- }
+ 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>
- </p>
+
+
+
+ </div>
</div>
- </div>
- );
- } \ No newline at end of file
+ <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>
+
+ );
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx
index 8ab3e1323..90ddd611d 100644
--- a/packages/demobank-ui/src/pages/admin/Account.tsx
+++ b/packages/demobank-ui/src/pages/admin/Account.tsx
@@ -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);
}
- \ No newline at end of file
+ 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}
+ />
+ );
+}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 9ca0323a1..02df824a2 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -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;
}
-
-
- \ No newline at end of file
+ initial.contact_data.email;
+ return initial as any;
+}
+
+
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 56b15818b..56d9c45f9 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -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>
} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index 90835d52b..2146fc6f0 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -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>
+ );
}
diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx
index e1ec6cfe0..f7d4e426e 100644
--- a/packages/demobank-ui/src/pages/admin/Home.tsx
+++ b/packages/demobank-ui/src/pages/admin/Home.tsx
@@ -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>
);
} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 2900db9d2..050f1fb8a 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -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>
+ 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>
+
</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 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>
- <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>
+ </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>
- </p>
+ <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>
- );
- }
- \ No newline at end of file
+ </div>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/Home.tsx
index 8beea640a..318a4cfda 100644
--- a/packages/demobank-ui/src/pages/business/Home.tsx
+++ b/packages/demobank-ui/src/pages/business/Home.tsx
@@ -109,7 +109,7 @@ export function BusinessAccount({
notifyInfo(i18n.str`Password changed`);
setUpdatePassword(false);
}}
- onClear={() => {
+ onCancel={() => {
setUpdatePassword(false);
}}
/>