summaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src/pages/home/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/anastasis-webui/src/pages/home/index.tsx')
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx336
1 files changed, 208 insertions, 128 deletions
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 4cec47ec8..c665144a4 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -1,43 +1,55 @@
+/*
+ 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/>
+ */
+import { BackupStates, RecoveryStates } from "@gnu-taler/anastasis-core";
import {
- BackupStates,
- RecoveryStates,
- ReducerStateBackup,
- ReducerStateRecovery
-} from "anastasis-core";
-import {
- ComponentChildren, Fragment,
+ ComponentChildren,
+ Fragment,
FunctionalComponent,
h,
- VNode
+ VNode,
} from "preact";
+import { useCallback, useEffect, useErrorBoundary } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton.js";
+import { Menu } from "../../components/menu/index.js";
+import { Notifications } from "../../components/Notifications.js";
import {
- useErrorBoundary,
- useLayoutEffect,
- useRef
-} from "preact/hooks";
-import { Menu } from "../../components/menu";
-import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
+ AnastasisProvider,
+ useAnastasisContext,
+} from "../../context/anastasis.js";
import {
AnastasisReducerApi,
- useAnastasisReducer
-} from "../../hooks/use-anastasis-reducer";
-import { AttributeEntryScreen } from "./AttributeEntryScreen";
-import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
-import { BackupFinishedScreen } from "./BackupFinishedScreen";
-import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen";
-import { ContinentSelectionScreen } from "./ContinentSelectionScreen";
-import { CountrySelectionScreen } from "./CountrySelectionScreen";
-import { PoliciesPayingScreen } from "./PoliciesPayingScreen";
-import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen";
-import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen";
-import { SecretEditorScreen } from "./SecretEditorScreen";
-import { SecretSelectionScreen } from "./SecretSelectionScreen";
-import { SolveScreen } from "./SolveScreen";
-import { StartScreen } from "./StartScreen";
-import { TruthsPayingScreen } from "./TruthsPayingScreen";
+ useAnastasisReducer,
+} 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";
function isBackup(reducer: AnastasisReducerApi): boolean {
- return !!reducer.currentReducerState?.backup_state;
+ return reducer.currentReducerState?.reducer_type === "backup";
}
export function withProcessLabel(
@@ -51,7 +63,11 @@ export function withProcessLabel(
}
interface AnastasisClientFrameProps {
- onNext?(): void;
+ onNext?(): Promise<void>;
+ /**
+ * Override for the "back" functionality.
+ */
+ onBack?(): Promise<void>;
title: string;
children: ComponentChildren;
/**
@@ -61,7 +77,7 @@ interface AnastasisClientFrameProps {
/**
* Hide only the "next" button.
*/
- hideNext?: boolean;
+ hideNext?: string;
}
function ErrorBoundary(props: {
@@ -69,7 +85,7 @@ function ErrorBoundary(props: {
children: ComponentChildren;
}): VNode {
const [error, resetError] = useErrorBoundary((error) =>
- console.log("got error", error),
+ console.log("ErrorBoundary got error", error),
);
if (error) {
return (
@@ -91,39 +107,100 @@ function ErrorBoundary(props: {
return <div>{props.children}</div>;
}
+let currentHistoryId = 0;
+
export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
const reducer = useAnastasisContext();
- if (!reducer) {
- return <p>Fatal: Reducer must be in context.</p>;
- }
- const next = (): void => {
+
+ const doBack = async (): Promise<void> => {
+ if (props.onBack) {
+ await props.onBack();
+ } else {
+ if (!reducer) return;
+ await reducer.back();
+ }
+ };
+ 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;
+
+ currentHistoryId = nextId;
+
+ history.pushState({ id: nextId }, "unused", `#${nextId}`);
+ } catch (e) {
+ console.log("ERROR doNext ", e);
+ }
+ }
+
if (props.onNext) {
- props.onNext();
+ await props.onNext();
} else {
- reducer.transition("next", {});
+ if (!reducer) return;
+ await reducer.transition("next", {});
}
};
const handleKeyPress = (
e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
): void => {
- console.log("Got key press", e.key);
+ // console.log("Got key press", e.key);
// FIXME: By default, "next" action should be executed here
};
+
+ 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) {
+ await doBack();
+ } else {
+ await doNext(true);
+ }
+
+ // reducer
+ return false;
+ }, []);
+ useEffect(() => {
+ window.addEventListener("popstate", browserOnBackButton);
+
+ return () => {
+ window.removeEventListener("popstate", browserOnBackButton);
+ };
+ }, []);
+ // if (!reducer) {
+ // return <p>Fatal: Reducer must be in context.</p>;
+ // }
+
return (
<Fragment>
- <Menu title="Anastasis" />
- <div>
- <div class="home" onKeyPress={(e) => handleKeyPress(e)}>
- <h1>{props.title}</h1>
- <ErrorBanner />
+ <div class="home" onKeyPress={(e) => handleKeyPress(e)}>
+ <h1 class="title">{props.title}</h1>
+ <ErrorBanner />
+ <section class="section is-main-section">
{props.children}
{!props.hideNav ? (
- <div>
- <button onClick={() => reducer.back()}>Back</button>
- {!props.hideNext ? <button onClick={next}>Next</button> : null}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => doBack()}>
+ Back
+ </button>
+ <AsyncButton
+ class="button is-info"
+ data-tooltip={props.hideNext}
+ onClick={() => doNext()}
+ disabled={props.hideNext !== undefined}
+ >
+ Next
+ </AsyncButton>
</div>
) : null}
- </div>
+ </section>
</div>
</Fragment>
);
@@ -134,14 +211,15 @@ const AnastasisClient: FunctionalComponent = () => {
return (
<AnastasisProvider value={reducer}>
<ErrorBoundary reducer={reducer}>
+ <Menu title="Anastasis" />
<AnastasisClientImpl />
</ErrorBoundary>
</AnastasisProvider>
);
};
-const AnastasisClientImpl: FunctionalComponent = () => {
- const reducer = useAnastasisContext()
+function AnastasisClientImpl(): VNode {
+ const reducer = useAnastasisContext();
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
@@ -149,115 +227,113 @@ const AnastasisClientImpl: FunctionalComponent = () => {
if (!state) {
return <StartScreen />;
}
- console.log("state", reducer.currentReducerState);
+
+ // FIXME: Use switch statements here!
if (
- state.backup_state === BackupStates.ContinentSelecting ||
- state.recovery_state === RecoveryStates.ContinentSelecting
+ (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)
) {
- return (
- <ContinentSelectionScreen />
- );
+ return <ContinentSelectionScreen />;
}
if (
- state.backup_state === BackupStates.CountrySelecting ||
- state.recovery_state === RecoveryStates.CountrySelecting
+ (state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.UserAttributesCollecting) ||
+ (state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.UserAttributesCollecting)
) {
- return (
- <CountrySelectionScreen />
- );
+ return <AttributeEntryScreen />;
}
if (
- state.backup_state === BackupStates.UserAttributesCollecting ||
- state.recovery_state === RecoveryStates.UserAttributesCollecting
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.AuthenticationsEditing
) {
- return (
- <AttributeEntryScreen />
- );
+ return <AuthenticationEditorScreen />;
}
- if (state.backup_state === BackupStates.AuthenticationsEditing) {
- return (
- <AuthenticationEditorScreen />
- );
- }
- if (state.backup_state === BackupStates.PoliciesReviewing) {
- return (
- <ReviewPoliciesScreen />
- );
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.PoliciesReviewing
+ ) {
+ return <ReviewPoliciesScreen />;
}
- if (state.backup_state === BackupStates.SecretEditing) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.SecretEditing
+ ) {
return <SecretEditorScreen />;
}
- if (state.backup_state === BackupStates.BackupFinished) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.BackupFinished
+ ) {
return <BackupFinishedScreen />;
}
- if (state.backup_state === BackupStates.TruthsPaying) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.TruthsPaying
+ ) {
return <TruthsPayingScreen />;
}
- if (state.backup_state === BackupStates.PoliciesPaying) {
+ if (
+ state.reducer_type === "backup" &&
+ state.backup_state === BackupStates.PoliciesPaying
+ ) {
return <PoliciesPayingScreen />;
}
- if (state.recovery_state === RecoveryStates.SecretSelecting) {
- return (
- <SecretSelectionScreen />
- );
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.SecretSelecting
+ ) {
+ return <SecretSelectionScreen />;
}
- if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
- return (
- <ChallengeOverviewScreen />
- );
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.ChallengeSelecting
+ ) {
+ return <ChallengeOverviewScreen />;
}
- if (state.recovery_state === RecoveryStates.ChallengeSolving) {
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.ChallengeSolving
+ ) {
return <SolveScreen />;
}
- if (state.recovery_state === RecoveryStates.RecoveryFinished) {
- return (
- <RecoveryFinishedScreen />
- );
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.RecoveryFinished
+ ) {
+ return <RecoveryFinishedScreen />;
+ }
+ if (
+ state.reducer_type === "recovery" &&
+ state.recovery_state === RecoveryStates.ChallengePaying
+ ) {
+ return <ChallengePayingScreen />;
}
-
console.log("unknown state", reducer.currentReducerState);
return (
<AnastasisClientFrame hideNav title="Bug">
<p>Bug: Unknown state.</p>
<div class="buttons is-right">
- <button class="button" onClick={() => reducer.reset()}>Reset</button>
+ <button class="button" onClick={() => reducer.reset()}>
+ Reset
+ </button>
</div>
</AnastasisClientFrame>
);
-};
-
-interface LabeledInputProps {
- label: string;
- grabFocus?: boolean;
- bind: [string, (x: string) => void];
-}
-
-export function LabeledInput(props: LabeledInputProps): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) {
- inputRef.current?.focus();
- }
- }, [props.grabFocus]);
- return (
- <label>
- {props.label}
- <input
- value={props.bind[0]}
- onChange={(e) => props.bind[1]((e.target as HTMLInputElement).value)}
- ref={inputRef}
- style={{ display: "block" }}
- />
- </label>
- );
}
/**
@@ -267,12 +343,16 @@ function ErrorBanner(): VNode | null {
const reducer = useAnastasisContext();
if (!reducer || !reducer.currentError) return null;
return (
- <div id="error">
- <p>Error: {JSON.stringify(reducer.currentError)}</p>
- <button onClick={() => reducer.dismissError()}>
- Dismiss Error
- </button>
- </div>
+ <Notifications
+ removeNotification={reducer.dismissError}
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Error code: ${reducer.currentError.code}`,
+ description: reducer.currentError.hint,
+ },
+ ]}
+ />
);
}