lang selector and fix logout

This commit is contained in:
Sebastian 2023-09-26 15:18:43 -03:00
parent dcdf8fb6a0
commit 1e4f21cc76
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
16 changed files with 272 additions and 220 deletions

View File

@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"name": "@gnu-taler/demobank-ui", "name": "@gnu-taler/demobank-ui",
"version": "0.1.0", "version": "0.9.3-dev.17",
"license": "AGPL-3.0-OR-LATER", "license": "AGPL-3.0-OR-LATER",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -42,11 +42,11 @@ function getLangName(s: keyof LangsNames | string): string {
return String(s); return String(s);
} }
// FIXME: explain "like py". export function LangSelector(): VNode {
export function LangSelectorLikePy(): VNode {
const [updatingLang, setUpdatingLang] = useState(false); const [updatingLang, setUpdatingLang] = useState(false);
const { lang, changeLanguage } = useTranslationContext(); const { lang, changeLanguage } = useTranslationContext();
const [hidden, setHidden] = useState(true); const [hidden, setHidden] = useState(true);
useEffect(() => { useEffect(() => {
function bodyKeyPress(event: KeyboardEvent) { function bodyKeyPress(event: KeyboardEvent) {
if (event.code === "Escape") setHidden(true); if (event.code === "Escape") setHidden(true);
@ -62,51 +62,49 @@ export function LangSelectorLikePy(): VNode {
}; };
}, []); }, []);
return ( return (
<Fragment> <div>
<a <div class="relative mt-2">
href="#" <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
class="pure-button" onClick={() => {
name="language" setHidden((h) => !h);
onClick={(ev) => { }}>
ev.preventDefault(); <span class="flex items-center">
setHidden((h) => !h); <img src="https://taler.net/images/languageicon.svg" alt="" class="h-5 w-5 flex-shrink-0 rounded-full" />
ev.stopPropagation(); <span class="ml-3 block truncate">{getLangName(lang)}</span>
}} </span>
> <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
{getLangName(lang)} <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
</a> <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" />
<div </svg>
id="lang" </span>
class={hidden ? "hide" : ""} </button>
style={{
display: "inline-block", {!hidden &&
}} <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3">
>
<div style="position: relative; overflow: visible;">
<div
class="nav"
style="position: absolute; max-height: 60vh; overflow-y: auto; margin-left: -120px; margin-top: 20px"
>
{Object.keys(messages) {Object.keys(messages)
.filter((l) => l !== lang) .filter((l) => l !== lang)
.map((l) => ( .map((lang) => (
<a <li class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" role="option"
key={l}
href="#"
class="navbtn langbtn"
value={l}
onClick={() => { onClick={() => {
changeLanguage(l); changeLanguage(lang);
setUpdatingLang(false); setUpdatingLang(false);
setHidden(true)
}} }}
> >
{getLangName(l)} <span class="font-normal block truncate">{getLangName(lang)}</span>
</a>
<span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4">
{/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
</svg> */}
</span>
</li>
))} ))}
<br />
</div> </ul>
</div> }
</div> </div>
</Fragment> </div>
); );
} }

View File

@ -78,17 +78,17 @@ function VersionCheck({ children }: { children: ComponentChildren }): VNode {
if (checked === undefined) { if (checked === undefined) {
return <Loading /> return <Loading />
} }
if (typeof checked === "string") { if (checked.type === "wrong") {
return <BankFrame> return <BankFrame>
the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}" the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
</BankFrame> </BankFrame>
} }
if (checked === true) { if (checked.type === "ok") {
return <Fragment>{children}</Fragment> return <Fragment>{children}</Fragment>
} }
return <BankFrame> return <BankFrame>
<ErrorLoading error={checked}/> <ErrorLoading error={checked.result}/>
</BankFrame> </BankFrame>
} }

View File

