Compare commits

...

3 Commits

Author SHA1 Message Date
490b813f77
Merge branch 'master' into age-withdraw 2023-10-10 14:14:42 +02:00
adda4f8ce3
wallet-core: order transactions by descending timestamp 2023-10-10 12:14:15 +02:00
Sebastian
4e11051d9f
removing url from login 2023-10-10 07:12:36 -03:00
9 changed files with 48 additions and 171 deletions

View File

@ -26,7 +26,6 @@ import {
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/browser"; } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact"; import { Fragment, VNode, h } from "preact";
import { route } from "preact-router";
import { useMemo } from "preact/hooks"; import { useMemo } from "preact/hooks";
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
import { Loading } from "./components/exception/loading.js"; import { Loading } from "./components/exception/loading.js";
@ -41,8 +40,7 @@ import {
import { ConfigContextProvider } from "./context/config.js"; import { ConfigContextProvider } from "./context/config.js";
import { useBackendConfig } from "./hooks/backend.js"; import { useBackendConfig } from "./hooks/backend.js";
import { strings } from "./i18n/strings.js"; import { strings } from "./i18n/strings.js";
import { ConnectionPage, LoginPage } from "./paths/login/index.js"; import { LoginPage } from "./paths/login/index.js";
import { LoginToken } from "./declaration.js";
export function Application(): VNode { export function Application(): VNode {
return ( return (
@ -60,7 +58,6 @@ export function Application(): VNode {
* @returns * @returns
*/ */
function ApplicationStatusRoutes(): VNode { function ApplicationStatusRoutes(): VNode {
const { changeBackend, selected: backendSelected } = useBackendContext();
const result = useBackendConfig(); const result = useBackendConfig();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -69,15 +66,6 @@ function ApplicationStatusRoutes(): VNode {
: { currency: "unknown", version: "unknown" }; : { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]); const ctx = useMemo(() => ({ currency, version }), [currency, version]);
if (!backendSelected) {
return (
<Fragment>
<NotConnectedAppMenu title="Welcome!" />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
if (!result.ok) { if (!result.ok) {
if (result.loading) return <Loading />; if (result.loading) return <Loading />;
if ( if (
@ -87,7 +75,13 @@ function ApplicationStatusRoutes(): VNode {
return ( return (
<Fragment> <Fragment>
<NotConnectedAppMenu title="Login" /> <NotConnectedAppMenu title="Login" />
<ConnectionPage onConfirm={changeBackend} /> <NotificationCard
notification={{
message: i18n.str`Checking the /config endpoint got authorization error`,
type: "ERROR",
description: `The /config endpoint of the backend server should be accesible`,
}}
/>
</Fragment> </Fragment>
); );
} }
@ -100,12 +94,11 @@ function ApplicationStatusRoutes(): VNode {
<NotConnectedAppMenu title="Error" /> <NotConnectedAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Server not found`, message: i18n.str`Could not find /config enpoint on this URL`,
type: "ERROR", type: "ERROR",
description: `Check your url`, description: `Check the URL or contact the system administrator.`,
}} }}
/> />
<ConnectionPage onConfirm={changeBackend} />
</Fragment> </Fragment>
); );
} }
@ -119,7 +112,6 @@ function ApplicationStatusRoutes(): VNode {
description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
}} }}
/> />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>; </Fragment>;
} }
if (result.type === ErrorType.UNREADABLE) { if (result.type === ErrorType.UNREADABLE) {
@ -132,7 +124,6 @@ function ApplicationStatusRoutes(): VNode {
description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
}} }}
/> />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>; </Fragment>;
} }
return ( return (
@ -145,7 +136,6 @@ function ApplicationStatusRoutes(): VNode {
description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
}} }}
/> />
<ConnectionPage onConfirm={changeBackend} />
</Fragment> </Fragment>
); );
} }
@ -164,9 +154,7 @@ function ApplicationStatusRoutes(): VNode {
description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`, description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`,
}} }}
/> />
<ConnectionPage onConfirm={changeBackend} />
</Fragment> </Fragment>
} }
return ( return (

View File

@ -26,13 +26,14 @@ import { Route, Router, route } from "preact-router";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { InstanceRoutes } from "./InstanceRoutes.js"; import { InstanceRoutes } from "./InstanceRoutes.js";
import { import {
NotConnectedAppMenu,
NotYetReadyAppMenu, NotYetReadyAppMenu,
NotificationCard, NotificationCard,
} from "./components/menu/index.js"; } from "./components/menu/index.js";
import { useBackendContext } from "./context/backend.js"; import { useBackendContext } from "./context/backend.js";
import { LoginToken } from "./declaration.js"; import { LoginToken } from "./declaration.js";
import { useBackendInstancesTestForAdmin } from "./hooks/backend.js"; import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
import { ConnectionPage, LoginPage } from "./paths/login/index.js"; import { LoginPage } from "./paths/login/index.js";
import { Settings } from "./paths/settings/index.js"; import { Settings } from "./paths/settings/index.js";
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
@ -42,10 +43,11 @@ import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
*/ */
export function ApplicationReadyRoutes(): VNode { export function ApplicationReadyRoutes(): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const { url: backendURL, changeBackend } = useBackendContext()
const [unauthorized, setUnauthorized] = useState(false) const [unauthorized, setUnauthorized] = useState(false)
const { const {
url: backendURL,
updateToken, updateToken,
alreadyTriedLogin,
} = useBackendContext(); } = useBackendContext();
function updateLoginStatus(token: LoginToken | undefined) { function updateLoginStatus(token: LoginToken | undefined) {
@ -64,6 +66,15 @@ export function ApplicationReadyRoutes(): VNode {
&& result.type === ErrorType.CLIENT && result.type === ErrorType.CLIENT
&& result.status === HttpStatusCode.Unauthorized; && result.status === HttpStatusCode.Unauthorized;
if (!alreadyTriedLogin) {
return (
<Fragment>
<NotConnectedAppMenu title="Welcome!" />
<LoginPage onConfirm={updateToken} />
</Fragment>
);
}
if (showSettings) { if (showSettings) {
return <Fragment> return <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
@ -100,7 +111,7 @@ export function ApplicationReadyRoutes(): VNode {
type: "ERROR", type: "ERROR",
}} }}
/> />
<ConnectionPage onConfirm={changeBackend} /> {/* <ConnectionPage onConfirm={changeBackend} /> */}
</Fragment> </Fragment>
); );
} }

View File

@ -205,7 +205,7 @@ export function InstanceRoutes({
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Access denied`, message: i18n.str`Access denied`,
description: i18n.str`Redirecting to login page.`, description: i18n.str`Session expired or password changed.`,
type: "ERROR", type: "ERROR",
}} }}
/> />

View File

@ -49,7 +49,7 @@ export function Sidebar({
isPasswordOk isPasswordOk
}: Props): VNode { }: Props): VNode {
const config = useConfigContext(); const config = useConfigContext();
const { url: backendURL, resetBackend } = useBackendContext() const { url: backendURL } = useBackendContext()
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const kycStatus = useInstanceKYCDetails(); const kycStatus = useInstanceKYCDetails();
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect"; const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
@ -283,20 +283,7 @@ export function Sidebar({
<i18n.Translate>Log out</i18n.Translate> <i18n.Translate>Log out</i18n.Translate>
</span> </span>
</a> </a>
</li> : </li> : undefined
<li>
<a
class="has-icon is-state-info is-hoverable"
onClick={(): void => resetBackend()}
>
<span class="icon">
<i class="mdi mdi-logout default" />
</span>
<span class="menu-item-label">
<i18n.Translate>Change server</i18n.Translate>
</span>
</a>
</li>
} }
</ul> </ul>
</div> </div>

View File

@ -19,56 +19,39 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
import { createContext, h, VNode } from "preact"; import { createContext, h, VNode } from "preact";
import { useContext, useState } from "preact/hooks"; import { useContext } from "preact/hooks";
import { LoginToken } from "../declaration.js"; import { LoginToken } from "../declaration.js";
import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js"; import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
import { codecForBoolean } from "@gnu-taler/taler-util";
interface BackendContextType { interface BackendContextType {
url: string, url: string,
selected: boolean; alreadyTriedLogin: boolean;
token?: LoginToken; token?: LoginToken;
updateToken: (token: LoginToken | undefined) => void; updateToken: (token: LoginToken | undefined) => void;
changeBackend: (url: string) => void;
resetBackend: () => void;
} }
const BackendContext = createContext<BackendContextType>({ const BackendContext = createContext<BackendContextType>({
url: "", url: "",
selected: false, alreadyTriedLogin: false,
token: undefined, token: undefined,
updateToken: () => null, updateToken: () => null,
changeBackend: () => null,
resetBackend: () => null,
}); });
const BACKEND_SELECTED = buildStorageKey("backend-selected", codecForBoolean());
function useBackendContextState( function useBackendContextState(
defaultUrl?: string, defaultUrl?: string,
): BackendContextType { ): BackendContextType {
const [url, changeBackend2] = useBackendURL(defaultUrl); const [url] = useBackendURL(defaultUrl);
const [token, updateToken] = useBackendDefaultToken(); const [token, updateToken] = useBackendDefaultToken();
const {value, update} = useLocalStorage(BACKEND_SELECTED)
function changeBackend(s:string) { console.log(JSON.stringify(token))
changeBackend2(s)
update(true)
}
function resetBackend() {
update(false)
}
return { return {
url, url,
token, token,
selected: value ?? false, alreadyTriedLogin: token !== undefined,
updateToken, updateToken,
changeBackend,
resetBackend
}; };
} }

View File

@ -345,7 +345,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
//and avoid network //and avoid network
return Promise.resolve({ return Promise.resolve({
ok: true, ok: true,
data: {orders:[]} as T, data: { orders: [] } as T,
}) })
} }
return requestHandler<T>(baseUrl, endpoint, { params, token }); return requestHandler<T>(baseUrl, endpoint, { params, token });
@ -398,7 +398,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
//and avoid network //and avoid network
return Promise.resolve({ return Promise.resolve({
ok: true, ok: true,
data: {transfers:[]} as T, data: { transfers: [] } as T,
}) })
} }
if (delta !== undefined) { if (delta !== undefined) {
@ -424,7 +424,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
//and avoid network //and avoid network
return Promise.resolve({ return Promise.resolve({
ok: true, ok: true,
data: {templates:[]} as T, data: { templates: [] } as T,
}) })
} }
if (delta !== undefined) { if (delta !== undefined) {
@ -450,7 +450,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
//and avoid network //and avoid network
return Promise.resolve({ return Promise.resolve({
ok: true, ok: true,
data: {webhooks:[]} as T, data: { webhooks: [] } as T,
}) })
} }
if (delta !== undefined) { if (delta !== undefined) {

View File

@ -36,7 +36,7 @@ function normalizeToken(r: string): AccessToken {
} }
export function LoginPage({ onConfirm }: Props): VNode { export function LoginPage({ onConfirm }: Props): VNode {
const { url: backendURL, changeBackend, resetBackend } = useBackendContext(); const { url: backendURL } = useBackendContext();
const { admin, id } = useInstanceContext(); const { admin, id } = useInstanceContext();
const { requestNewLoginToken } = useCredentialsChecker(); const { requestNewLoginToken } = useCredentialsChecker();
const [token, setToken] = useState(""); const [token, setToken] = useState("");
@ -54,11 +54,7 @@ export function LoginPage({ onConfirm }: Props): VNode {
} else { } else {
onConfirm(undefined); onConfirm(undefined);
} }
}, [backendURL, id, token]) }, [id, token])
async function changeServer() {
resetBackend()
}
if (admin && id !== "default") { if (admin && id !== "default") {
//admin trying to access another instance //admin trying to access another instance
@ -139,26 +135,7 @@ export function LoginPage({ onConfirm }: Props): VNode {
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
> >
<i18n.Translate>Please enter your access token.</i18n.Translate> <i18n.Translate>Please enter your access token.</i18n.Translate>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">URL</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="text"
placeholder="set new url"
name="id"
value={backendURL}
disabled
readOnly
/>
</p>
</div>
</div>
</div>
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label is-normal"> <div class="field-label is-normal">
<label class="label"> <label class="label">
@ -194,10 +171,7 @@ export function LoginPage({ onConfirm }: Props): VNode {
borderTop: 0, borderTop: 0,
}} }}
> >
<AsyncButton onClick={changeServer}> <div />
<i18n.Translate>Change server</i18n.Translate>
</AsyncButton>
<AsyncButton <AsyncButton
type="is-info" type="is-info"
onClick={doLogin} onClick={doLogin}
@ -226,73 +200,3 @@ function AsyncButton({ onClick, disabled, type = "", children }: { type?: string
} }
export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode {
const { url: backendURL } = useBackendContext()
const [error, setError] = useState<string>();
const [url, setURL] = useState(backendURL ?? "");
const { i18n } = useTranslationContext();
async function doConnect() {
const withHttp = url.startsWith("http") ? url : "https://" + url
onConfirm(withHttp)
}
return (
<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Connect to backend`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<i18n.Translate>Location of the backend server</i18n.Translate>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">URL</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="text"
placeholder="set new url"
name="id"
value={url ?? ""}
onKeyPress={(e) =>
e.keyCode === 13
? doConnect()
: null
}
onInput={(e): void => setURL(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "flex-end",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton onClick={doConnect}>
<i18n.Translate>Connect</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>
);
}

View File

@ -1291,10 +1291,12 @@ export async function getTransactions(
const txNotPending = transactions.filter((x) => !isPending(x)); const txNotPending = transactions.filter((x) => !isPending(x));
const txCmp = (h1: Transaction, h2: Transaction) => { const txCmp = (h1: Transaction, h2: Transaction) => {
const tsCmp = AbsoluteTime.cmp( // Order transactions by timestamp. Newest transactions come first.
const tsCmp = -AbsoluteTime.cmp(
AbsoluteTime.fromPreciseTimestamp(h1.timestamp), AbsoluteTime.fromPreciseTimestamp(h1.timestamp),
AbsoluteTime.fromPreciseTimestamp(h2.timestamp), AbsoluteTime.fromPreciseTimestamp(h2.timestamp),
); );
// If the timestamp is exactly the same, order by transaction type.
if (tsCmp === 0) { if (tsCmp === 0) {
return Math.sign(txOrder[h1.type] - txOrder[h2.type]); return Math.sign(txOrder[h1.type] - txOrder[h2.type]);
} }

View File

@ -1,6 +1,8 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
function setupLiveReload(): void { function setupLiveReload(): void {
const stopWs = localStorage.getItem("stop-ws")
if (!!stopWs) return;
const protocol = window.location.protocol === "http:" ? "ws:" : "wss:"; const protocol = window.location.protocol === "http:" ? "ws:" : "wss:";
const ws = new WebSocket(`${protocol}//${window.location.hostname}:${window.location.port}/ws`); const ws = new WebSocket(`${protocol}//${window.location.hostname}:${window.location.port}/ws`);