summaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages/CreateAccount.tsx')
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx219
1 files changed, 155 insertions, 64 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 603813f8e..87310aa27 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -1,26 +1,130 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import {
- createNewForm,
- notifyError,
+ Button,
+ InputLine,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ UIHandlerId,
+ useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { useSettings } from "../hooks/useSettings.js";
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ RecursivePartial,
+ useFormState,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { usePreferences } from "../hooks/preferences.js";
+
+type FormType = {
+ password: string;
+ repeat: string;
+};
+function createFormValidator(
+ i18n: InternationalizationAPI,
+ allowInsecurePassword: boolean,
+) {
+ return function check(
+ state: RecursivePartial<FormValues<FormType>>,
+ ): FormStatus<FormType> {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ password: !state.password
+ ? i18n.str`required`
+ : allowInsecurePassword
+ ? undefined
+ : state.password.length < 8
+ ? i18n.str`should have at least 8 characters`
+ : !state.password.match(/[a-z]/) && state.password.match(/[A-Z]/)
+ ? i18n.str`should have lowercase and uppercase characters`
+ : !state.password.match(/\d/)
+ ? i18n.str`should have numbers`
+ : !state.password.match(/[^a-zA-Z\d]/)
+ ? i18n.str`should have at least one character which is not a number or letter`
+ : undefined,
+
+ repeat: !state.repeat
+ ? i18n.str`required`
+ : state.password !== state.repeat
+ ? i18n.str`doesn't match`
+ : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: FormType = {
+ password: state.password!,
+ repeat: state.repeat!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormType> = {
+ password: state.password,
+ repeat: state.repeat,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+
+export function undefinedIfEmpty<T extends object | undefined>(obj: T): T | undefined {
+ if (obj === undefined) return undefined;
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
-export function CreateAccount({
- onNewAccount,
-}: {
- onNewAccount: (password: string) => void;
-}): VNode {
+export function CreateAccount(): VNode {
const { i18n } = useTranslationContext();
- const Form = createNewForm<{
- password: string;
- repeat: string;
- }>();
- const [settings] = useSettings()
+ const [settings] = usePreferences();
+ const officer = useOfficer();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const [form, status] = useFormState<FormType>(
+ [".password", ".repeat"] as Array<UIHandlerId>,
+ {
+ password: undefined,
+ repeat: undefined,
+ },
+ createFormValidator(i18n, settings.allowInsecurePassword),
+ );
+
+ const createAccountHandler =
+ status.status === "fail" || officer.state !== "not-found"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.create(form.password!.value!),
+ () => {},
+ );
return (
<div class="flex min-h-full flex-col ">
+ <LocalNotificationBanner notification={notification} />
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Create account</i18n.Translate>
@@ -29,78 +133,65 @@ export function CreateAccount({
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <Form.Provider
- computeFormState={(v) => {
- return {
- password: {
- error: !v.password
- ? i18n.str`required`
- : settings.allowInsecurePassword
- ? undefined
- : v.password.length < 8
- ? i18n.str`should have at least 8 characters`
- : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
- ? i18n.str`should have lowercase and uppercase characters`
- : !v.password.match(/\d/)
- ? i18n.str`should have numbers`
- : !v.password.match(/[^a-zA-Z\d]/)
- ? i18n.str`should have at least one character which is not a number or letter`
- : undefined,
- },
- repeat: {
- error: !v.repeat
- ? i18n.str`required`
- : v.repeat !== v.password
- ? i18n.str`doesn't match`
- : undefined,
- },
- };
- }}
- onSubmit={async (v, s) => {
- const error = s?.password?.error ?? s?.repeat?.error;
- if (error) {
- notifyError(
- i18n.str`Can't create account`,
- error as TranslatedString,
- );
- } else {
- onNewAccount(v.password!);
- }
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
}}
+ autoCapitalize="none"
+ autoCorrect="off"
>
- <div class="mb-4">
- <Form.InputLine
+ <div class="mt-2">
+ <InputLine<FormType, "password">
label={i18n.str`Password`}
name="password"
type="password"
- help={
- settings.allowInsecurePassword
- ? i18n.str`short password are insecure, turn off insecure password in settings`
- : i18n.str`lower and upper case letters, number and special character`
- }
required
+ handler={form.password}
/>
</div>
- <div class="mb-4">
- <Form.InputLine
+
+ <div class="mt-2">
+ <InputLine<FormType, "repeat">
label={i18n.str`Repeat password`}
name="repeat"
type="password"
required
+ handler={form.repeat}
/>
</div>
<div class="mt-8">
- <button
+ <Button
type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 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={!createAccountHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 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"
+ handler={createAccountHandler}
>
<i18n.Translate>Create</i18n.Translate>
- </button>
+ </Button>
</div>
- </Form.Provider>
+ </form>
</div>
</div>
</div>
);
}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}