diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts index efa0592dd..3acaaa361 100644 --- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts +++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts @@ -32,6 +32,11 @@ export interface ReducerStateBackup { payto: string; provider: string; }[]; + + core_secret?: { + mime: string; + value: string; + }; } export interface AuthMethod { diff --git a/packages/anastasis-webui/src/routes/home/index.tsx b/packages/anastasis-webui/src/routes/home/index.tsx index c6bf15be6..b1d017f30 100644 --- a/packages/anastasis-webui/src/routes/home/index.tsx +++ b/packages/anastasis-webui/src/routes/home/index.tsx @@ -45,43 +45,39 @@ function withProcessLabel(reducer: AnastasisReducerApi, text: string): string { function ContinentSelection(props: CommonReducerProps) { const { reducer, reducerState } = props; + const sel = (x: string) => + reducer.transition("select_continent", { continent: x }); return ( - {reducerState.continents.map((x: any) => { - const sel = (x: string) => - reducer.transition("select_continent", { continent: x }); - return ( - - ); - })} + {reducerState.continents.map((x: any) => ( + + ))} ); } function CountrySelection(props: CommonReducerProps) { const { reducer, reducerState } = props; + const sel = (x: any) => + reducer.transition("select_country", { + country_code: x.code, + currencies: [x.currency], + }); return ( - {reducerState.countries.map((x: any) => { - const sel = (x: any) => - reducer.transition("select_country", { - country_code: x.code, - currencies: [x.currency], - }); - return ( - - ); - })} + {reducerState.countries.map((x: any) => ( + + ))} ); } @@ -106,21 +102,85 @@ function SolveQuestionEntry(props: SolveEntryProps) { >

Feedback: {JSON.stringify(feedback)}

Question: {challenge.instructions}

- + + + ); +} + +function SolveSmsEntry(props: SolveEntryProps) { + const [answer, setAnswer] = useState(""); + const { reducer, challenge, feedback } = props; + const next = () => + reducer.transition("solve_challenge", { + answer, + }); + return ( + next()} + > +

Feedback: {JSON.stringify(feedback)}

+

{challenge.instructions}

+ +
+ ); +} + +function SolvePostEntry(props: SolveEntryProps) { + const [answer, setAnswer] = useState(""); + const { reducer, challenge, feedback } = props; + const next = () => + reducer.transition("solve_challenge", { + answer, + }); + return ( + next()} + > +

Feedback: {JSON.stringify(feedback)}

+

{challenge.instructions}

+ +
+ ); +} + +function SolveEmailEntry(props: SolveEntryProps) { + const [answer, setAnswer] = useState(""); + const { reducer, challenge, feedback } = props; + const next = () => + reducer.transition("solve_challenge", { + answer, + }); + return ( + next()} + > +

Feedback: {JSON.stringify(feedback)}

+

{challenge.instructions}

+ +
+ ); +} + +function SolveUnsupportedEntry(props: SolveEntryProps) { + return ( + +

{JSON.stringify(props.challenge)}

+

Challenge not supported.

); } function SecretEditor(props: BackupReducerProps) { const { reducer } = props; - const [secretName, setSecretName] = useState(""); - const [secretValue, setSecretValue] = useState(""); + const [secretName, setSecretName] = useState( + props.backupState.secret_name ?? "", + ); + const [secretValue, setSecretValue] = useState( + props.backupState.core_secret?.value ?? "" ?? "", + ); const secretNext = () => { reducer.runTransaction(async (tx) => { await tx.transition("enter_secret_name", { @@ -144,34 +204,17 @@ function SecretEditor(props: BackupReducerProps) { onNext={() => secretNext()} >
- +
- -
- or: -
- +
); @@ -234,6 +277,7 @@ function SecretSelection(props: RecoveryReducerProps) { const [otherVersion, setOtherVersion] = useState( recoveryState.recovery_document?.version ?? 0, ); + const recoveryDocument = recoveryState.recovery_document!; const [otherProvider, setOtherProvider] = useState(""); function selectVersion(p: string, n: number) { reducer.runTransaction(async (tx) => { @@ -250,9 +294,11 @@ function SecretSelection(props: RecoveryReducerProps) {

Select a different version of the secret

@@ -264,7 +310,7 @@ function SecretSelection(props: RecoveryReducerProps) { type="number" />
@@ -280,9 +326,9 @@ function SecretSelection(props: RecoveryReducerProps) { } return ( -

Provider: {recoveryState.recovery_document!.provider_url}

-

Secret version: {recoveryState.recovery_document!.version}

-

Secret name: {recoveryState.recovery_document!.version}

+

Provider: {recoveryDocument.provider_url}

+

Secret version: {recoveryDocument.version}

+

Secret name: {recoveryDocument.version}

@@ -305,37 +351,99 @@ interface AnastasisClientFrameProps { } function AnastasisClientFrame(props: AnastasisClientFrameProps) { + const reducer = useContext(WithReducer); + if (!reducer) { + return

Fatal: Reducer must be in context.

; + } + const next = () => { + if (props.onNext) { + props.onNext(); + } else { + reducer.transition("next", {}); + } + }; + const handleKeyPress = (e: h.JSX.TargetedKeyboardEvent) => { + console.log("Got key press", e.key); + // FIXME: By default, "next" action should be executed here + }; return ( - - {(reducer) => { - if (!reducer) { - return

Fatal: Reducer must be in context.

; - } - const next = () => { - if (props.onNext) { - props.onNext(); - } else { - reducer.transition("next", {}); - } - }; +
handleKeyPress(e)}> + +

{props.title}

+ + {props.children} + {!props.hideNav ? ( +
+ + {!props.hideNext ? ( + + ) : null} +
+ ) : null} +
+ ); +} + +function ChallengeOverview(props: RecoveryReducerProps) { + const { recoveryState, reducer } = props; + const policies = recoveryState.recovery_information!.policies; + const chArr = recoveryState.recovery_information!.challenges; + const challenges: { + [uuid: string]: { + type: string; + instructions: string; + cost: string; + }; + } = {}; + for (const ch of chArr) { + challenges[ch.uuid] = { + type: ch.type, + cost: ch.cost, + instructions: ch.instructions, + }; + } + return ( + +

Policies

+ {policies.map((x, i) => { return ( -
- -

{props.title}

- - {props.children} - {!props.hideNav ? ( -
- - {!props.hideNext ? ( - - ) : null} -
- ) : null} +
+

Policy #{i + 1}

+ {x.map((x) => { + const ch = challenges[x.uuid]; + const feedback = recoveryState.challenge_feedback?.[x.uuid]; + return ( +
+

+ {ch.type} ({ch.instructions}) +

+

Status: {feedback?.state ?? "unknown"}

+ {feedback?.state !== "solved" ? ( + + ) : null} +
+ ); + })}
); - }} - + })} + ); } @@ -472,51 +580,7 @@ const AnastasisClientImpl: FunctionalComponent = () => { } if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) { - const policies = reducerState.recovery_information!.policies; - const chArr = reducerState.recovery_information!.challenges; - const challenges: { - [uuid: string]: { - type: string; - instructions: string; - cost: string; - }; - } = {}; - for (const ch of chArr) { - challenges[ch.uuid] = { - type: ch.type, - cost: ch.cost, - instructions: ch.instructions, - }; - } - return ( - -

Policies

- {policies.map((x, i) => { - return ( -
-

Policy #{i + 1}

- {x.map((x) => { - const ch = challenges[x.uuid]; - return ( -
- {ch.type} ({ch.instructions}) - -
- ); - })} -
- ); - })} -
- ); + return ; } if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) { @@ -530,22 +594,21 @@ const AnastasisClientImpl: FunctionalComponent = () => { challenges[ch.uuid] = ch; } const selectedChallenge = challenges[selectedUuid]; - if (selectedChallenge.type === "question") { - return ( - - ); - } else { - return ( - -

{JSON.stringify(selectedChallenge)}

-

Challenge not supported.

-
- ); - } + const dialogMap: Record h.JSX.Element> = { + question: SolveQuestionEntry, + sms: SolveSmsEntry, + email: SolveEmailEntry, + post: SolvePostEntry, + }; + const SolveDialog = + dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry; + return ( + + ); } if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) { @@ -620,6 +683,14 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) { function AuthMethodQuestionSetup(props: AuthMethodSetupProps) { const [questionText, setQuestionText] = useState(""); const [answerText, setAnswerText] = useState(""); + const addQuestionAuth = () => + props.addAuthMethod({ + authentication_method: { + type: "question", + instructions: questionText, + challenge: encodeCrock(stringToBytes(answerText)), + }, + }); return (
@@ -630,44 +701,18 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) { here.

- +
- +
- +
@@ -684,16 +729,11 @@ function AuthMethodEmailSetup(props: AuthMethodSetupProps) { email.

- +
@@ -723,24 +763,20 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) { const [country, setCountry] = useState(""); const addPostAuth = () => { - () => - props.addAuthMethod({ - authentication_method: { - type: "email", - instructions: `Letter to address in postal code ${postcode}`, - challenge: encodeCrock( - stringToBytes( - canonicalJson({ - full_name: fullName, - street, - city, - postcode, - country, - }), - ), - ), - }, - }); + const challengeJson = { + full_name: fullName, + street, + city, + postcode, + country, + }; + props.addAuthMethod({ + authentication_method: { + type: "email", + instructions: `Letter to address in postal code ${postcode}`, + challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))), + }, + }); }; return ( @@ -753,59 +789,23 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) { code that you will receive in a letter to that address.

- +
- +
- +
- +
- +
@@ -851,48 +851,23 @@ function AuthenticationEditor(props: AuthenticationEditorProps) { reducer.transition("add_authentication", args); setSelectedMethod(undefined); }; - switch (selectedMethod) { - case "sms": - return ( - - ); - case "question": - return ( - - ); - case "email": - return ( - - ); - case "post": - return ( - - ); - default: - return ( - - ); - } + const methodMap: Record< + string, + (props: AuthMethodSetupProps) => h.JSX.Element + > = { + sms: AuthMethodSmsSetup, + question: AuthMethodQuestionSetup, + email: AuthMethodEmailSetup, + post: AuthMethodPostSetup, + }; + const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented; + return ( + + ); } function MethodButton(props: { method: string; label: String }) { return ( @@ -978,6 +953,32 @@ function AttributeEntry(props: AttributeEntryProps) { ); } +interface LabeledInputProps { + label: string; + grabFocus?: boolean; + bind: [string, (x: string) => void]; +} + +function LabeledInput(props: LabeledInputProps) { + const inputRef = useRef(null); + useLayoutEffect(() => { + if (props.grabFocus) { + inputRef.current?.focus(); + } + }, []); + return ( + + ); +} + export interface AttributeEntryFieldProps { isFirst: boolean; value: string; @@ -988,13 +989,10 @@ export interface AttributeEntryFieldProps { function AttributeEntryField(props: AttributeEntryFieldProps) { return (
- - props.setValue((e as any).target.value)} +
); diff --git a/packages/anastasis-webui/src/routes/home/style.css b/packages/anastasis-webui/src/routes/home/style.css index b94981f10..e70f11a59 100644 --- a/packages/anastasis-webui/src/routes/home/style.css +++ b/packages/anastasis-webui/src/routes/home/style.css @@ -2,6 +2,7 @@ padding: 1em 1em; min-height: 100%; width: 100%; + max-width: 40em; } .home div { diff --git a/packages/anastasis-webui/tests/header.test.tsx b/packages/anastasis-webui/tests/header.test.tsx deleted file mode 100644 index b2cfc2f4d..000000000 --- a/packages/anastasis-webui/tests/header.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { h } from 'preact'; -import Header from '../src/components/header'; -// See: https://github.com/preactjs/enzyme-adapter-preact-pure -import { shallow } from 'enzyme'; - -describe('Initial Test of the Header', () => { - test('Header renders 3 nav items', () => { - const context = shallow(
); - expect(context.find('h1').text()).toBe('Preact App'); - expect(context.find('Link').length).toBe(3); - }); -});