aboutsummaryrefslogtreecommitdiff
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.tsx252
1 files changed, 182 insertions, 70 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 9afd0d212..094e78531 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -13,29 +13,113 @@
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 { TranslatedString } from "@gnu-taler/taler-util";
import {
- createNewForm,
- notifyError,
+ Button,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
+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) {
+ return {
+ status: "ok",
+ result: state as FormType,
+ errors,
+ };
+ }
+ return {
+ status: "fail",
+ result: state,
+ errors,
+ };
+ };
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
+
export function CreateAccount({
onNewAccount,
}: {
- onNewAccount: (password: string) => void;
+ onNewAccount: () => void;
}): VNode {
const { i18n } = useTranslationContext();
- const Form = createNewForm<{
- password: string;
- repeat: string;
- }>();
+ // const Form = createNewForm<FormType>();
const [settings] = usePreferences();
-
+ const officer = useOfficer();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+ const [form, status] = useFormState<FormType>(
+ {
+ 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!),
+ onNewAccount,
+ );
+
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>
@@ -44,79 +128,107 @@ 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
- 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
- />
+ <div>
+ <label
+ for="password"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Password</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ ref={doAutoFocus}
+ type="text"
+ name="password"
+ id="password"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={form.password?.value ?? ""}
+ enterkeyhint="next"
+ placeholder="strong password"
+ autocomplete="password"
+ title={i18n.str`Password`}
+ required
+ onChange={(e) => {
+ console.log("ASDASD", form.password?.onUpdate);
+ form.password?.onUpdate(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={form.password?.error}
+ isDirty={form.password?.value !== undefined}
+ />
+ </div>
</div>
- <div class="mb-4">
- <Form.InputLine
- label={i18n.str`Repeat password`}
- name="repeat"
- type="password"
- required
- />
+
+ <div>
+ <label
+ for="repeat"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Repeat password</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ name="repeat"
+ id="repeat"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={form.repeat?.value ?? ""}
+ enterkeyhint="next"
+ placeholder="identification"
+ autocomplete="repeat"
+ title={i18n.str`Repeat password`}
+ required
+ onChange={(e): void => {
+ form.repeat?.onUpdate(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={form.repeat?.error}
+ isDirty={form.repeat?.value !== undefined}
+ />
+ </div>
</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);
+ }
+}