diff options
Diffstat (limited to 'packages')
8 files changed, 155 insertions, 104 deletions
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 02ec75dbf..e0ff77417 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -28,9 +28,9 @@ import { assertUnreachable } from "./WithdrawalOperationPage.js"; /** * Collect and submit login data. */ -export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forbidden", onRegister?: () => void }): VNode { +export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?: boolean, currentUser?: string, onRegister?: () => void }): VNode { const backend = useBackendState(); - const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined + const [username, setUsername] = useState<string | undefined>(currentUser); const [password, setPassword] = useState<string | undefined>(); const { i18n } = useTranslationContext(); @@ -38,21 +38,11 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb const [notification, notify, handleError] = useLocalNotification() const {config} = useBankCoreApiContext(); - /** - * Register form may be shown in the initialization step. - * If no register handler then this is invoke - * to show a session expired or unauthorized - */ - const isLogginAgain = !onRegister - const ref = useRef<HTMLInputElement>(null); useEffect(function focusInput() { - if (isLogginAgain && backend.state.status !== "expired") { - backend.expired() - window.location.reload() - } ref.current?.focus(); }, []); + const [busy, setBusy] = useState<Record<string, undefined>>() const errors = undefinedIfEmpty({ @@ -128,7 +118,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb id="username" 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={username ?? ""} - disabled={isLogginAgain} + disabled={fixedUser} enterkeyhint="next" placeholder="identification" autocomplete="username" @@ -170,7 +160,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb </div> </div> - {isLogginAgain ? <div class="flex justify-between"> + {currentUser ? <div class="flex justify-between"> <button type="submit" class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600" onClick={(e) => { @@ -189,7 +179,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb doLogin() }} > - <i18n.Translate>Renew session</i18n.Translate> + <i18n.Translate>Check</i18n.Translate> </button> </div> : <div> <button type="submit" diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx b/packages/demobank-ui/src/pages/WireTransfer.tsx index a68c085c9..88bdc70a6 100644 --- a/packages/demobank-ui/src/pages/WireTransfer.tsx +++ b/packages/demobank-ui/src/pages/WireTransfer.tsx @@ -24,8 +24,8 @@ export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { o } if (result.type === "fail") { switch (result.case) { - case "unauthorized": return <LoginForm reason="forbidden" /> - case "not-found": return <LoginForm reason="not-found" /> + case "unauthorized": return <LoginForm currentUser={account}/> + case "not-found": return <LoginForm currentUser={account} /> default: assertUnreachable(result) } } diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index f8913f0ec..3af619c2d 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -47,6 +47,7 @@ import { useWithdrawalDetails } from "../hooks/access.js"; import { OperationState } from "./OperationState/index.js"; import { OperationNotFound } from "./WithdrawalQRCode.js"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; +import { LoginForm } from "./LoginForm.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -340,16 +341,16 @@ export function ShouldBeSameUser({ username, children }: { username: string, chi const { state: credentials } = useBackendState(); const { i18n } = useTranslationContext() if (credentials.status === "loggedOut") { - return <Attention type="info" title={i18n.str`Authentication required`}> - <p>You should login as "{username}"</p> - </Attention> + return <Fragment> + <Attention type="info" title={i18n.str`Authentication required`} /> + <LoginForm currentUser={username} fixedUser/> + </Fragment> } if (credentials.username !== username) { - return <Attention type="warning" title={i18n.str`This operation was created with other username`}> - <p> - You can switch to account "{username}" and complete the operation. - </p> - </Attention> + return <Fragment> + <Attention type="warning" title={i18n.str`This operation was created with other username`} /> + <LoginForm currentUser={username} fixedUser/> + </Fragment> } return <Fragment> {children} diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx index 06a88c1c6..d435673a2 100644 --- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -44,8 +44,8 @@ export function ShowAccountDetails({ } if (result.type === "fail") { switch (result.case) { - case "not-found": return <LoginForm reason="not-found" /> - case "unauthorized": return <LoginForm reason="forbidden" /> + case "not-found": return <LoginForm currentUser={account} /> + case "unauthorized": return <LoginForm currentUser={account} /> default: assertUnreachable(result) } } diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index c8abde74b..e76204a81 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -1,8 +1,8 @@ -import { AmountString, Amounts, PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { AmountString, Amounts, ChallengeContactData, PaytoString, TalerCorebankApi, TranslatedString, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; +import { ErrorMessageMappingFor, PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { useBackendContext } from "../../context/backend.js"; @@ -14,9 +14,14 @@ const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; -export type AccountFormData = TalerCorebankApi.AccountData & { username: string } +export type AccountFormData = TalerCorebankApi.AccountData & { + username: string, + debitAmount: string, + isExchange: boolean, + isPublic: boolean, +} -type MM = { +type ChangeByPurposeType = { "create": (a: TalerCorebankApi.RegisterAccountRequest | undefined) => void, "update": (a: TalerCorebankApi.AccountReconfiguration | undefined) => void, "show": undefined @@ -29,12 +34,13 @@ type MM = { * @param param0 * @returns */ -export function AccountForm<T extends keyof MM>({ +export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ template, username, purpose, onChange, focus, + admin, noCashout, children, }: { @@ -44,31 +50,27 @@ export function AccountForm<T extends keyof MM>({ noCashout?: boolean, admin?: boolean, template: TalerCorebankApi.AccountData | undefined; - onChange: MM[T]; - purpose: T; + onChange: ChangeByPurposeType[PurposeType]; + purpose: PurposeType; }): VNode { const initial = initializeFromTemplate(username, template); const [form, setForm] = useState(initial); const [errors, setErrors] = useState< - RecursivePartial<typeof initial> | undefined + ErrorMessageMappingFor<typeof initial> | undefined >(undefined); const { i18n } = useTranslationContext(); const { config } = useBankCoreApiContext() - const [debitAmount, setDebitAmount] = useState<string>() - - const [isExchange, setIsExchange] = useState<boolean>(); - const [isPublic, setIsPublic] = useState<boolean>(); function updateForm(newForm: typeof initial): void { const parsed = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined);; - const trimmedAmountStr = debitAmount?.trim(); + const trimmedAmountStr = newForm.debitAmount?.trim(); const parsedAmount = Amounts.parse(`${config.currency}:${trimmedAmountStr}`); - const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({ + const errors = undefinedIfEmpty<ErrorMessageMappingFor<typeof initial>>({ cashout_payto_uri: (!newForm.cashout_payto_uri ? undefined : !parsed @@ -77,7 +79,7 @@ export function AccountForm<T extends keyof MM>({ ? 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)) as PaytoString, + : validateIBAN(parsed.iban, i18n)), contact_data: undefinedIfEmpty({ email: !newForm.contact_data?.email ? undefined @@ -93,7 +95,7 @@ export function AccountForm<T extends keyof MM>({ : undefined, }), debit_threshold: !trimmedAmountStr - ? i18n.str`required` + ? (purpose === "create" ? i18n.str`required` : undefined) : !parsedAmount ? i18n.str`not valid` : Amounts.isZero(parsedAmount) @@ -111,8 +113,14 @@ export function AccountForm<T extends keyof MM>({ } else { const cashout = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined) const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout) + + const internal = !newForm.payto_uri ? undefined : buildPayto("iban", newForm.payto_uri, undefined); + const internalURI = !internal ? undefined : stringifyPaytoUri(internal) + switch (purpose) { case "create": { + //typescript doesn't correctly narrow a generic type + const callback = onChange as ChangeByPurposeType["create"] const result: TalerCorebankApi.RegisterAccountRequest = { cashout_payto_uri: cashoutURI, name: newForm.name!, @@ -123,20 +131,36 @@ export function AccountForm<T extends keyof MM>({ phone: newForm.contact_data?.phone, }), debit_threshold: newForm.debit_threshold as AmountString, - // , - // internal_payto_uri + internal_payto_uri: internalURI, + is_public: newForm.isPublic, + is_taler_exchange: newForm.isExchange, } - onChange(result) + callback(result) return; } case "update": { + //typescript doesn't correctly narrow a generic type + const callback = onChange as ChangeByPurposeType["update"] const result: TalerCorebankApi.AccountReconfiguration = { - cashout_payto_uri: cashoutURI + cashout_payto_uri: cashoutURI, + challenge_contact_data: undefinedIfEmpty({ + email: newForm.contact_data?.email, + phone: newForm.contact_data?.phone, + }), + debit_threshold: newForm.debit_threshold as AmountString, + is_taler_exchange: newForm.isExchange, + name: newForm.name + // is_public: newForm.isPublic } - onChange(result as any) + callback(result) return; } - case "show": + case "show": { + return; + } + default: { + assertUnreachable(purpose) + } } } } @@ -203,7 +227,7 @@ export function AccountForm<T extends keyof MM>({ name="name" data-error={!!errors?.name && form.name !== undefined} id="name" - disabled={purpose === "show"} + disabled={purpose !== "create"} value={form.name ?? ""} onChange={(e) => { form.name = e.currentTarget.value; @@ -332,53 +356,70 @@ export function AccountForm<T extends keyof MM>({ </div> } - <div class="sm:col-span-5"> - <label for="debit" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Max debt`}</label> - <InputAmount - name="debit" - left - currency={config.currency} - value={debitAmount ?? ""} - onChange={(e) => { - setDebitAmount(e); - }} - /> - <ShowInputErrorLabel - message={errors?.debit_threshold ? String(errors?.debit_threshold) : undefined} - isDirty={form.debit_threshold !== undefined} - /> - <p class="mt-2 text-sm text-gray-500" >allow user debt</p> - </div> + {admin ? <Fragment> + <div class="sm:col-span-5"> + <label for="debit" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Max debt`}</label> + <InputAmount + name="debit" + left + currency={config.currency} + value={form.debitAmount ?? ""} + onChange={(e) => { + form.debitAmount = e + updateForm(structuredClone(form)) + }} + /> + <ShowInputErrorLabel + message={errors?.debit_threshold ? String(errors?.debit_threshold) : undefined} + isDirty={form.debit_threshold !== undefined} + /> + <p class="mt-2 text-sm text-gray-500" >how much is user able to transfer </p> + </div> - <div class="sm:col-span-5"> - <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>Is an exchange</i18n.Translate> + <div class="sm:col-span-5"> + <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>Is an exchange</i18n.Translate> + </span> </span> - </span> - <button type="button" data-enabled={!!isExchange} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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" + <button type="button" data-enabled={!!form.isExchange} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => setIsExchange(!isExchange)}> - <span aria-hidden="true" data-enabled={!!isExchange} class="translate-x-5 data-[enabled=false]: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> + onClick={() => { + form.isExchange = !form.isExchange + updateForm(structuredClone(form)) + }}> + <span aria-hidden="true" data-enabled={!!form.isExchange} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> </div> - </div> + </Fragment> : + undefined + } - <div class="sm:col-span-5"> - <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>Is public</i18n.Translate> + {purpose === "create" ? + <div class="sm:col-span-5"> + <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>Is public</i18n.Translate> + </span> </span> - </span> - <button type="button" data-enabled={!!isPublic} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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" + <button type="button" data-enabled={!!form.isPublic} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => setIsPublic(!isPublic)}> - <span aria-hidden="true" data-enabled={!!isPublic} class="translate-x-5 data-[enabled=false]: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> + onClick={() => { + form.isPublic = !form.isPublic + updateForm(structuredClone(form)) + }}> + <span aria-hidden="true" data-enabled={!!form.isPublic} class="translate-x-5 data-[enabled=false]: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> + <p class="mt-2 text-sm text-gray-500" > + <i18n.Translate>public accounts have their balance publicly accesible</i18n.Translate> + </p> </div> - </div> + : undefined + } </div> </div> @@ -386,7 +427,7 @@ export function AccountForm<T extends keyof MM>({ </form> ); } - +// JNTMECG7RM3AAQB6SRAZNWDSM8 function initializeFromTemplate( username: string | undefined, account: TalerCorebankApi.AccountData | undefined, diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index 5ee887128..57144177c 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -39,8 +39,8 @@ export function RemoveAccount({ } if (result.type === "fail") { switch (result.case) { - case "unauthorized": return <LoginForm reason="forbidden" /> - case "not-found": return <LoginForm reason="not-found" /> + case "unauthorized": return <LoginForm currentUser={account} /> + case "not-found": return <LoginForm currentUser={account} /> default: assertUnreachable(result) } } diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index b2ff41e63..ce1a6cf49 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -102,8 +102,8 @@ export function CreateCashout({ } if (resultAccount.type === "fail") { switch (resultAccount.case) { - case "unauthorized": return <LoginForm reason="forbidden" /> - case "not-found": return <LoginForm reason="not-found" /> + case "unauthorized": return <LoginForm currentUser={accountName} /> + case "not-found": return <LoginForm currentUser={accountName} /> default: assertUnreachable(resultAccount) } } diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 805d68660..7cdd8a861 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util"; +import { AmountString, HttpStatusCode, PaytoString, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util"; import { ErrorNotification, ErrorType, @@ -62,17 +62,36 @@ export type PartialButDefined<T> = { [P in keyof T]: T[P] | undefined; }; -export type WithIntermediate<Type extends object> = { - [prop in keyof Type]: Type[prop] extends object +/** + * every non-map field can be undefined + */ +export type WithIntermediate<Type> = { + [prop in keyof Type]: + Type[prop] extends PaytoString ? Type[prop] | undefined : + Type[prop] extends AmountString ? Type[prop] | undefined : + Type[prop] extends TranslatedString ? Type[prop] | undefined : + Type[prop] extends object ? WithIntermediate<Type[prop]> : Type[prop] | undefined; }; -export type RecursivePartial<T> = { - [P in keyof T]?: T[P] extends (infer U)[] +export type RecursivePartial<Type> = { + [P in keyof Type]?: Type[P] extends (infer U)[] ? RecursivePartial<U>[] - : T[P] extends object - ? RecursivePartial<T[P]> - : T[P]; + : Type[P] extends object + ? RecursivePartial<Type[P]> + : Type[P]; +}; +export type ErrorMessageMappingFor<Type> = { + [prop in keyof Type]+?: + //enumerate known object + Exclude<Type[prop],undefined> extends PaytoString ? TranslatedString : + Exclude<Type[prop],undefined> extends AmountString ? TranslatedString : + Exclude<Type[prop],undefined> extends TranslatedString ? TranslatedString : + // arrays: every element + Exclude<Type[prop],undefined> extends (infer U)[] ? ErrorMessageMappingFor<U>[] : + // map: every field + Exclude<Type[prop],undefined> extends object ? ErrorMessageMappingFor<Type[prop]> + : TranslatedString; }; export enum TanChannel { @@ -337,7 +356,7 @@ export const COUNTRY_TABLE = { export function validateIBAN( iban: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { +): TranslatedString | undefined { // Check total length if (iban.length < 4) return i18n.str`IBAN numbers usually have more that 4 digits`; |