lang selector and fix logout
This commit is contained in:
parent
dcdf8fb6a0
commit
1e4f21cc76
@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@gnu-taler/demobank-ui",
|
||||
"version": "0.1.0",
|
||||
"version": "0.9.3-dev.17",
|
||||
"license": "AGPL-3.0-OR-LATER",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@ -42,11 +42,11 @@ function getLangName(s: keyof LangsNames | string): string {
|
||||
return String(s);
|
||||
}
|
||||
|
||||
// FIXME: explain "like py".
|
||||
export function LangSelectorLikePy(): VNode {
|
||||
export function LangSelector(): VNode {
|
||||
const [updatingLang, setUpdatingLang] = useState(false);
|
||||
const { lang, changeLanguage } = useTranslationContext();
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
function bodyKeyPress(event: KeyboardEvent) {
|
||||
if (event.code === "Escape") setHidden(true);
|
||||
@ -62,51 +62,49 @@ export function LangSelectorLikePy(): VNode {
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Fragment>
|
||||
<a
|
||||
href="#"
|
||||
class="pure-button"
|
||||
name="language"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
setHidden((h) => !h);
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{getLangName(lang)}
|
||||
</a>
|
||||
<div
|
||||
id="lang"
|
||||
class={hidden ? "hide" : ""}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
<div style="position: relative; overflow: visible;">
|
||||
<div
|
||||
class="nav"
|
||||
style="position: absolute; max-height: 60vh; overflow-y: auto; margin-left: -120px; margin-top: 20px"
|
||||
>
|
||||
<div>
|
||||
<div class="relative mt-2">
|
||||
<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"
|
||||
onClick={() => {
|
||||
setHidden((h) => !h);
|
||||
}}>
|
||||
<span class="flex items-center">
|
||||
<img src="https://taler.net/images/languageicon.svg" alt="" class="h-5 w-5 flex-shrink-0 rounded-full" />
|
||||
<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">
|
||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{!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">
|
||||
{Object.keys(messages)
|
||||
.filter((l) => l !== lang)
|
||||
.map((l) => (
|
||||
<a
|
||||
key={l}
|
||||
href="#"
|
||||
class="navbtn langbtn"
|
||||
value={l}
|
||||
.map((lang) => (
|
||||
<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"
|
||||
onClick={() => {
|
||||
changeLanguage(l);
|
||||
changeLanguage(lang);
|
||||
setUpdatingLang(false);
|
||||
setHidden(true)
|
||||
}}
|
||||
>
|
||||
{getLangName(l)}
|
||||
</a>
|
||||
<span class="font-normal block truncate">{getLangName(lang)}</span>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
}
|
||||
|
||||
</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -78,17 +78,17 @@ function VersionCheck({ children }: { children: ComponentChildren }): VNode {
|
||||
if (checked === undefined) {
|
||||
return <Loading />
|
||||
}
|
||||
if (typeof checked === "string") {
|
||||
if (checked.type === "wrong") {
|
||||
return <BankFrame>
|
||||
the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
|
||||
</BankFrame>
|
||||
}
|
||||
if (checked === true) {
|
||||
if (checked.type === "ok") {
|
||||
return <Fragment>{children}</Fragment>
|
||||
}
|
||||
|
||||
return <BankFrame>
|
||||
<ErrorLoading error={checked}/>
|
||||
<ErrorLoading error={checked.result}/>
|
||||
</BankFrame>
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,9 @@ const initial: Type = {
|
||||
logOut() {
|
||||
null;
|
||||
},
|
||||
expired() {
|
||||
null;
|
||||
},
|
||||
logIn(info) {
|
||||
null;
|
||||
},
|
||||
@ -65,6 +68,7 @@ export const BackendStateProviderTesting = ({
|
||||
const value: BackendStateHandler = {
|
||||
state,
|
||||
logIn: () => {},
|
||||
expired: () => {},
|
||||
logOut: () => {},
|
||||
};
|
||||
|
||||
|
4
packages/demobank-ui/src/declaration.d.ts
vendored
4
packages/demobank-ui/src/declaration.d.ts
vendored
@ -74,7 +74,9 @@ type HashCode = string;
|
||||
type EddsaPublicKey = string;
|
||||
type EddsaSignature = string;
|
||||
type WireTransferIdentifierRawP = string;
|
||||
type RelativeTime = Duration;
|
||||
type RelativeTime = {
|
||||
d_us: number | "forever"
|
||||
};
|
||||
type ImageDataUrl = string;
|
||||
|
||||
interface WithId {
|
||||
|
@ -46,16 +46,18 @@ import { AccessToken } from "./useCredentialsChecker.js";
|
||||
* Has the information to reach and
|
||||
* 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;
|
||||
token: AccessToken;
|
||||
}
|
||||
|
||||
interface LoggedIn extends BackendCredentials {
|
||||
status: "loggedIn";
|
||||
interface Expired {
|
||||
status: "expired";
|
||||
isUserAdministrator: boolean;
|
||||
username: string;
|
||||
}
|
||||
interface LoggedOut {
|
||||
status: "loggedOut";
|
||||
@ -69,6 +71,13 @@ export const codecForBackendStateLoggedIn = (): Codec<LoggedIn> =>
|
||||
.property("isUserAdministrator", codecForBoolean())
|
||||
.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> =>
|
||||
buildCodecForObject<LoggedOut>()
|
||||
.property("status", codecForConstString("loggedOut"))
|
||||
@ -79,6 +88,7 @@ export const codecForBackendState = (): Codec<BackendState> =>
|
||||
.discriminateOn("status")
|
||||
.alternative("loggedIn", codecForBackendStateLoggedIn())
|
||||
.alternative("loggedOut", codecForBackendStateLoggedOut())
|
||||
.alternative("expired", codecForBackendStateExpired())
|
||||
.build("BackendState");
|
||||
|
||||
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'",
|
||||
);
|
||||
result = window.origin
|
||||
} else {
|
||||
result = bankUiSettings.backendBaseURL;
|
||||
}
|
||||
result = bankUiSettings.backendBaseURL;
|
||||
} else {
|
||||
// testing/development path
|
||||
result = overrideUrl
|
||||
@ -115,7 +126,8 @@ export const defaultState: BackendState = {
|
||||
export interface BackendStateHandler {
|
||||
state: BackendState;
|
||||
logOut(): void;
|
||||
logIn(info: BackendCredentials): void;
|
||||
expired(): void;
|
||||
logIn(info: {username: string, token: AccessToken}): void;
|
||||
}
|
||||
|
||||
const BACKEND_STATE_KEY = buildStorageKey(
|
||||
@ -133,12 +145,22 @@ export function useBackendState(): BackendStateHandler {
|
||||
BACKEND_STATE_KEY,
|
||||
defaultState,
|
||||
);
|
||||
const mutateAll = useMatchMutate();
|
||||
|
||||
return {
|
||||
state,
|
||||
logOut() {
|
||||
update(defaultState);
|
||||
},
|
||||
expired() {
|
||||
if (state.status === "loggedOut") return;
|
||||
const nextState: BackendState = {
|
||||
status: "expired",
|
||||
username: state.username,
|
||||
isUserAdministrator: state.username === "admin",
|
||||
};
|
||||
update(nextState);
|
||||
},
|
||||
logIn(info) {
|
||||
//admin is defined by the username
|
||||
const nextState: BackendState = {
|
||||
@ -147,6 +169,7 @@ export function useBackendState(): BackendStateHandler {
|
||||
isUserAdministrator: info.username === "admin",
|
||||
};
|
||||
update(nextState);
|
||||
mutateAll(/.*/)
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -194,7 +217,7 @@ export function usePublicBackend(): useBackendType {
|
||||
number,
|
||||
]): Promise<HttpResponseOk<T>> {
|
||||
const delta = -1 * size //descending order
|
||||
const params = start ? { delta, start } : {delta}
|
||||
const params = start ? { delta, start } : { delta }
|
||||
return requestHandler<T>(baseUrl, endpoint, {
|
||||
params,
|
||||
});
|
||||
@ -262,7 +285,8 @@ export function useAuthenticatedBackend(): useBackendType {
|
||||
const { state } = useBackendContext();
|
||||
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 request = useCallback(
|
||||
@ -288,7 +312,7 @@ export function useAuthenticatedBackend(): useBackendType {
|
||||
number,
|
||||
]): Promise<HttpResponseOk<T>> {
|
||||
const delta = -1 * size //descending order
|
||||
const params = start ? { delta, start } : {delta}
|
||||
const params = start ? { delta, start } : { delta }
|
||||
return requestHandler<T>(baseUrl, endpoint, {
|
||||
token: creds,
|
||||
params,
|
||||
|
@ -268,7 +268,7 @@ export function useEstimator(): CashoutEstimators {
|
||||
const { state } = useBackendContext();
|
||||
const { request } = useApiContext();
|
||||
const creds =
|
||||
state.status === "loggedOut"
|
||||
state.status !== "loggedIn"
|
||||
? undefined
|
||||
: state.token;
|
||||
return {
|
||||
@ -340,7 +340,7 @@ export function useBusinessAccountFlag(): boolean | undefined {
|
||||
const { state } = useBackendContext();
|
||||
const { request } = useApiContext();
|
||||
const creds =
|
||||
state.status === "loggedOut"
|
||||
state.status !== "loggedIn"
|
||||
? undefined
|
||||
: {user: state.username, token: state.token};
|
||||
|
||||
|
@ -18,23 +18,29 @@ async function getConfigState(
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export function useConfigState(): undefined | true | string | HttpError<SandboxBackend.SandboxError> {
|
||||
const [checked, setChecked] = useState<true | string | HttpError<SandboxBackend.SandboxError>>()
|
||||
type Result = undefined
|
||||
| { 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();
|
||||
|
||||
useEffect(() => {
|
||||
getConfigState(request)
|
||||
.then((s) => {
|
||||
const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, s.version)
|
||||
.then((result) => {
|
||||
const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version)
|
||||
if (r?.compatible) {
|
||||
setChecked(true);
|
||||
setChecked({ type: "ok",result });
|
||||
} else {
|
||||
setChecked(s.version)
|
||||
setChecked({ type: "wrong",result })
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (error instanceof RequestError) {
|
||||
setChecked(error.cause);
|
||||
const result = error.cause
|
||||
setChecked({ type:"error", result });
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
@ -15,7 +15,7 @@ export function useCredentialsChecker() {
|
||||
scope: "readwrite" as "write", //FIX: different than merchant
|
||||
duration: {
|
||||
// d_us: "forever" //FIX: should return shortest
|
||||
d_us: 1000 * 60 * 60 * 23
|
||||
d_us: 1000 * 1000 * 5 //60 * 60 * 24 * 7
|
||||
},
|
||||
refreshable: true,
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringi
|
||||
import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { ComponentChildren, Fragment, h, VNode } from "preact";
|
||||
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 { useBusinessAccountDetails } from "../hooks/circuit.js";
|
||||
import { bankUiSettings } from "../settings.js";
|
||||
@ -65,12 +65,14 @@ export function BankFrame({
|
||||
}, [error])
|
||||
|
||||
const demo_sites = [];
|
||||
for (const i in bankUiSettings.demoSites)
|
||||
demo_sites.push(
|
||||
<a href={bankUiSettings.demoSites[i][1]}>
|
||||
{bankUiSettings.demoSites[i][0]}
|
||||
</a>,
|
||||
);
|
||||
if (bankUiSettings.demoSites) {
|
||||
for (const i in bankUiSettings.demoSites)
|
||||
demo_sites.push(
|
||||
<a href={bankUiSettings.demoSites[i][1]}>
|
||||
{bankUiSettings.demoSites[i][0]}
|
||||
</a>,
|
||||
);
|
||||
}
|
||||
|
||||
return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;">
|
||||
<div class="bg-indigo-600 pb-32">
|
||||
@ -88,14 +90,16 @@ export function BankFrame({
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden sm:block lg:ml-10 ">
|
||||
<div class="flex space-x-4">
|
||||
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
|
||||
{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>
|
||||
})}
|
||||
{bankUiSettings.demoSites &&
|
||||
<div class="hidden sm:block lg:ml-10 ">
|
||||
<div class="flex space-x-4">
|
||||
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
|
||||
{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 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">
|
||||
<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>
|
||||
Log out
|
||||
{/* <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> */}
|
||||
<i18n.Translate>Log out</i18n.Translate>
|
||||
</a>
|
||||
</li>
|
||||
<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">></span>
|
||||
<span class="truncate">{name}</span>
|
||||
</a>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
<li>
|
||||
<LangSelector />
|
||||
</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">></span>
|
||||
<span class="truncate">{name}</span>
|
||||
</a>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<ul role="list" class="space-y-1">
|
||||
<li class="mt-2">
|
||||
@ -291,63 +298,6 @@ export function BankFrame({
|
||||
<Footer />
|
||||
</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 &&
|
||||
<div class="mt-2 text-sm text-red-700 font-mono break-all">
|
||||
{n.message.debug}
|
||||
{n.message.debug}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -30,14 +30,32 @@ import { undefinedIfEmpty } from "../utils.js";
|
||||
*/
|
||||
export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
||||
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 [password, setPassword] = useState<string | undefined>();
|
||||
const { i18n } = useTranslationContext();
|
||||
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);
|
||||
useEffect(function focusInput() {
|
||||
//FIXME: show invalidate session and allow relogin
|
||||
if (isSessionExpired) {
|
||||
localStorage.removeItem("backend-state");
|
||||
window.location.reload()
|
||||
}
|
||||
ref.current?.focus();
|
||||
}, []);
|
||||
const [busy, setBusy] = useState<Record<string, undefined>>()
|
||||
@ -124,13 +142,6 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
||||
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 (
|
||||
<div class="flex min-h-full flex-col justify-center">
|
||||
|
||||
|
@ -14,20 +14,15 @@
|
||||
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 { Fragment, h, VNode } 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 { Fragment, VNode, h } from "preact";
|
||||
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 { 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) {
|
||||
return (
|
||||
|
@ -49,6 +49,8 @@ export function RegistrationPage({
|
||||
}
|
||||
|
||||
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.
|
||||
@ -58,21 +60,33 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
|
||||
const [username, setUsername] = useState<string | undefined>();
|
||||
const [name, setName] = 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 {requestNewLoginToken} = useCredentialsChecker()
|
||||
const { requestNewLoginToken } = useCredentialsChecker()
|
||||
|
||||
const { register } = useTestingAPI();
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
const errors = undefinedIfEmpty({
|
||||
name: !name
|
||||
? i18n.str`Missing name`
|
||||
: undefined,
|
||||
// name: !name
|
||||
// ? i18n.str`Missing name`
|
||||
// : undefined,
|
||||
username: !username
|
||||
? i18n.str`Missing username`
|
||||
: !USERNAME_REGEX.test(username)
|
||||
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
|
||||
: 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,
|
||||
repeatPassword: !repeatPassword
|
||||
? i18n.str`Missing password`
|
||||
@ -82,9 +96,9 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
|
||||
});
|
||||
|
||||
async function doRegistrationStep() {
|
||||
if (!username || !password || !name) return;
|
||||
if (!username || !password) return;
|
||||
try {
|
||||
await register({ name, username, password });
|
||||
await register({ name: name ?? "", username, password });
|
||||
const resp = await requestNewLoginToken(username, password)
|
||||
setUsername(undefined);
|
||||
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="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 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"
|
||||
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>
|
||||
<label for="username" class="block text-sm font-medium leading-6 text-gray-900">
|
||||
<i18n.Translate>Username</i18n.Translate>
|
||||
@ -237,7 +223,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
@ -266,7 +252,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
@ -292,6 +278,84 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
|
||||
</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">
|
||||
<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"
|
||||
|
@ -9,7 +9,7 @@ import { Fragment, h, VNode } from "preact";
|
||||
export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
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);
|
||||
|
||||
if (!result.ok) {
|
||||
|
@ -32,7 +32,6 @@ import { Fragment, VNode, h } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Cashouts } from "../../components/Cashouts/index.js";
|
||||
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
|
||||
import { useBackendContext } from "../../context/backend.js";
|
||||
import { useAccountDetails } from "../../hooks/access.js";
|
||||
import {
|
||||
useCashoutDetails,
|
||||
@ -46,7 +45,6 @@ import {
|
||||
undefinedIfEmpty,
|
||||
} from "../../utils.js";
|
||||
import { handleNotOkResult } from "../HomePage.js";
|
||||
import { LoginForm } from "../LoginForm.js";
|
||||
import { Amount } from "../PaytoWireTransferForm.js";
|
||||
import { ShowAccountDetails } from "../ShowAccountDetails.js";
|
||||
import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
|
||||
|
@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
export interface BankUiSettings {
|
||||
backendBaseURL: string;
|
||||
allowRegistrations: boolean;
|
||||
showDemoNav: boolean;
|
||||
simplePasswordForRandomAccounts: boolean;
|
||||
allowRandomAccountCreation: boolean;
|
||||
bankName: string;
|
||||
demoSites: [string, string][];
|
||||
backendBaseURL?: string;
|
||||
allowRegistrations?: boolean;
|
||||
showDemoNav?: boolean;
|
||||
simplePasswordForRandomAccounts?: boolean;
|
||||
allowRandomAccountCreation?: boolean;
|
||||
bankName?: string;
|
||||
demoSites?: [string, string][];
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user