wallet-core/packages/anastasis-webui/src/pages/home/index.tsx

358 lines
9.7 KiB
TypeScript
Raw Normal View History

2022-06-06 16:46:49 +02:00
/*
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
GNU Anastasis is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
2022-01-24 18:39:27 +01:00
import { BackupStates, RecoveryStates } from "@gnu-taler/anastasis-core";
2021-10-19 15:56:52 +02:00
import {
ComponentChildren,
Fragment,
FunctionalComponent,
h,
VNode,
2021-10-19 15:56:52 +02:00
} from "preact";
2022-04-13 21:10:11 +02:00
import { useCallback, useEffect, useErrorBoundary } from "preact/hooks";
2022-06-06 05:54:55 +02:00
import { AsyncButton } from "../../components/AsyncButton.js";
import { Menu } from "../../components/menu/index.js";
import { Notifications } from "../../components/Notifications.js";
import {
AnastasisProvider,
useAnastasisContext,
2022-06-06 05:54:55 +02:00
} from "../../context/anastasis.js";
2021-10-19 15:56:52 +02:00
import {
AnastasisReducerApi,
useAnastasisReducer,
2022-06-06 05:54:55 +02:00
} from "../../hooks/use-anastasis-reducer.js";
import { AttributeEntryScreen } from "./AttributeEntryScreen.js";
import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen.js";
import { BackupFinishedScreen } from "./BackupFinishedScreen.js";
import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen.js";
import { ChallengePayingScreen } from "./ChallengePayingScreen.js";
import { ContinentSelectionScreen } from "./ContinentSelectionScreen.js";
import { PoliciesPayingScreen } from "./PoliciesPayingScreen.js";
import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen.js";
import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen.js";
import { SecretEditorScreen } from "./SecretEditorScreen.js";
import { SecretSelectionScreen } from "./SecretSelectionScreen.js";
import { SolveScreen } from "./SolveScreen.js";
import { StartScreen } from "./StartScreen.js";
import { TruthsPayingScreen } from "./TruthsPayingScreen.js";
2021-10-19 15:56:52 +02:00
function isBackup(reducer: AnastasisReducerApi): boolean {
2022-04-13 19:32:12 +02:00
return reducer.currentReducerState?.reducer_type === "backup";
2021-10-19 15:56:52 +02:00
}
export function withProcessLabel(
reducer: AnastasisReducerApi,
text: string,
): string {
2021-10-19 15:56:52 +02:00
if (isBackup(reducer)) {
return `Backup: ${text}`;
}
return `Recovery: ${text}`;
}
interface AnastasisClientFrameProps {
2021-11-08 17:09:26 +01:00
onNext?(): Promise<void>;
/**
* Override for the "back" functionality.
*/
onBack?(): Promise<void>;
2021-10-19 15:56:52 +02:00
title: string;
children: ComponentChildren;
/**
* Should back/next buttons be provided?
*/
hideNav?: boolean;
/**
* Hide only the "next" button.
*/
hideNext?: string;
2021-10-19 15:56:52 +02:00
}
function ErrorBoundary(props: {
reducer: AnastasisReducerApi;
children: ComponentChildren;
2021-10-22 06:31:46 +02:00
}): VNode {
const [error, resetError] = useErrorBoundary((error) =>
2022-08-26 17:59:00 +02:00
console.log("ErrorBoundary got error", error),
);
if (error) {
return (
<div>
<button
onClick={() => {
props.reducer.reset();
resetError();
}}
>
Reset
</button>
<p>
Error: <pre>{error.stack}</pre>
</p>
</div>
);
}
return <div>{props.children}</div>;
}
2022-04-14 21:35:00 +02:00
let currentHistoryId = 0;
2021-10-19 15:56:52 +02:00
export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
2021-10-22 06:31:46 +02:00
const reducer = useAnastasisContext();
2022-06-06 05:54:55 +02:00
2022-04-14 21:35:00 +02:00
const doBack = async (): Promise<void> => {
if (props.onBack) {
await props.onBack();
2022-04-14 22:08:36 +02:00
} else {
2022-06-06 05:54:55 +02:00
if (!reducer) return;
2022-04-14 22:08:36 +02:00
await reducer.back();
2022-04-14 21:35:00 +02:00
}
};
2022-04-14 22:08:36 +02:00
const doNext = async (fromPopstate?: boolean): Promise<void> => {
if (!fromPopstate) {
try {
const nextId: number =
(history.state && typeof history.state.id === "number"
? history.state.id
: 0) + 1;
2022-04-14 21:35:00 +02:00
2022-04-14 22:08:36 +02:00
currentHistoryId = nextId;
2022-04-14 21:35:00 +02:00
2022-04-14 22:08:36 +02:00
history.pushState({ id: nextId }, "unused", `#${nextId}`);
} catch (e) {
2022-08-26 17:59:00 +02:00
console.log("ERROR doNext ", e);
2022-04-14 22:08:36 +02:00
}
2022-04-14 21:35:00 +02:00
}
2021-11-04 19:18:30 +01:00
if (props.onNext) {
await props.onNext();
} else {
2022-06-06 05:54:55 +02:00
if (!reducer) return;
2021-11-04 19:18:30 +01:00
await reducer.transition("next", {});
}
2021-10-19 15:56:52 +02:00
};
const handleKeyPress = (
e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
): void => {
2022-06-09 21:11:49 +02:00
// console.log("Got key press", e.key);
2021-10-19 15:56:52 +02:00
// FIXME: By default, "next" action should be executed here
};
2022-04-13 21:10:11 +02:00
2022-04-14 21:35:00 +02:00
const browserOnBackButton = useCallback(async (ev: PopStateEvent) => {
//check if we are going back or forward
if (!ev.state || ev.state.id === 0 || ev.state.id < currentHistoryId) {
2022-04-14 22:08:36 +02:00
await doBack();
2022-04-14 21:35:00 +02:00
} else {
2022-04-14 22:08:36 +02:00
await doNext(true);
2022-04-14 21:35:00 +02:00
}
2022-04-13 21:10:11 +02:00
// reducer
return false;
}, []);
useEffect(() => {
window.addEventListener("popstate", browserOnBackButton);
return () => {
window.removeEventListener("popstate", browserOnBackButton);
};
}, []);
2022-06-12 00:10:26 +02:00
// if (!reducer) {
// return <p>Fatal: Reducer must be in context.</p>;
// }
2022-04-13 21:10:11 +02:00
return (
<Fragment>
<div class="home" onKeyPress={(e) => handleKeyPress(e)}>
<h1 class="title">{props.title}</h1>
2021-11-04 19:18:30 +01:00
<ErrorBanner />
<section class="section is-main-section">
{props.children}
{!props.hideNav ? (
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
2022-04-14 22:08:36 +02:00
<button class="button" onClick={() => doBack()}>
Back
</button>
<AsyncButton
class="button is-info"
data-tooltip={props.hideNext}
2022-04-14 22:08:36 +02:00
onClick={() => doNext()}
disabled={props.hideNext !== undefined}
>
Next
</AsyncButton>
</div>
) : null}
</section>
2021-10-19 15:56:52 +02:00
</div>
</Fragment>
2021-10-19 15:56:52 +02:00
);
}
const AnastasisClient: FunctionalComponent = () => {
const reducer = useAnastasisReducer();
return (
2021-10-22 06:31:46 +02:00
<AnastasisProvider value={reducer}>
<ErrorBoundary reducer={reducer}>
2022-06-06 04:10:51 +02:00
<Menu title="Anastasis" />
<AnastasisClientImpl />
</ErrorBoundary>
2021-10-22 06:31:46 +02:00
</AnastasisProvider>
2021-10-19 15:56:52 +02:00
);
};
2021-11-05 15:17:42 +01:00
function AnastasisClientImpl(): VNode {
const reducer = useAnastasisContext();
2021-10-22 06:31:46 +02:00
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
const state = reducer.currentReducerState;
if (!state) {
return <StartScreen />;
2021-10-19 15:56:52 +02:00
}
if (
2022-04-13 19:32:12 +02:00
(state.reducer_type === "backup" &&
state.backup_state === BackupStates.ContinentSelecting) ||
(state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.ContinentSelecting) ||
(state.reducer_type === "backup" &&
state.backup_state === BackupStates.CountrySelecting) ||
(state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.CountrySelecting)
2021-10-19 15:56:52 +02:00
) {
return <ContinentSelectionScreen />;
2021-10-19 15:56:52 +02:00
}
if (
2022-04-13 19:32:12 +02:00
(state.reducer_type === "backup" &&
state.backup_state === BackupStates.UserAttributesCollecting) ||
(state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.UserAttributesCollecting)
2021-10-19 15:56:52 +02:00
) {
return <AttributeEntryScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.AuthenticationsEditing
) {
return <AuthenticationEditorScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.PoliciesReviewing
) {
return <ReviewPoliciesScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.SecretEditing
) {
2021-10-22 06:31:46 +02:00
return <SecretEditorScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.BackupFinished
) {
2021-10-22 06:31:46 +02:00
return <BackupFinishedScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.TruthsPaying
) {
2021-10-22 06:31:46 +02:00
return <TruthsPayingScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.PoliciesPaying
) {
2021-10-22 06:31:46 +02:00
return <PoliciesPayingScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.SecretSelecting
) {
return <SecretSelectionScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.ChallengeSelecting
) {
return <ChallengeOverviewScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.ChallengeSolving
) {
2021-10-22 06:31:46 +02:00
return <SolveScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.RecoveryFinished
) {
return <RecoveryFinishedScreen />;
2021-10-19 15:56:52 +02:00
}
2022-04-13 19:32:12 +02:00
if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.ChallengePaying
) {
return <ChallengePayingScreen />;
}
2021-10-19 15:56:52 +02:00
console.log("unknown state", reducer.currentReducerState);
return (
<AnastasisClientFrame hideNav title="Bug">
<p>Bug: Unknown state.</p>
2021-10-22 06:31:46 +02:00
<div class="buttons is-right">
<button class="button" onClick={() => reducer.reset()}>
Reset
</button>
2021-10-22 06:31:46 +02:00
</div>
2021-10-19 15:56:52 +02:00
</AnastasisClientFrame>
);
2021-11-05 15:17:42 +01:00
}
2021-10-19 15:56:52 +02:00
/**
2021-10-22 06:31:46 +02:00
* Show a dismissible error banner if there is a current error.
2021-10-19 15:56:52 +02:00
*/
2021-10-22 06:31:46 +02:00
function ErrorBanner(): VNode | null {
const reducer = useAnastasisContext();
if (!reducer || !reducer.currentError) return null;
return (
<Notifications
removeNotification={reducer.dismissError}
notifications={[
{
type: "ERROR",
message: `Error code: ${reducer.currentError.code}`,
description: reducer.currentError.hint,
},
]}
/>
2021-10-22 06:31:46 +02:00
);
2021-10-19 15:56:52 +02:00
}
export default AnastasisClient;