fix 7426: URI fragment routing

This commit is contained in:
Sebastian 2022-11-07 12:44:09 -03:00
parent afa87c2cf4
commit 3eafb64912
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
4 changed files with 183 additions and 135 deletions

View File

@ -10,11 +10,12 @@
"pretty": "prettier --write src" "pretty": "prettier --write src"
}, },
"dependencies": { "dependencies": {
"@gnu-taler/taler-util": "workspace:*",
"date-fns": "2.29.3", "date-fns": "2.29.3",
"history": "4.10.1",
"jed": "1.1.1", "jed": "1.1.1",
"preact": "10.6.5", "preact": "10.6.5",
"preact-router": "3.2.1", "preact-router": "3.2.1",
"@gnu-taler/taler-util": "workspace:*",
"qrcode-generator": "^1.4.4", "qrcode-generator": "^1.4.4",
"react": "npm:@preact/compat@^17.1.2", "react": "npm:@preact/compat@^17.1.2",
"swr": "1.3.0" "swr": "1.3.0"

View File

@ -17,8 +17,8 @@
import { ComponentChildren, Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import Match from "preact-router/match"; import Match from "preact-router/match";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { NavigationBar } from "./NavigationBar"; import { NavigationBar } from "./NavigationBar.js";
import { Sidebar } from "./SideBar"; import { Sidebar } from "./SideBar.js";
interface MenuProps { interface MenuProps {
title: string; title: string;

View File

@ -32,6 +32,8 @@ import { useLocalStorage, useNotNullLocalStorage } from "../../hooks/index.js";
import { Translate, useTranslator } from "../../i18n/index.js"; import { Translate, useTranslator } from "../../i18n/index.js";
import "../../scss/main.scss"; import "../../scss/main.scss";
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
import { createHashHistory } from "history";
import Router, { Route, route } from "preact-router";
interface BankUiSettings { interface BankUiSettings {
allowRegistrations: boolean; allowRegistrations: boolean;
@ -94,7 +96,7 @@ const PageContextDefault: PageContextType = [
isLoggedIn: false, isLoggedIn: false,
isRawPayto: false, isRawPayto: false,
showPublicHistories: false, showPublicHistories: false,
tryRegister: false,
withdrawalInProgress: false, withdrawalInProgress: false,
}, },
() => { () => {
@ -158,7 +160,6 @@ interface WireTransferRequestType {
interface PageStateType { interface PageStateType {
isLoggedIn: boolean; isLoggedIn: boolean;
isRawPayto: boolean; isRawPayto: boolean;
tryRegister: boolean;
showPublicHistories: boolean; showPublicHistories: boolean;
hasError: boolean; hasError: boolean;
hasInfo: boolean; hasInfo: boolean;
@ -480,7 +481,6 @@ function usePageState(
state: PageStateType = { state: PageStateType = {
isLoggedIn: false, isLoggedIn: false,
isRawPayto: false, isRawPayto: false,
tryRegister: false,
showPublicHistories: false, showPublicHistories: false,
hasError: false, hasError: false,
hasInfo: false, hasInfo: false,
@ -502,17 +502,18 @@ function usePageState(
//when moving from one page to another //when moving from one page to another
//clean up the info and error bar //clean up the info and error bar
function removeLatestInfo(val: any): ReturnType<typeof retSetter> { function removeLatestInfo(val: any): ReturnType<typeof retSetter> {
const updater = typeof val === 'function' ? val : (c:any) => val const updater = typeof val === "function" ? val : (c: any) => val;
return retSetter((current:any) => { return retSetter((current: any) => {
const cleanedCurrent: PageStateType = {...current, const cleanedCurrent: PageStateType = {
...current,
hasInfo: false, hasInfo: false,
info: undefined, info: undefined,
hasError: false, hasError: false,
errors: undefined, errors: undefined,
timestamp: new Date().getTime() timestamp: new Date().getTime(),
} };
return updater(cleanedCurrent) return updater(cleanedCurrent);
}) });
} }
return [retObj, removeLatestInfo]; return [retObj, removeLatestInfo];
@ -926,7 +927,7 @@ async function registrationCall(
const headers = new Headers(); const headers = new Headers();
headers.append("Content-Type", "application/json"); headers.append("Content-Type", "application/json");
const url = new URL("access-api/testing/register", baseUrl); const url = new URL("access-api/testing/register", baseUrl);
let res: any; let res: Response;
try { try {
res = await fetch(url.href, { res = await fetch(url.href, {
method: "POST", method: "POST",
@ -953,19 +954,30 @@ async function registrationCall(
} }
if (!res.ok) { if (!res.ok) {
const response = await res.json(); const response = await res.json();
pageStateSetter((prevState) => ({ if (res.status === 409) {
...prevState, pageStateSetter((prevState) => ({
hasError: true, ...prevState,
error: { hasError: true,
title: `New registration gave response error`, error: {
debug: JSON.stringify(response), title: `That username is already taken`,
}, debug: JSON.stringify(response),
})); },
}));
} else {
pageStateSetter((prevState) => ({
...prevState,
hasError: true,
error: {
title: `New registration gave response error`,
debug: JSON.stringify(response),
},
}));
}
} else { } else {
// registration was ok
pageStateSetter((prevState) => ({ pageStateSetter((prevState) => ({
...prevState, ...prevState,
isLoggedIn: true, isLoggedIn: true,
tryRegister: false,
})); }));
backendStateSetter((prevState) => ({ backendStateSetter((prevState) => ({
...prevState, ...prevState,
@ -973,6 +985,7 @@ async function registrationCall(
username: req.username, username: req.username,
password: req.password, password: req.password,
})); }));
route("/account");
} }
} }
@ -1072,7 +1085,10 @@ function BankFrame(Props: any): VNode {
This part of the demo shows how a bank that supports Taler This part of the demo shows how a bank that supports Taler
directly would work. In addition to using your own bank account, directly would work. In addition to using your own bank account,
you can also see the transaction history of some{" "} you can also see the transaction history of some{" "}
<a href="#" onClick={goPublicAccounts(pageStateSetter)}> <a
href="/public-accounts"
onClick={goPublicAccounts(pageStateSetter)}
>
Public Accounts Public Accounts
</a> </a>
. .
@ -1215,7 +1231,7 @@ function PaytoWireTransfer(Props: any): VNode {
name="subject" name="subject"
id="subject" id="subject"
placeholder="subject" placeholder="subject"
value={submitData?.subject ?? ""} value={submitData?.subject ?? ""}
required required
onInput={(e): void => { onInput={(e): void => {
submitDataSetter((submitData: any) => ({ submitDataSetter((submitData: any) => ({
@ -1237,7 +1253,7 @@ function PaytoWireTransfer(Props: any): VNode {
id="amount" id="amount"
placeholder="amount" placeholder="amount"
required required
value={submitData?.amount ?? ""} value={submitData?.amount ?? ""}
pattern={amountRegex} pattern={amountRegex}
onInput={(e): void => { onInput={(e): void => {
submitDataSetter((submitData: any) => ({ submitDataSetter((submitData: any) => ({
@ -1298,11 +1314,12 @@ function PaytoWireTransfer(Props: any): VNode {
transactionData, transactionData,
backendState, backendState,
pageStateSetter, pageStateSetter,
() => submitDataSetter((p) => ({ () =>
amount: undefined, submitDataSetter((p) => ({
iban: undefined, amount: undefined,
subject: undefined, iban: undefined,
})), subject: undefined,
})),
); );
}} }}
/> />
@ -1537,7 +1554,7 @@ function QrCodeSection({
//Taler Wallet WebExtension is listening to headers response and tab updates. //Taler Wallet WebExtension is listening to headers response and tab updates.
//In the SPA there is no header response with the Taler URI so //In the SPA there is no header response with the Taler URI so
//this hack manually triggers the tab update after the QR is in the DOM. //this hack manually triggers the tab update after the QR is in the DOM.
window.location.href = `${window.location.href.split("#")[0]}#`; window.location.hash = `/account/${new Date().getTime()}`;
}, []); }, []);
return ( return (
@ -1786,10 +1803,7 @@ function RegistrationButton(Props: any): VNode {
<button <button
class="pure-button pure-button-secondary btn-cancel" class="pure-button pure-button-secondary btn-cancel"
onClick={() => { onClick={() => {
pageStateSetter((prevState: PageStateType) => ({ route("/register");
...prevState,
tryRegister: true,
}));
}} }}
> >
{i18n`Register`} {i18n`Register`}
@ -1816,14 +1830,12 @@ function LoginForm(Props: any): VNode {
ref.current?.focus(); ref.current?.focus();
}, []); }, []);
const errors = !submitData ? undefined : undefinedIfEmpty({ const errors = !submitData
username: !submitData.username ? undefined
? i18n`Missing username` : undefinedIfEmpty({
: undefined, username: !submitData.username ? i18n`Missing username` : undefined,
password: !submitData.password password: !submitData.password ? i18n`Missing password` : undefined,
? i18n`Missing password` });
: undefined,
});
return ( return (
<div class="login-div"> <div class="login-div">
@ -1893,7 +1905,7 @@ function LoginForm(Props: any): VNode {
submitDataSetter({ submitDataSetter({
password: "", password: "",
repeatPassword: "", repeatPassword: "",
username:"", username: "",
}); });
}} }}
> >
@ -2043,10 +2055,7 @@ function RegistrationForm(Props: any): VNode {
password: "", password: "",
repeatPassword: "", repeatPassword: "",
}); });
pageStateSetter((prevState: PageStateType) => ({ route("/account");
...prevState,
tryRegister: false,
}));
}} }}
> >
{i18n`Cancel`} {i18n`Cancel`}
@ -2069,8 +2078,8 @@ function Transactions(Props: any): VNode {
`access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`, `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
); );
useEffect(() => { useEffect(() => {
mutate() mutate();
}, [balanceValue]) }, [balanceValue]);
if (typeof error !== "undefined") { if (typeof error !== "undefined") {
console.log("transactions not found error", error); console.log("transactions not found error", error);
switch (error.status) { switch (error.status) {
@ -2152,12 +2161,17 @@ function Account(Props: any): VNode {
// revalidateOnReconnect: false, // revalidateOnReconnect: false,
}); });
const [pageState, setPageState] = useContext(PageContext); const [pageState, setPageState] = useContext(PageContext);
const { withdrawalInProgress, withdrawalId, isLoggedIn, talerWithdrawUri, timestamp } = const {
pageState; withdrawalInProgress,
withdrawalId,
isLoggedIn,
talerWithdrawUri,
timestamp,
} = pageState;
const i18n = useTranslator(); const i18n = useTranslator();
useEffect(() => { useEffect(() => {
mutate() mutate();
}, [timestamp]) }, [timestamp]);
/** /**
* This part shows a list of transactions: with 5 elements by * This part shows a list of transactions: with 5 elements by
@ -2294,7 +2308,11 @@ function Account(Props: any): VNode {
<section id="main"> <section id="main">
<article> <article>
<h2>{i18n`Latest transactions:`}</h2> <h2>{i18n`Latest transactions:`}</h2>
<Transactions balanceValue={balanceValue} pageNumber="0" accountLabel={accountLabel} /> <Transactions
balanceValue={balanceValue}
pageNumber="0"
accountLabel={accountLabel}
/>
</article> </article>
</section> </section>
</BankFrame> </BankFrame>
@ -2440,50 +2458,39 @@ function PublicHistories(Props: any): VNode {
); );
} }
/** function PublicHistoriesPage(): VNode {
* If the user is logged in, it displays // const [backendState, backendStateSetter] = useBackendState();
* the balance, otherwise it offers to login. const [pageState, pageStateSetter] = usePageState();
*/ // const i18n = useTranslator();
export function BankHome(): VNode { return (
<SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
<PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame>
<PublicHistories pageStateSetter={pageStateSetter}>
<br />
<a
class="pure-button"
onClick={() => {
pageStateSetter((prevState: PageStateType) => ({
...prevState,
showPublicHistories: false,
}));
}}
>
Go back
</a>
</PublicHistories>
</BankFrame>
</PageContext.Provider>
</SWRWithoutCredentials>
);
}
function RegistrationPage(): VNode {
const [backendState, backendStateSetter] = useBackendState(); const [backendState, backendStateSetter] = useBackendState();
const [pageState, pageStateSetter] = usePageState(); const [pageState, pageStateSetter] = usePageState();
const i18n = useTranslator(); const i18n = useTranslator();
if (!bankUiSettings.allowRegistrations) {
if (pageState.showPublicHistories)
return (
<SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
<PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame>
<PublicHistories pageStateSetter={pageStateSetter}>
<br />
<a
class="pure-button"
onClick={() => {
pageStateSetter((prevState: PageStateType) => ({
...prevState,
showPublicHistories: false,
}));
}}
>
Go back
</a>
</PublicHistories>
</BankFrame>
</PageContext.Provider>
</SWRWithoutCredentials>
);
if (pageState.tryRegister) {
console.log("allow registrations?", bankUiSettings.allowRegistrations);
if (bankUiSettings.allowRegistrations)
return (
<PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame>
<RegistrationForm backendStateSetter={backendStateSetter} />
</BankFrame>
</PageContext.Provider>
);
return ( return (
<PageContext.Provider value={[pageState, pageStateSetter]}> <PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame> <BankFrame>
@ -2492,44 +2499,82 @@ export function BankHome(): VNode {
</PageContext.Provider> </PageContext.Provider>
); );
} }
if (pageState.isLoggedIn) {
if (typeof backendState === "undefined") {
pageStateSetter((prevState) => ({
...prevState,
hasError: true,
isLoggedIn: false,
error: {
title: i18n`Page has a problem: logged in but backend state is lost.`,
},
}));
return <p>Error: waiting for details...</p>;
}
console.log("Showing the profile page..");
return (
<SWRWithCredentials
username={backendState.username}
password={backendState.password}
backendUrl={backendState.url}
>
<PageContext.Provider value={[pageState, pageStateSetter]}>
<Account
accountLabel={backendState.username}
backendState={backendState}
/>
</PageContext.Provider>
</SWRWithCredentials>
);
} // end of logged-in state.
return ( return (
<PageContext.Provider value={[pageState, pageStateSetter]}> <PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame> <BankFrame>
<h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1> <RegistrationForm backendStateSetter={backendStateSetter} />
<LoginForm
pageStateSetter={pageStateSetter}
backendStateSetter={backendStateSetter}
/>
</BankFrame> </BankFrame>
</PageContext.Provider> </PageContext.Provider>
); );
} }
function AccountPage(): VNode {
const [backendState, backendStateSetter] = useBackendState();
const [pageState, pageStateSetter] = usePageState();
const i18n = useTranslator();
if (!pageState.isLoggedIn) {
return (
<PageContext.Provider value={[pageState, pageStateSetter]}>
<BankFrame>
<h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1>
<LoginForm
pageStateSetter={pageStateSetter}
backendStateSetter={backendStateSetter}
/>
</BankFrame>
</PageContext.Provider>
);
}
if (typeof backendState === "undefined") {
pageStateSetter((prevState) => ({
...prevState,
hasError: true,
isLoggedIn: false,
error: {
title: i18n`Page has a problem: logged in but backend state is lost.`,
},
}));
return <p>Error: waiting for details...</p>;
}
console.log("Showing the profile page..");
return (
<SWRWithCredentials
username={backendState.username}
password={backendState.password}
backendUrl={backendState.url}
>
<PageContext.Provider value={[pageState, pageStateSetter]}>
<Account
accountLabel={backendState.username}
backendState={backendState}
/>
</PageContext.Provider>
</SWRWithCredentials>
);
}
function Redirect({ to }: { to: string }): VNode {
useEffect(() => {
debugger;
route(to, true);
}, []);
return <div>being redirected to {to}</div>;
}
/**
* If the user is logged in, it displays
* the balance, otherwise it offers to login.
*/
export function BankHome(): VNode {
const history = createHashHistory();
return (
<Router history={history}>
<Route path="/public-accounts" component={PublicHistoriesPage} />
<Route path="/register" component={RegistrationPage} />
<Route path="/account/:id*" component={AccountPage} />
<Route default component={Redirect} to="/account" />
</Router>
);
}

View File

@ -105,6 +105,7 @@ importers:
esbuild-sass-plugin: ^2.4.0 esbuild-sass-plugin: ^2.4.0
eslint: ^8.26.0 eslint: ^8.26.0
eslint-config-preact: ^1.2.0 eslint-config-preact: ^1.2.0
history: 4.10.1
jed: 1.1.1 jed: 1.1.1
po2json: ^0.4.5 po2json: ^0.4.5
preact: 10.6.5 preact: 10.6.5
@ -117,6 +118,7 @@ importers:
dependencies: dependencies:
'@gnu-taler/taler-util': link:../taler-util '@gnu-taler/taler-util': link:../taler-util
date-fns: 2.29.3 date-fns: 2.29.3
history: 4.10.1
jed: 1.1.1 jed: 1.1.1
preact: 10.6.5 preact: 10.6.5
preact-router: 3.2.1_preact@10.6.5 preact-router: 3.2.1_preact@10.6.5