@ -34,6 +34,9 @@ const initial: Type = {
logOut() { logOut() {
null; null;
}, },
expired() {
null;
},
logIn(info) { logIn(info) {
null; null;
}, },
@ -65,6 +68,7 @@ export const BackendStateProviderTesting = ({
const value: BackendStateHandler = { const value: BackendStateHandler = {
state, state,
logIn: () => {}, logIn: () => {},
expired: () => {},
logOut: () => {}, logOut: () => {},
}; };

View File

@ -74,7 +74,9 @@ type HashCode = string;
type EddsaPublicKey = string; type EddsaPublicKey = string;
type EddsaSignature = string; type EddsaSignature = string;
type WireTransferIdentifierRawP = string; type WireTransferIdentifierRawP = string;
type RelativeTime = Duration; type RelativeTime = {
d_us: number | "forever"
};
type ImageDataUrl = string; type ImageDataUrl = string;
interface WithId { interface WithId {

View File

@ -46,16 +46,18 @@ import { AccessToken } from "./useCredentialsChecker.js";
* Has the information to reach and * Has the information to reach and
* authenticate at the bank's backend. * authenticate at the bank's backend.
*/ */
export type BackendState = LoggedIn | LoggedOut; export type BackendState = LoggedIn | LoggedOut | Expired;
export interface BackendCredentials { interface LoggedIn {
status: "loggedIn";
isUserAdministrator: boolean;
username: string; username: string;
token: AccessToken; token: AccessToken;
} }
interface Expired {
interface LoggedIn extends BackendCredentials { status: "expired";
status: "loggedIn";
isUserAdministrator: boolean; isUserAdministrator: boolean;
username: string;
} }
interface LoggedOut { interface LoggedOut {
status: "loggedOut"; status: "loggedOut";
@ -69,6 +71,13 @@ export const codecForBackendStateLoggedIn = (): Codec<LoggedIn> =>
.property("isUserAdministrator", codecForBoolean()) .property("isUserAdministrator", codecForBoolean())
.build("BackendState.LoggedIn"); .build("BackendState.LoggedIn");
export const codecForBackendStateExpired = (): Codec<Expired> =>
buildCodecForObject<Expired>()
.property("status", codecForConstString("expired"))
.property("username", codecForString())
.property("isUserAdministrator", codecForBoolean())
.build("BackendState.Expired");
export const codecForBackendStateLoggedOut = (): Codec<LoggedOut> => export const codecForBackendStateLoggedOut = (): Codec<LoggedOut> =>
buildCodecForObject<LoggedOut>() buildCodecForObject<LoggedOut>()
.property("status", codecForConstString("loggedOut")) .property("status", codecForConstString("loggedOut"))
@ -79,6 +88,7 @@ export const codecForBackendState = (): Codec<BackendState> =>
.discriminateOn("status") .discriminateOn("status")
.alternative("loggedIn", codecForBackendStateLoggedIn()) .alternative("loggedIn", codecForBackendStateLoggedIn())
.alternative("loggedOut", codecForBackendStateLoggedOut()) .alternative("loggedOut", codecForBackendStateLoggedOut())
.alternative("expired", codecForBackendStateExpired())
.build("BackendState"); .build("BackendState");
export function getInitialBackendBaseURL(): string { export function getInitialBackendBaseURL(): string {
@ -94,8 +104,9 @@ export function getInitialBackendBaseURL(): string {
"ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
); );
result = window.origin result = window.origin
} else {
result = bankUiSettings.backendBaseURL;
} }
result = bankUiSettings.backendBaseURL;
} else { } else {
// testing/development path // testing/development path
result = overrideUrl result = overrideUrl
@ -115,7 +126,8 @@ export const defaultState: BackendState = {
export interface BackendStateHandler { export interface BackendStateHandler {
state: BackendState; state: BackendState;
logOut(): void; logOut(): void;
logIn(info: BackendCredentials): void; expired(): void;
logIn(info: {username: string, token: AccessToken}): void;
} }
const BACKEND_STATE_KEY = buildStorageKey( const BACKEND_STATE_KEY = buildStorageKey(
@ -133,12 +145,22 @@ export function useBackendState(): BackendStateHandler {
BACKEND_STATE_KEY, BACKEND_STATE_KEY,
defaultState, defaultState,
); );
const mutateAll = useMatchMutate();
return { return {
state, state,
logOut() { logOut() {
update(defaultState); update(defaultState);
}, },
expired() {
if (state.status === "loggedOut") return;
const nextState: BackendState = {
status: "expired",
username: state.username,
isUserAdministrator: state.username === "admin",
};
update(nextState);
},
logIn(info) { logIn(info) {
//admin is defined by the username //admin is defined by the username
const nextState: BackendState = { const nextState: BackendState = {
@ -147,6 +169,7 @@ export function useBackendState(): BackendStateHandler {
isUserAdministrator: info.username === "admin", isUserAdministrator: info.username === "admin",
}; };
update(nextState); update(nextState);
mutateAll(/.*/)
}, },
}; };
} }
@ -194,7 +217,7 @@ export function usePublicBackend(): useBackendType {
number, number,
]): Promise<HttpResponseOk<T>> { ]): Promise<HttpResponseOk<T>> {
const delta = -1 * size //descending order const delta = -1 * size //descending order
const params = start ? { delta, start } : {delta} const params = start ? { delta, start } : { delta }
return requestHandler<T>(baseUrl, endpoint, { return requestHandler<T>(baseUrl, endpoint, {
params, params,
}); });
@ -262,7 +285,8 @@ export function useAuthenticatedBackend(): useBackendType {
const { state } = useBackendContext(); const { state } = useBackendContext();
const { request: requestHandler } = useApiContext(); const { request: requestHandler } = useApiContext();
const creds = state.status === "loggedIn" ? state.token : undefined; // FIXME: libeufin returns 400 insteand of 401 if there is no auth token
const creds = state.status === "loggedIn" ? state.token : "secret-token:a";
const baseUrl = getInitialBackendBaseURL(); const baseUrl = getInitialBackendBaseURL();
const request = useCallback( const request = useCallback(
@ -288,7 +312,7 @@ export function useAuthenticatedBackend(): useBackendType {
number, number,
]): Promise<HttpResponseOk<T>> { ]): Promise<HttpResponseOk<T>> {
const delta = -1 * size //descending order const delta = -1 * size //descending order
const params = start ? { delta, start } : {delta} const params = start ? { delta, start } : { delta }
return requestHandler<T>(baseUrl, endpoint, { return requestHandler<T>(baseUrl, endpoint, {
token: creds, token: creds,
params, params,

View File

@ -268,7 +268,7 @@ export function useEstimator(): CashoutEstimators {
const { state } = useBackendContext(); const { state } = useBackendContext();
const { request } = useApiContext(); const { request } = useApiContext();
const creds = const creds =
state.status === "loggedOut" state.status !== "loggedIn"
? undefined ? undefined
: state.token; : state.token;
return { return {
@ -340,7 +340,7 @@ export function useBusinessAccountFlag(): boolean | undefined {
const { state } = useBackendContext(); const { state } = useBackendContext();
const { request } = useApiContext(); const { request } = useApiContext();
const creds = const creds =
state.status === "loggedOut" state.status !== "loggedIn"
? undefined ? undefined
: {user: state.username, token: state.token}; : {user: state.username, token: state.token};

View File

@ -18,23 +18,29 @@ async function getConfigState(
return result.data; return result.data;
} }
export function useConfigState(): undefined | true | string | HttpError<SandboxBackend.SandboxError> { type Result = undefined
const [checked, setChecked] = useState<true | string | HttpError<SandboxBackend.SandboxError>>() | { type: "ok", result: SandboxBackend.Config }
| { type: "wrong", result: SandboxBackend.Config }
| { type: "error", result: HttpError<SandboxBackend.SandboxError> }
export function useConfigState(): Result {
const [checked, setChecked] = useState<Result>()
const { request } = useApiContext(); const { request } = useApiContext();
useEffect(() => { useEffect(() => {
getConfigState(request) getConfigState(request)
.then((s) => { .then((result) => {
const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, s.version) const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version)
if (r?.compatible) { if (r?.compatible) {
setChecked(true); setChecked({ type: "ok",result });
} else { } else {
setChecked(s.version) setChecked({ type: "wrong",result })
} }
}) })
.catch((error: unknown) => { .catch((error: unknown) => {
if (error instanceof RequestError) { if (error instanceof RequestError) {
setChecked(error.cause); const result = error.cause
setChecked({ type:"error", result });
} }
}); });
}, []); }, []);

View File

@ -15,7 +15,7 @@ export function useCredentialsChecker() {
scope: "readwrite" as "write", //FIX: different than merchant scope: "readwrite" as "write", //FIX: different than merchant
duration: { duration: {
// d_us: "forever" //FIX: should return shortest // d_us: "forever" //FIX: should return shortest
d_us: 1000 * 60 * 60 * 23 d_us: 1000 * 1000 * 5 //60 * 60 * 24 * 7
}, },
refreshable: true, refreshable: true,
} }

View File

@ -18,7 +18,7 @@ import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringi
import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks"; import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks";
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; import { LangSelector } from "../components/LangSelector.js";
import { useBackendContext } from "../context/backend.js"; import { useBackendContext } from "../context/backend.js";
import { useBusinessAccountDetails } from "../hooks/circuit.js"; import { useBusinessAccountDetails } from "../hooks/circuit.js";
import { bankUiSettings } from "../settings.js"; import { bankUiSettings } from "../settings.js";
@ -65,12 +65,14 @@ export function BankFrame({
}, [error]) }, [error])
const demo_sites = []; const demo_sites = [];
for (const i in bankUiSettings.demoSites) if (bankUiSettings.demoSites) {
demo_sites.push( for (const i in bankUiSettings.demoSites)
<a href={bankUiSettings.demoSites[i][1]}> demo_sites.push(
{bankUiSettings.demoSites[i][0]} <a href={bankUiSettings.demoSites[i][1]}>
</a>, {bankUiSettings.demoSites[i][0]}
); </a>,
);
}
return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;"> return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;">
<div class="bg-indigo-600 pb-32"> <div class="bg-indigo-600 pb-32">
@ -88,14 +90,16 @@ export function BankFrame({
/> />
</a> </a>
</div> </div>
<div class="hidden sm:block lg:ml-10 "> {bankUiSettings.demoSites &&
<div class="flex space-x-4"> <div class="hidden sm:block lg:ml-10 ">
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */} <div class="flex space-x-4">
{bankUiSettings.demoSites.map(([name, url]) => { {/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
return <a href={url} class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md py-2 px-3 text-sm font-medium">{name}</a> {bankUiSettings.demoSites.map(([name, url]) => {
})} return <a href={url} class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md py-2 px-3 text-sm font-medium">{name}</a>
})}
</div>
</div> </div>
</div> }
</div> </div>
<div class="flex"> <div class="flex">
@ -166,26 +170,29 @@ export function BankFrame({
<svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg> </svg>
Log out <i18n.Translate>Log out</i18n.Translate>
{/* <span class="ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-gray-50 px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600 ring-1 ring-inset ring-gray-200" aria-hidden="true">5</span> */}
</a> </a>
</li> </li>
<li class="sm:hidden"> <li>
<div class="text-xs font-semibold leading-6 text-gray-400"> <LangSelector />
<i18n.Translate>Sites</i18n.Translate>
</div>
<ul role="list" class="-mx-2 mt-2 space-y-1">
{bankUiSettings.demoSites.map(([name, url]) => {
return <li>
<a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
<span class="truncate">{name}</span>
</a>
</li>
})}
</ul>
</li> </li>
{bankUiSettings.demoSites &&
<li class="sm:hidden">
<div class="text-xs font-semibold leading-6 text-gray-400">
<i18n.Translate>Sites</i18n.Translate>
</div>
<ul role="list" class="-mx-2 mt-2 space-y-1">
{bankUiSettings.demoSites.map(([name, url]) => {
return <li>
<a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
<span class="truncate">{name}</span>
</a>
</li>
})}
</ul>
</li>
}
<li> <li>
<ul role="list" class="space-y-1"> <ul role="list" class="space-y-1">
<li class="mt-2"> <li class="mt-2">
@ -291,63 +298,6 @@ export function BankFrame({
<Footer /> <Footer />
</div > </div >
// <Fragment>
// <header
// class="demobar"
// style="display: flex; flex-direction: row; justify-content: space-between;"
// >
// <a href="#main" class="skip">{i18n.str`Skip to main content`}</a>
// <div style="max-width: 50em; margin-left: 2em; margin-right: 2em;">
// {maybeDemoContent(
// <p>
// {IS_PUBLIC_ACCOUNT_ENABLED ? (
// <i18n.Translate>
// This part of the demo shows how a bank that supports Taler
// directly would work. In addition to using your own bank
// account, you can also see the transaction history of some{" "}
// <a href="/public-accounts">Public Accounts</a>.
// </i18n.Translate>
// ) : (
// <i18n.Translate>
// This part of the demo shows how a bank that supports Taler
// directly would work.
// </i18n.Translate>
// )}
// </p>,
// )}
// </div>
// </header>
// <div style="display:flex; flex-direction: column;" class="navcontainer">
// <nav class="demolist">
// {maybeDemoContent(<Fragment>{demo_sites}</Fragment>)}
// {backend.state.status === "loggedIn" ? (
// <Fragment>
// {goToBusinessAccount && !backend.state.isUserAdministrator ? (
// <MaybeBusinessButton
// account={backend.state.username}
// onClick={goToBusinessAccount}
// />
// ) : undefined}
// <LangSelector />
// <a
// href="#"
// class="pure-button logout-button"
// onClick={() => {
// backend.logOut();
// updateSettings("currentWithdrawalOperationId", undefined);
// }}
// >{i18n.str`Logout`}</a>
// </Fragment>
// ) : undefined}
// </nav>
// </div>
// <section id="main" class="content">
// <StatusBanner />
// {children}
// </section>
// </Fragment>
); );
} }
@ -393,7 +343,7 @@ function StatusBanner(): VNode {
} }
{n.message.debug && {n.message.debug &&
<div class="mt-2 text-sm text-red-700 font-mono break-all"> <div class="mt-2 text-sm text-red-700 font-mono break-all">
{n.message.debug} {n.message.debug}
</div> </div>
} }
</div> </div>

View File

@ -30,14 +30,32 @@ import { undefinedIfEmpty } from "../utils.js";
*/ */
export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
const backend = useBackendContext(); const backend = useBackendContext();
const currentUser = backend.state.status === "loggedIn" ? backend.state.username : undefined const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
const [username, setUsername] = useState<string | undefined>(currentUser); const [username, setUsername] = useState<string | undefined>(currentUser);
const [password, setPassword] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker(); const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
/**
* Register form may be shown in the initialization step.
* If this is an error when usgin the app the registration
* callback is not set
*/
const isSessionExpired = !onRegister
// useEffect(() => {
// if (backend.state.status === "loggedIn") {
// backend.expired()
// }
// },[])
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
useEffect(function focusInput() { useEffect(function focusInput() {
//FIXME: show invalidate session and allow relogin
if (isSessionExpired) {
localStorage.removeItem("backend-state");
window.location.reload()
}
ref.current?.focus(); ref.current?.focus();
}, []); }, []);
const [busy, setBusy] = useState<Record<string, undefined>>() const [busy, setBusy] = useState<Record<string, undefined>>()
@ -124,13 +142,6 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
setBusy(undefined) setBusy(undefined)
} }
/**
* Register form may be shown in the initialization step.
* If this is an error when usgin the app the registration
* callback is not set
*/
const isSessionExpired = !onRegister
return ( return (
<div class="flex min-h-full flex-col justify-center"> <div class="flex min-h-full flex-col justify-center">

View File

@ -14,20 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, stringifyPaytoUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; import { stringifyWithdrawUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact"; import { Fragment, VNode, h } from "preact";
import { Transactions } from "../../components/Transactions/index.js";
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";
import { useSettings } from "../../hooks/settings.js";
import { useEffect, useMemo, useState } from "preact/hooks"; import { useEffect, useMemo, useState } from "preact/hooks";
import { undefinedIfEmpty } from "../../utils.js";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { QR } from "../../components/QR.js"; import { QR } from "../../components/QR.js";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { useSettings } from "../../hooks/settings.js";
import { undefinedIfEmpty } from "../../utils.js";
import { State } from "./index.js";
export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
return ( return (

View File

@ -49,6 +49,8 @@ export function RegistrationPage({
} }
export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/; export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/;
export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
/** /**
* Collect and submit registration data. * Collect and submit registration data.
@ -58,21 +60,33 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
const [username, setUsername] = useState<string | undefined>(); const [username, setUsername] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>(); const [name, setName] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>();
const [phone, setPhone] = useState<string | undefined>();
const [email, setEmail] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>(); const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
const {requestNewLoginToken} = useCredentialsChecker() const { requestNewLoginToken } = useCredentialsChecker()
const { register } = useTestingAPI(); const { register } = useTestingAPI();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({ const errors = undefinedIfEmpty({
name: !name // name: !name
? i18n.str`Missing name` // ? i18n.str`Missing name`
: undefined, // : undefined,
username: !username username: !username
? i18n.str`Missing username` ? i18n.str`Missing username`
: !USERNAME_REGEX.test(username) : !USERNAME_REGEX.test(username)
? i18n.str`Use letters and numbers only, and start with a lowercase letter` ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined, : undefined,
phone: !phone
? undefined
: !PHONE_REGEX.test(phone)
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined,
email: !email
? undefined
: !EMAIL_REGEX.test(email)
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined,
password: !password ? i18n.str`Missing password` : undefined, password: !password ? i18n.str`Missing password` : undefined,
repeatPassword: !repeatPassword repeatPassword: !repeatPassword
? i18n.str`Missing password` ? i18n.str`Missing password`
@ -82,9 +96,9 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
}); });
async function doRegistrationStep() { async function doRegistrationStep() {
if (!username || !password || !name) return; if (!username || !password) return;
try { try {
await register({ name, username, password }); await register({ name: name ?? "", username, password });
const resp = await requestNewLoginToken(username, password) const resp = await requestNewLoginToken(username, password)
setUsername(undefined); setUsername(undefined);
if (resp.valid) { if (resp.valid) {
@ -167,7 +181,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
<div class="flex min-h-full flex-col justify-center"> <div class="flex min-h-full flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Sign up!`}</h2> <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Registration form`}</h2>
</div> </div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
@ -178,34 +192,6 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
autoCapitalize="none" autoCapitalize="none"
autoCorrect="off" autoCorrect="off"
> >
<div>
<label for="name" class="block text-sm font-medium leading-6 text-gray-900">
<i18n.Translate>Name</i18n.Translate>
<b style={{ color: "red" }}> *</b>
</label>
<div class="mt-2">
<input
autoFocus
type="text"
name="name"
id="name"
class="block w-full 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={name ?? ""}
enterkeyhint="next"
placeholder="your name"
autocomplete="name"
required
onInput={(e): void => {
setName(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.name}
isDirty={name !== undefined}
/>
</div>
</div>
<div> <div>
<label for="username" class="block text-sm font-medium leading-6 text-gray-900"> <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
<i18n.Translate>Username</i18n.Translate> <i18n.Translate>Username</i18n.Translate>
@ -237,7 +223,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
<div> <div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<label for="password" class="block text-sm font-medium leading-6 text-gray-900"> <label for="password" class="block text-sm font-medium leading-6 text-gray-900">
Password <i18n.Translate>Password</i18n.Translate>
<b style={{ color: "red" }}> *</b> <b style={{ color: "red" }}> *</b>
</label> </label>
</div> </div>
@ -266,7 +252,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
<div> <div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<label for="register-repeat" class="block text-sm font-medium leading-6 text-gray-900"> <label for="register-repeat" class="block text-sm font-medium leading-6 text-gray-900">
Repeat assword <i18n.Translate>Repeat password</i18n.Translate>
<b style={{ color: "red" }}> *</b> <b style={{ color: "red" }}> *</b>
</label> </label>
</div> </div>
@ -292,6 +278,84 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
</div> </div>
</div> </div>
<div>
<label for="name" class="block text-sm font-medium leading-6 text-gray-900">
<i18n.Translate>Name</i18n.Translate>
</label>
<div class="mt-2">
<input
autoFocus
type="text"
name="name"
id="name"
class="block w-full 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={name ?? ""}
enterkeyhint="next"
placeholder="your name"
autocomplete="name"
required
onInput={(e): void => {
setName(e.currentTarget.value);
}}
/>
{/* <ShowInputErrorLabel
message={errors?.name}
isDirty={name !== undefined}
/> */}
</div>
</div>
{/* <div>
<label for="phone" class="block text-sm font-medium leading-6 text-gray-900">
<i18n.Translate>Phone</i18n.Translate>
</label>
<div class="mt-2">
<input
autoFocus
type="text"
name="phone"
id="phone"
class="block w-full 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={phone ?? ""}
enterkeyhint="next"
placeholder="your phone"
autocomplete="none"
onInput={(e): void => {
setPhone(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.phone}
isDirty={phone !== undefined}
/>
</div>
</div>
<div>
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">
<i18n.Translate>Email</i18n.Translate>
</label>
<div class="mt-2">
<input
autoFocus
type="text"
name="email"
id="email"
class="block w-full 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={email ?? ""}
enterkeyhint="next"
placeholder="your email"
autocomplete="email"
onInput={(e): void => {
setEmail(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.email}
isDirty={email !== undefined}
/>
</div>
</div> */}
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<button type="submit" <button type="submit"
class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2" class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"

View File

@ -9,7 +9,7 @@ import { Fragment, h, VNode } from "preact";
export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const r = useBackendContext(); const r = useBackendContext();
const account = r.state.status === "loggedIn" ? r.state.username : "admin"; const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
const result = useAccountDetails(account); const result = useAccountDetails(account);
if (!result.ok) { if (!result.ok) {

View File

@ -32,7 +32,6 @@ import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Cashouts } from "../../components/Cashouts/index.js"; import { Cashouts } from "../../components/Cashouts/index.js";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { useBackendContext } from "../../context/backend.js";
import { useAccountDetails } from "../../hooks/access.js"; import { useAccountDetails } from "../../hooks/access.js";
import { import {
useCashoutDetails, useCashoutDetails,
@ -46,7 +45,6 @@ import {
undefinedIfEmpty, undefinedIfEmpty,
} from "../../utils.js"; } from "../../utils.js";
import { handleNotOkResult } from "../HomePage.js"; import { handleNotOkResult } from "../HomePage.js";
import { LoginForm } from "../LoginForm.js";
import { Amount } from "../PaytoWireTransferForm.js"; import { Amount } from "../PaytoWireTransferForm.js";
import { ShowAccountDetails } from "../ShowAccountDetails.js"; import { ShowAccountDetails } from "../ShowAccountDetails.js";
import { UpdateAccountPassword } from "../UpdateAccountPassword.js"; import { UpdateAccountPassword } from "../UpdateAccountPassword.js";

View File

@ -15,13 +15,13 @@
*/ */
export interface BankUiSettings { export interface BankUiSettings {
backendBaseURL: string; backendBaseURL?: string;
allowRegistrations: boolean; allowRegistrations?: boolean;
showDemoNav: boolean; showDemoNav?: boolean;
simplePasswordForRandomAccounts: boolean; simplePasswordForRandomAccounts?: boolean;
allowRandomAccountCreation: boolean; allowRandomAccountCreation?: boolean;
bankName: string; bankName?: string;
demoSites: [string, string][]; demoSites?: [string, string][];
} }
/** /**