From 4ed3a05fdd97b08085e5a390963f9810b93a75fd Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 29 Apr 2024 11:42:52 -0300 Subject: create form --- packages/aml-backoffice-ui/src/Routing.tsx | 10 +- packages/aml-backoffice-ui/src/hooks/form.ts | 22 +- packages/aml-backoffice-ui/src/hooks/officer.ts | 18 +- packages/aml-backoffice-ui/src/pages/Cases.tsx | 42 +++- .../aml-backoffice-ui/src/pages/CreateAccount.tsx | 252 +++++++++++++++------ 5 files changed, 244 insertions(+), 100 deletions(-) (limited to 'packages/aml-backoffice-ui/src') diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx index 1e32e4f4c..f38fc29c2 100644 --- a/packages/aml-backoffice-ui/src/Routing.tsx +++ b/packages/aml-backoffice-ui/src/Routing.tsx @@ -97,18 +97,18 @@ function PublicRounting(): VNode { export const privatePages = { account: urlPattern(/\/account/, () => "#/account"), cases: urlPattern(/\/cases/, () => "#/cases"), - caseDetails: urlPattern<{ cid: string }>( - /\/case\/(?[a-zA-Z0-9]+)/, - ({ cid }) => `#/case/${cid}`, - ), caseUpdate: urlPattern<{ cid: string; type: string }>( - /\/case\/(?[a-zA-Z0-9]+)\/new\/(?[a-zA-Z0-9]+)/, + /\/case\/(?[a-zA-Z0-9]+)\/new\/(?[a-zA-Z0-9_.]+)/, ({ cid, type }) => `#/case/${cid}/new/${type}`, ), caseNew: urlPattern<{ cid: string }>( /\/case\/(?[a-zA-Z0-9]+)\/new/, ({ cid }) => `#/case/${cid}/new`, ), + caseDetails: urlPattern<{ cid: string }>( + /\/case\/(?[a-zA-Z0-9]+)/, + ({ cid }) => `#/case/${cid}`, + ), }; function PrivateRouting(): VNode { diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts index fae11c05c..e3d97db8c 100644 --- a/packages/aml-backoffice-ui/src/hooks/form.ts +++ b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -68,11 +68,10 @@ export type FormStatus = }; function constructFormHandler( - form: FormValues, - updateForm: (d: FormValues) => void, + form: RecursivePartial>, + updateForm: (d: RecursivePartial>) => void, errors: FormErrors | undefined, ): FormHandler { - const keys = Object.keys(form) as Array; const handler = keys.reduce((prev, fieldName) => { @@ -105,17 +104,18 @@ function constructFormHandler( /** * FIXME: Consider sending this to web-utils - * - * - * @param defaultValue - * @param check - * @returns + * + * + * @param defaultValue + * @param check + * @returns */ export function useFormState( - defaultValue: FormValues, - check: (f: FormValues) => FormStatus, + defaultValue: RecursivePartial>, + check: (f: RecursivePartial>) => FormStatus, ): [FormHandler, FormStatus] { - const [form, updateForm] = useState>(defaultValue); + const [form, updateForm] = + useState>>(defaultValue); const status = check(form); const handler = constructFormHandler(form, updateForm, status.errors); diff --git a/packages/aml-backoffice-ui/src/hooks/officer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts index 3ac4c857c..dabe866d3 100644 --- a/packages/aml-backoffice-ui/src/hooks/officer.ts +++ b/packages/aml-backoffice-ui/src/hooks/officer.ts @@ -19,6 +19,7 @@ import { LockedAccount, OfficerAccount, OfficerId, + OperationOk, SigningKey, buildCodecForObject, codecForAbsoluteTime, @@ -26,6 +27,7 @@ import { createNewOfficerAccount, decodeCrock, encodeCrock, + opFixedSuccess, unlockOfficerAccount, } from "@gnu-taler/taler-util"; import { buildStorageKey, useExchangeApiContext, useLocalStorage } from "@gnu-taler/web-util/browser"; @@ -60,7 +62,7 @@ export type OfficerState = OfficerNotReady | OfficerReady; export type OfficerNotReady = OfficerNotFound | OfficerLocked; interface OfficerNotFound { state: "not-found"; - create: (password: string) => Promise; + create: (password: string) => Promise>; } interface OfficerLocked { state: "locked"; @@ -81,7 +83,7 @@ const DEV_ACCOUNT_KEY = buildStorageKey( ); export function useOfficer(): OfficerState { - const exchangeContext = useExchangeApiContext(); + const {lib:{exchange: api}} = useExchangeApiContext(); const [pref] = usePreferences(); pref.keepSessionAfterReload; // dev account, is kept on reloaded. @@ -105,16 +107,12 @@ export function useOfficer(): OfficerState { return { state: "not-found", create: async (pwd: string) => { - const req = await fetch( - new URL("seed", exchangeContext.lib.exchange.baseUrl).href, - ); - const b = await req.blob(); - const ar = await b.arrayBuffer(); - const uintar = new Uint8Array(ar); + const resp = await api.getSeed() + const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array(); const { id, safe, signingKey } = await createNewOfficerAccount( pwd, - uintar, + extraEntropy, ); officerStorage.update({ account: safe, @@ -124,6 +122,8 @@ export function useOfficer(): OfficerState { // accountStorage.update({ id, signingKey }); const strKey = encodeCrock(signingKey); accountStorage.update({ id, strKey }); + + return opFixedSuccess(id) }, }; } diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index e928b831f..6b59b2736 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -20,19 +20,20 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + Attention, ErrorLoading, Loading, createNewForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useCases } from "../hooks/useCases.js"; +import { privatePages } from "../Routing.js"; import { amlStateConverter } from "../utils/converter.js"; import { AmlExchangeBackend } from "../utils/types.js"; import { Officer } from "./Officer.js"; -import { privatePages } from "../Routing.js"; export function CasesUI({ records, @@ -130,7 +131,9 @@ export function CasesUI({
{r.h_payto.substring(0, 16)}... @@ -187,6 +190,7 @@ export function Cases() { ); const list = useCases(stateFilter); + const { i18n } = useTranslationContext(); if (!list) { return ; @@ -197,8 +201,36 @@ export function Cases() { if (list.type === "fail") { switch (list.case) { - case HttpStatusCode.Unauthorized: - case HttpStatusCode.Forbidden: + case HttpStatusCode.Forbidden: { + return ( + + + + This account doesnt have access. Request account activation sending your public key. + + + + + ); + } + case HttpStatusCode.Unauthorized: { + return ( + + + + This account is not allowed to perform list the cases. + + + + + ); + } case HttpStatusCode.NotFound: case HttpStatusCode.Conflict: return ; 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 */ -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>, + ): FormStatus { + const errors = undefinedIfEmpty>({ + 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(obj: T): T | undefined { + return Object.keys(obj).some( + (k) => (obj as Record)[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(); const [settings] = usePreferences(); - + const officer = useOfficer(); + + const [notification, withErrorHandler] = useLocalNotificationHandler(); + + const [form, status] = useFormState( + { + 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 (
+ +

Create account @@ -44,79 +128,107 @@ export function CreateAccount({
- { - 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!); - } +
{ + e.preventDefault(); }} + autoCapitalize="none" + autoCorrect="off" > -
- +
+ +
+ { + console.log("ASDASD", form.password?.onUpdate); + form.password?.onUpdate(e.currentTarget.value); + }} + /> + +
-
- + +
+ +
+ { + form.repeat?.onUpdate(e.currentTarget.value); + }} + /> + +
- +
- +
); } + +/** + * 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); + } +} -- cgit v1.2.3