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 {
|
2021-11-08 16:10:22 +01:00
|
|
|
ComponentChildren,
|
|
|
|
Fragment,
|
2021-10-21 13:11:17 +02:00
|
|
|
FunctionalComponent,
|
|
|
|
h,
|
2021-11-08 16:10:22 +01:00
|
|
|
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";
|
2021-11-08 16:10:22 +01:00
|
|
|
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,
|
2021-11-08 16:10:22 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-10-21 13:11:17 +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>;
|
2021-11-08 16:10:22 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-11-01 20:10:49 +01:00
|
|
|
hideNext?: string;
|
2021-10-19 15:56:52 +02:00
|
|
|
}
|
|
|
|
|
2021-10-21 13:11:17 +02:00
|
|
|
function ErrorBoundary(props: {
|
|
|
|
reducer: AnastasisReducerApi;
|
|
|
|
children: ComponentChildren;
|
2021-10-22 06:31:46 +02:00
|
|
|
}): VNode {
|
2021-10-21 13:11:17 +02:00
|
|
|
const [error, resetError] = useErrorBoundary((error) =>
|
|
|
|
console.log("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) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
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
|
|
|
};
|
2021-10-21 13:11:17 +02:00
|
|
|
const handleKeyPress = (
|
|
|
|
e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
|
|
|
|
): void => {
|
2021-10-19 15:56:52 +02:00
|
|
|
console.log("Got key press", e.key);
|
|
|
|
// 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-06 05:54:55 +02:00
|
|
|
if (!reducer) {
|
|
|
|
return <p>Fatal: Reducer must be in context.</p>;
|
|
|
|
}
|
2022-04-13 21:10:11 +02:00
|
|
|
|
2021-10-21 13:11:17 +02:00
|
|
|
return (
|
|
|
|
<Fragment>
|
2021-11-03 21:30:11 +01:00
|
|
|
<div class="home" onKeyPress={(e) => handleKeyPress(e)}>
|
|
|
|
<h1 class="title">{props.title}</h1>
|
2021-11-04 19:18:30 +01:00
|
|
|
<ErrorBanner />
|
2021-11-03 21:30:11 +01:00
|
|
|
<section class="section is-main-section">
|
2021-10-21 13:11:17 +02:00
|
|
|
{props.children}
|
|
|
|
{!props.hideNav ? (
|
2021-11-08 16:10:22 +01:00
|
|
|
<div
|
|
|
|
style={{
|
|
|
|
marginTop: "2em",
|
|
|
|
display: "flex",
|
|
|
|
justifyContent: "space-between",
|
|
|
|
}}
|
|
|
|
>
|
2022-04-14 22:08:36 +02:00
|
|
|
<button class="button" onClick={() => doBack()}>
|
2021-11-08 16:10:22 +01:00
|
|
|
Back
|
|
|
|
</button>
|
|
|
|
<AsyncButton
|
|
|
|
class="button is-info"
|
|
|
|
data-tooltip={props.hideNext}
|
2022-04-14 22:08:36 +02:00
|
|
|
onClick={() => doNext()}
|
2021-11-08 16:10:22 +01:00
|
|
|
disabled={props.hideNext !== undefined}
|
|
|
|
>
|
|
|
|
Next
|
|
|
|
</AsyncButton>
|
2021-10-21 13:11:17 +02:00
|
|
|
</div>
|
|
|
|
) : null}
|
2021-11-03 21:30:11 +01:00
|
|
|
</section>
|
2021-10-19 15:56:52 +02:00
|
|
|
</div>
|
2021-10-21 13:11:17 +02:00
|
|
|
</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}>
|
2021-10-21 13:11:17 +02:00
|
|
|
<ErrorBoundary reducer={reducer}>
|
2022-06-06 04:10:51 +02:00
|
|
|
<Menu title="Anastasis" />
|
2021-10-21 13:11:17 +02:00
|
|
|
<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 {
|
2021-11-08 16:10:22 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
console.log("state", reducer.currentReducerState);
|
|
|
|
|
|
|
|
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
|
|
|
) {
|
2021-11-08 16:10:22 +01: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
|
|
|
) {
|
2021-11-08 16:10:22 +01: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
|
|
|
|
) {
|
2021-11-08 16:10:22 +01:00
|
|
|
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
|
|
|
|
) {
|
2021-11-08 16:10:22 +01:00
|
|
|
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
|
|
|
|
) {
|
2021-11-08 16:10:22 +01:00
|
|
|
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
|
|
|
|
) {
|
2021-11-08 16:10:22 +01:00
|
|
|
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
|
|
|
|
) {
|
2021-11-08 16:10:22 +01:00
|
|
|
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
|
|
|
|
) {
|
2021-10-26 17:08:03 +02:00
|
|
|
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">
|
2021-11-08 16:10:22 +01:00
|
|
|
<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;
|
2021-11-08 16:10:22 +01:00
|
|
|
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;
|