diff options
Diffstat (limited to 'packages/anastasis-webui/src/pages/home/index.tsx')
-rw-r--r-- | packages/anastasis-webui/src/pages/home/index.tsx | 336 |
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, + }, + ]} + /> ); } |