summaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/anastasis-webui/src/pages')
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx50
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx101
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx81
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx150
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx42
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx69
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx46
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx51
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx222
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx7
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx33
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx223
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx207
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx (renamed from packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx)10
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx33
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx98
-rw-r--r--packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx27
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx109
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx133
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx5
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx2
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx7
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx14
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx215
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx78
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx5
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx33
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx8
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx179
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx25
-rw-r--r--packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx23
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx23
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx17
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.tsx220
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx25
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx12
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx3
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.tsx33
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx5
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx4
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx66
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx62
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx65
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx68
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx66
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx102
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx66
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx71
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx66
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx63
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx64
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx81
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx66
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx56
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/index.tsx69
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/totp.ts56
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx86
59 files changed, 3099 insertions, 784 deletions
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
new file mode 100644
index 000000000..43807fefe
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
@@ -0,0 +1,50 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { ReducerState } from 'anastasis-core';
+import { createExample, reducerStatesExample } from '../../utils';
+import { AddingProviderScreen as TestedComponent } from './AddingProviderScreen';
+
+
+export default {
+ title: 'Pages/backup/AddingProviderScreen',
+ component: TestedComponent,
+ args: {
+ order: 4,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+export const NewProvider = createExample(TestedComponent, {
+ ...reducerStatesExample.authEditing,
+} as ReducerState);
+
+export const NewSMSProvider = createExample(TestedComponent, {
+ ...reducerStatesExample.authEditing,
+} as ReducerState, { providerType: 'sms'});
+
+export const NewIBANProvider = createExample(TestedComponent, {
+ ...reducerStatesExample.authEditing,
+} as ReducerState, { providerType: 'iban' });
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
new file mode 100644
index 000000000..9c83da49e
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
@@ -0,0 +1,101 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { TextInput } from "../../components/fields/TextInput";
+import { authMethods, KnownAuthMethods } from "./authMethod";
+import { AnastasisClientFrame } from "./index";
+
+interface Props {
+ providerType?: KnownAuthMethods;
+ cancel: () => void;
+}
+export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
+ const [providerURL, setProviderURL] = useState("");
+ const [error, setError] = useState<string | undefined>()
+ const providerLabel = providerType ? authMethods[providerType].label : undefined
+
+ function testProvider(): void {
+ setError(undefined)
+
+ fetch(`${providerURL}/config`)
+ .then(r => r.json().catch(d => ({})))
+ .then(r => {
+ if (!("methods" in r) || !Array.isArray(r.methods)) {
+ setError("This provider doesn't have authentication method. Check the provider URL")
+ return;
+ }
+ if (!providerLabel) {
+ setError("")
+ return
+ }
+ let found = false
+ for (let i = 0; i < r.methods.length && !found; i++) {
+ found = r.methods[i].type !== providerType
+ }
+ if (!found) {
+ setError(`This provider does not support authentication method ${providerLabel}`)
+ }
+ })
+ .catch(e => {
+ setError(`There was an error testing this provider, try another one. ${e.message}`)
+ })
+
+ }
+ function addProvider(): void {
+ // addAuthMethod({
+ // authentication_method: {
+ // type: "sms",
+ // instructions: `SMS to ${providerURL}`,
+ // challenge: encodeCrock(stringToBytes(providerURL)),
+ // },
+ // });
+ }
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+
+ let errors = !providerURL ? 'Add provider URL' : undefined
+ try {
+ new URL(providerURL)
+ } catch {
+ errors = 'Check the URL'
+ }
+ if (!!error && !errors) {
+ errors = error
+ }
+
+ return (
+ <AnastasisClientFrame hideNav
+ title={!providerLabel ? `Backup: Adding a provider` : `Backup: Adding a ${providerLabel} provider`}
+ hideNext={errors}>
+ <div>
+ <p>
+ Add a provider url {errors}
+ </p>
+ <div class="container">
+ <TextInput
+ label="Provider URL"
+ placeholder="https://provider.com"
+ grabFocus
+ bind={[providerURL, setProviderURL]} />
+ </div>
+ {!!error && <p class="block has-text-danger">{error}</p>}
+ {error === "" && <p class="block has-text-success">This provider worked!</p>}
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={testProvider}>TEST</button>
+ </div>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span data-tooltip={errors}>
+ <button class="button is-info" disabled={errors !== undefined} onClick={addProvider}>Add</button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
index d28a6df43..549686616 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
@@ -28,36 +28,103 @@ import { AttributeEntryScreen as TestedComponent } from './AttributeEntryScreen'
export default {
title: 'Pages/AttributeEntryScreen',
component: TestedComponent,
+ args: {
+ order: 4,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
},
};
-export const WithSomeAttributes = createExample(TestedComponent, {
- ...reducerStatesExample.attributeEditing,
+export const Backup = createExample(TestedComponent, {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: [{
+ name: 'first name',
+ label: 'first',
+ type: 'string',
+ uuid: 'asdasdsa1',
+ widget: 'wid',
+ }, {
+ name: 'last name',
+ label: 'second',
+ type: 'string',
+ uuid: 'asdasdsa2',
+ widget: 'wid',
+ }, {
+ name: 'birthdate',
+ label: 'birthdate',
+ type: 'date',
+ uuid: 'asdasdsa3',
+ widget: 'calendar',
+ }]
+} as ReducerState);
+
+export const Recovery = createExample(TestedComponent, {
+ ...reducerStatesExample.recoveryAttributeEditing,
required_attributes: [{
name: 'first',
label: 'first',
- type: 'type',
+ type: 'string',
uuid: 'asdasdsa1',
widget: 'wid',
}, {
name: 'pepe',
label: 'second',
- type: 'type',
+ type: 'string',
uuid: 'asdasdsa2',
widget: 'wid',
}, {
name: 'pepe2',
label: 'third',
- type: 'type',
+ type: 'date',
uuid: 'asdasdsa3',
widget: 'calendar',
}]
} as ReducerState);
-export const Empty = createExample(TestedComponent, {
- ...reducerStatesExample.attributeEditing,
+export const WithNoRequiredAttribute = createExample(TestedComponent, {
+ ...reducerStatesExample.backupAttributeEditing,
required_attributes: undefined
} as ReducerState);
+
+const allWidgets = [
+ "anastasis_gtk_ia_aadhar_in",
+ "anastasis_gtk_ia_ahv",
+ "anastasis_gtk_ia_birthdate",
+ "anastasis_gtk_ia_birthnumber_cz",
+ "anastasis_gtk_ia_birthnumber_sk",
+ "anastasis_gtk_ia_birthplace",
+ "anastasis_gtk_ia_cf_it",
+ "anastasis_gtk_ia_cpr_dk",
+ "anastasis_gtk_ia_es_dni",
+ "anastasis_gtk_ia_es_ssn",
+ "anastasis_gtk_ia_full_name",
+ "anastasis_gtk_ia_my_jp",
+ "anastasis_gtk_ia_nid_al",
+ "anastasis_gtk_ia_nid_be",
+ "anastasis_gtk_ia_ssn_de",
+ "anastasis_gtk_ia_ssn_us",
+ "anastasis_gtk_ia_tax_de",
+ "anastasis_gtk_xx_prime",
+ "anastasis_gtk_xx_square",
+]
+
+function typeForWidget(name: string): string {
+ if (["anastasis_gtk_xx_prime",
+ "anastasis_gtk_xx_square",
+ ].includes(name)) return "number";
+ if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date"
+ return "string";
+}
+
+export const WithAllPosibleWidget = createExample(TestedComponent, {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: allWidgets.map(w => ({
+ name: w,
+ label: `widget: ${w}`,
+ type: typeForWidget(w),
+ uuid: `uuid-${w}`,
+ widget: w
+ }))
+} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
index 2f804f940..f86994c97 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
@@ -1,10 +1,13 @@
/* eslint-disable @typescript-eslint/camelcase */
-import { h, VNode } from "preact";
+import { UserAttributeSpec, validators } from "anastasis-core";
+import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { ReducerStateRecovery, ReducerStateBackup, UserAttributeSpec } from "anastasis-core/lib";
import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
-import { AnastasisClientFrame, withProcessLabel, LabeledInput } from "./index";
+import { AnastasisClientFrame, withProcessLabel } from "./index";
+import { TextInput } from "../../components/fields/TextInput";
+import { DateInput } from "../../components/fields/DateInput";
+import { NumberInput } from "../../components/fields/NumberInput";
+import { isAfter, parse } from "date-fns";
export function AttributeEntryScreen(): VNode {
const reducer = useAnastasisContext()
@@ -18,48 +21,139 @@ export function AttributeEntryScreen(): VNode {
if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) {
return <div>invalid state</div>
}
-
+ const reqAttr = reducer.currentReducerState.required_attributes || []
+ let hasErrors = false;
+
+ const fieldList: VNode[] = reqAttr.map((spec, i: number) => {
+ const value = attrs[spec.name]
+ const error = checkIfValid(value, spec)
+ hasErrors = hasErrors || error !== undefined
+ return (
+ <AttributeEntryField
+ key={i}
+ isFirst={i == 0}
+ setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })}
+ spec={spec}
+ errorMessage={error}
+ value={value} />
+ );
+ })
+
return (
<AnastasisClientFrame
- title={withProcessLabel(reducer, "Select Country")}
+ title={withProcessLabel(reducer, "Who are you?")}
+ hideNext={hasErrors ? "Complete the form." : undefined}
onNext={() => reducer.transition("enter_user_attributes", {
identity_attributes: attrs,
})}
>
- {reducer.currentReducerState.required_attributes?.map((x, i: number) => {
- return (
- <AttributeEntryField
- key={i}
- isFirst={i == 0}
- setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
- spec={x}
- value={attrs[x.name]} />
- );
- })}
+ <div class="columns" style={{ maxWidth: 'unset' }}>
+ <div class="column is-half">
+ {fieldList}
+ </div>
+ <div class="column is-is-half" >
+ <p>This personal information will help to locate your secret.</p>
+ <h1 class="title">This stays private</h1>
+ <p>The information you have entered here:</p>
+ <ul>
+ <li>
+ <span class="icon is-right">
+ <i class="mdi mdi-circle-small" />
+ </span>
+ Will be hashed, and therefore unreadable
+ </li>
+ <li><span class="icon is-right">
+ <i class="mdi mdi-circle-small" />
+ </span>The non-hashed version is not shared</li>
+ </ul>
+ </div>
+ </div>
</AnastasisClientFrame>
);
}
-interface AttributeEntryProps {
- reducer: AnastasisReducerApi;
- reducerState: ReducerStateRecovery | ReducerStateBackup;
-}
-
-export interface AttributeEntryFieldProps {
+interface AttributeEntryFieldProps {
isFirst: boolean;
value: string;
setValue: (newValue: string) => void;
spec: UserAttributeSpec;
+ errorMessage: string | undefined;
+}
+const possibleBirthdayYear: Array<number> = []
+for (let i = 0; i < 100; i++) {
+ possibleBirthdayYear.push(2020 - i)
}
+function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
-export function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
return (
<div>
- <LabeledInput
- grabFocus={props.isFirst}
- label={props.spec.label}
- bind={[props.value, props.setValue]}
- />
+ {props.spec.type === 'date' &&
+ <DateInput
+ grabFocus={props.isFirst}
+ label={props.spec.label}
+ years={possibleBirthdayYear}
+ error={props.errorMessage}
+ bind={[props.value, props.setValue]}
+ />}
+ {props.spec.type === 'number' &&
+ <NumberInput
+ grabFocus={props.isFirst}
+ label={props.spec.label}
+ error={props.errorMessage}
+ bind={[props.value, props.setValue]}
+ />
+ }
+ {props.spec.type === 'string' &&
+ <TextInput
+ grabFocus={props.isFirst}
+ label={props.spec.label}
+ error={props.errorMessage}
+ bind={[props.value, props.setValue]}
+ />
+ }
+ <div class="block">
+ This stays private
+ <span class="icon is-right">
+ <i class="mdi mdi-eye-off" />
+ </span>
+ </div>
</div>
);
}
+const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/
+
+
+function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined {
+ const pattern = spec['validation-regex']
+ if (pattern) {
+ const re = new RegExp(pattern)
+ if (!re.test(value)) return 'The value is invalid'
+ }
+ const logic = spec['validation-logic']
+ if (logic) {
+ const func = (validators as any)[logic];
+ if (func && typeof func === 'function' && !func(value)) return 'Please check the value'
+ }
+ const optional = spec.optional
+ if (!optional && !value) {
+ return 'This value is required'
+ }
+ if ("date" === spec.type) {
+ if (!YEAR_REGEX.test(value)) {
+ return "The date doesn't follow the format"
+ }
+
+ try {
+ const v = parse(value, 'yyyy-MM-dd', new Date());
+ if (Number.isNaN(v.getTime())) {
+ return "Some numeric values seems out of range for a date"
+ }
+ if ("birthdate" === spec.name && isAfter(v, new Date())) {
+ return "A birthdate cannot be in the future"
+ }
+ } catch (e) {
+ return "Could not parse the date"
+ }
+ }
+ return undefined
+}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx
deleted file mode 100644
index 9567e0ef7..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-
-export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {
- const [email, setEmail] = useState("");
- return (
- <AnastasisClientFrame hideNav title="Add email authentication">
- <p>
- For email authentication, you need to provide an email address. When
- recovering your secret, you will need to enter the code you receive by
- email.
- </p>
- <div>
- <LabeledInput
- label="Email address"
- grabFocus
- bind={[email, setEmail]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button
- onClick={() => props.addAuthMethod({
- authentication_method: {
- type: "email",
- instructions: `Email to ${email}`,
- challenge: encodeCrock(stringToBytes(email)),
- },
- })}
- >
- Add
- </button>
- </div>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx
deleted file mode 100644
index 55e37a968..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- canonicalJson, encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { LabeledInput } from "./index";
-
-export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {
- const [fullName, setFullName] = useState("");
- const [street, setStreet] = useState("");
- const [city, setCity] = useState("");
- const [postcode, setPostcode] = useState("");
- const [country, setCountry] = useState("");
-
- const addPostAuth = () => {
- 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 (
- <div class="home">
- <h1>Add {props.method} authentication</h1>
- <div>
- <p>
- For postal letter authentication, you need to provide a postal
- address. When recovering your secret, you will be asked to enter a
- code that you will receive in a letter to that address.
- </p>
- <div>
- <LabeledInput
- grabFocus
- label="Full Name"
- bind={[fullName, setFullName]} />
- </div>
- <div>
- <LabeledInput label="Street" bind={[street, setStreet]} />
- </div>
- <div>
- <LabeledInput label="City" bind={[city, setCity]} />
- </div>
- <div>
- <LabeledInput label="Postal Code" bind={[postcode, setPostcode]} />
- </div>
- <div>
- <LabeledInput label="Country" bind={[country, setCountry]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addPostAuth()}>Add</button>
- </div>
- </div>
- </div>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx
deleted file mode 100644
index 7699cdf34..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-
-export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {
- const [questionText, setQuestionText] = useState("");
- const [answerText, setAnswerText] = useState("");
- const addQuestionAuth = (): void => props.addAuthMethod({
- authentication_method: {
- type: "question",
- instructions: questionText,
- challenge: encodeCrock(stringToBytes(answerText)),
- },
- });
- return (
- <AnastasisClientFrame hideNav title="Add Security Question">
- <div>
- <p>
- For security question authentication, you need to provide a question
- and its answer. When recovering your secret, you will be shown the
- question and you will need to type the answer exactly as you typed it
- here.
- </p>
- <div>
- <LabeledInput
- label="Security question"
- grabFocus
- bind={[questionText, setQuestionText]} />
- </div>
- <div>
- <LabeledInput label="Answer" bind={[answerText, setAnswerText]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addQuestionAuth()}>Add</button>
- </div>
- </div>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx
deleted file mode 100644
index 6f4797275..000000000
--- a/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useState, useRef, useLayoutEffect } from "preact/hooks";
-import { AuthMethodSetupProps } from "./AuthenticationEditorScreen";
-import { AnastasisClientFrame } from "./index";
-
-export function AuthMethodSmsSetup(props: AuthMethodSetupProps): VNode {
- const [mobileNumber, setMobileNumber] = useState("");
- const addSmsAuth = (): void => {
- props.addAuthMethod({
- authentication_method: {
- type: "sms",
- instructions: `SMS to ${mobileNumber}`,
- challenge: encodeCrock(stringToBytes(mobileNumber)),
- },
- });
- };
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- inputRef.current?.focus();
- }, []);
- return (
- <AnastasisClientFrame hideNav title="Add SMS authentication">
- <div>
- <p>
- For SMS authentication, you need to provide a mobile number. When
- recovering your secret, you will be asked to enter the code you
- receive via SMS.
- </p>
- <label>
- Mobile number:{" "}
- <input
- value={mobileNumber}
- ref={inputRef}
- style={{ display: "block" }}
- autoFocus
- onChange={(e) => setMobileNumber((e.target as any).value)}
- type="text" />
- </label>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addSmsAuth()}>Add</button>
- </div>
- </div>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
index 44d3795b2..5077c3eb0 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -19,13 +20,17 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
export default {
- title: 'Pages/AuthenticationEditorScreen',
+ title: 'Pages/backup/AuthenticationEditorScreen',
component: TestedComponent,
+ args: {
+ order: 5,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
@@ -33,3 +38,56 @@ export default {
};
export const Example = createExample(TestedComponent, reducerStatesExample.authEditing);
+export const OneAuthMethodConfigured = createExample(TestedComponent, {
+ ...reducerStatesExample.authEditing,
+ authentication_methods: [{
+ type: 'question',
+ instructions: 'what time is it?',
+ challenge: 'asd',
+ }]
+} as ReducerState);
+
+
+export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, {
+ ...reducerStatesExample.authEditing,
+ authentication_methods: [{
+ type: 'question',
+ instructions: 'what time is it?',
+ challenge: 'asd',
+ },{
+ type: 'question',
+ instructions: 'what time is it?',
+ challenge: 'qwe',
+ },{
+ type: 'sms',
+ instructions: 'what time is it?',
+ challenge: 'asd',
+ },{
+ type: 'email',
+ instructions: 'what time is it?',
+ challenge: 'asd',
+ },{
+ type: 'email',
+ instructions: 'what time is it?',
+ challenge: 'asd',
+ },{
+ type: 'email',
+ instructions: 'what time is it?',
+ challenge: 'asd',
+ },{
+ type: 'email',
+ instructions: 'what time is it?',
+ challenge: 'asd',
+ }]
+} as ReducerState);
+
+export const NoAuthMethodProvided = createExample(TestedComponent, {
+ ...reducerStatesExample.authEditing,
+ authentication_providers: {},
+ authentication_methods: []
+} as ReducerState);
+
+ // type: string;
+ // instructions: string;
+ // challenge: string;
+ // mime_type?: string;
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index e9ffccbac..93ca81194 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -1,19 +1,21 @@
/* eslint-disable @typescript-eslint/camelcase */
-import { AuthMethod, ReducerStateBackup } from "anastasis-core";
-import { h, VNode } from "preact";
+import { AuthMethod } from "anastasis-core";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
-import { AuthMethodEmailSetup } from "./AuthMethodEmailSetup";
-import { AuthMethodPostSetup } from "./AuthMethodPostSetup";
-import { AuthMethodQuestionSetup } from "./AuthMethodQuestionSetup";
-import { AuthMethodSmsSetup } from "./AuthMethodSmsSetup";
+import { authMethods, KnownAuthMethods } from "./authMethod";
import { AnastasisClientFrame } from "./index";
+
+
+const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
+
export function AuthenticationEditorScreen(): VNode {
- const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
- undefined
- );
+ const [noProvidersAck, setNoProvidersAck] = useState(false)
+ const [selectedMethod, setSelectedMethod] = useState<KnownAuthMethods | undefined>(undefined);
+ const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
+
const reducer = useAnastasisContext()
if (!reducer) {
return <div>no reducer in context</div>
@@ -21,7 +23,29 @@ export function AuthenticationEditorScreen(): VNode {
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
}
+ const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? [];
+ const haveMethodsConfigured = configuredAuthMethods.length > 0;
+
+ function removeByIndex(index: number): void {
+ if (reducer) reducer.transition("delete_authentication", {
+ authentication_method: index,
+ })
+ }
+
+ const camByType: { [s: string]: AuthMethodWithRemove[] } = {}
+ for (let index = 0; index < configuredAuthMethods.length; index++) {
+ const cam = {
+ ...configuredAuthMethods[index],
+ remove: () => removeByIndex(index)
+ }
+ const prevValue = camByType[cam.type] || []
+ prevValue.push(cam)
+ camByType[cam.type] = prevValue;
+ }
+
+
const providers = reducer.currentReducerState.authentication_providers!;
+
const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) {
const p = providers[provKey];
@@ -31,79 +55,125 @@ export function AuthenticationEditorScreen(): VNode {
}
}
}
+
if (selectedMethod) {
const cancel = (): void => setSelectedMethod(undefined);
const addMethod = (args: any): void => {
reducer.transition("add_authentication", args);
setSelectedMethod(undefined);
};
- const methodMap: Record<
- string, (props: AuthMethodSetupProps) => h.JSX.Element
- > = {
- sms: AuthMethodSmsSetup,
- question: AuthMethodQuestionSetup,
- email: AuthMethodEmailSetup,
- post: AuthMethodPostSetup,
- };
- const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
- return (
+
+ const AuthSetup = authMethods[selectedMethod].screen ?? AuthMethodNotImplemented;
+ return (<Fragment>
<AuthSetup
cancel={cancel}
+ configured={camByType[selectedMethod] || []}
addAuthMethod={addMethod}
method={selectedMethod} />
+
+ {!authAvailableSet.has(selectedMethod) && <ConfirmModal active
+ onCancel={cancel} description="No providers founds" label="Add a provider manually"
+ onConfirm={() => {
+ null
+ }}
+ >
+ We have found no trusted cloud providers for your recovery secret. You can add a provider manually.
+ To add a provider you must know the provider URL (e.g. https://provider.com)
+ <p>
+ <a>More about cloud providers</a>
+ </p>
+ </ConfirmModal>}
+
+ </Fragment>
);
}
- function MethodButton(props: { method: string; label: string }): VNode {
+
+ if (addingProvider !== undefined) {
+ return <div />
+ }
+
+ function MethodButton(props: { method: KnownAuthMethods }): VNode {
+ if (authMethods[props.method].skip) return <div />
+
return (
- <button
- disabled={!authAvailableSet.has(props.method)}
- onClick={() => {
- setSelectedMethod(props.method);
- if (reducer) reducer.dismissError();
- }}
- >
- {props.label}
- </button>
+ <div class="block">
+ <button
+ style={{ justifyContent: 'space-between' }}
+ class="button is-fullwidth"
+ onClick={() => {
+ setSelectedMethod(props.method);
+ }}
+ >
+ <div style={{ display: 'flex' }}>
+ <span class="icon ">
+ {authMethods[props.method].icon}
+ </span>
+ {authAvailableSet.has(props.method) ?
+ <span>
+ Add a {authMethods[props.method].label} challenge
+ </span> :
+ <span>
+ Add a {authMethods[props.method].label} provider
+ </span>
+ }
+ </div>
+ {!authAvailableSet.has(props.method) &&
+ <span class="icon has-text-danger" >
+ <i class="mdi mdi-exclamation-thick" />
+ </span>
+ }
+ {camByType[props.method] &&
+ <span class="tag is-info" >
+ {camByType[props.method].length}
+ </span>
+ }
+ </button>
+ </div>
);
}
- const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? [];
- const haveMethodsConfigured = configuredAuthMethods.length;
+ const errors = !haveMethodsConfigured ? "There is not enough authentication methods." : undefined;
return (
- <AnastasisClientFrame title="Backup: Configure Authentication Methods">
- <div>
- <MethodButton method="sms" label="SMS" />
- <MethodButton method="email" label="Email" />
- <MethodButton method="question" label="Question" />
- <MethodButton method="post" label="Physical Mail" />
- <MethodButton method="totp" label="TOTP" />
- <MethodButton method="iban" label="IBAN" />
- </div>
- <h2>Configured authentication methods</h2>
- {haveMethodsConfigured ? (
- configuredAuthMethods.map((x, i) => {
- return (
- <p key={i}>
- {x.type} ({x.instructions}){" "}
- <button
- onClick={() => reducer.transition("delete_authentication", {
- authentication_method: i,
- })}
- >
- Delete
- </button>
+ <AnastasisClientFrame title="Backup: Configure Authentication Methods" hideNext={errors}>
+ <div class="columns">
+ <div class="column is-half">
+ <div>
+ {getKeys(authMethods).map(method => <MethodButton key={method} method={method} />)}
+ </div>
+ {authAvailableSet.size === 0 && <ConfirmModal active={!noProvidersAck}
+ onCancel={() => setNoProvidersAck(true)} description="No providers founds" label="Add a provider manually"
+ onConfirm={() => {
+ null
+ }}
+ >
+ We have found no trusted cloud providers for your recovery secret. You can add a provider manually.
+ To add a provider you must know the provider URL (e.g. https://provider.com)
+ <p>
+ <a>More about cloud providers</a>
</p>
- );
- })
- ) : (
- <p>No authentication methods configured yet.</p>
- )}
+ </ConfirmModal>}
+ </div>
+ <div class="column is-half">
+ <p class="block">
+ When recovering your wallet, you will be asked to verify your identity via the methods you configure here.
+ The list of authentication method is defined by the backup provider list.
+ </p>
+ <p class="block">
+ <button class="button is-info">Manage the backup provider's list</button>
+ </p>
+ {authAvailableSet.size > 0 && <p class="block">
+ We couldn't find provider for some of the authentication methods.
+ </p>}
+ </div>
+ </div>
</AnastasisClientFrame>
);
}
+type AuthMethodWithRemove = AuthMethod & { remove: () => void }
export interface AuthMethodSetupProps {
method: string;
addAuthMethod: (x: any) => void;
+ configured: AuthMethodWithRemove[];
cancel: () => void;
}
@@ -116,8 +186,36 @@ function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
);
}
-interface AuthenticationEditorProps {
- reducer: AnastasisReducerApi;
- backupState: ReducerStateBackup;
+
+function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode {
+ return <div class={active ? "modal is-active" : "modal"}>
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card" style={{ maxWidth: 700 }}>
+ <header class="modal-card-head">
+ {!description ? null : <p class="modal-card-title"><b>{description}</b></p>}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">
+ {children}
+ </section>
+ <footer class="modal-card-foot">
+ <button class="button" onClick={onCancel} >Dismiss</button>
+ <div class="buttons is-right" style={{ width: '100%' }}>
+ <button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} >{label}</button>
+ </div>
+ </footer>
+ </div>
+ <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
+ </div>
}
+interface Props {
+ active?: boolean;
+ description?: string;
+ onCancel?: () => void;
+ onConfirm?: () => void;
+ label?: string;
+ children?: ComponentChildren;
+ danger?: boolean;
+ disabled?: boolean;
+}
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
index 65a2b7e13..b71a79727 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
@@ -26,15 +26,18 @@ import { BackupFinishedScreen as TestedComponent } from './BackupFinishedScreen'
export default {
- title: 'Pages/BackupFinishedScreen',
+ title: 'Pages/backup/FinishedScreen',
component: TestedComponent,
+ args: {
+ order: 9,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
},
};
-export const Simple = createExample(TestedComponent, reducerStatesExample.backupFinished);
+export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished);
export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
secret_name: 'super_secret',
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
index 218f1d1fd..7938baca4 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
@@ -1,3 +1,4 @@
+import { format } from "date-fns";
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
@@ -11,23 +12,33 @@ export function BackupFinishedScreen(): VNode {
return <div>invalid state</div>
}
const details = reducer.currentReducerState.success_details
- return (<AnastasisClientFrame hideNext title="Backup finished">
- <p>
- Your backup of secret "{reducer.currentReducerState.secret_name ?? "??"}" was
+
+ return (<AnastasisClientFrame hideNav title="Backup finished">
+ {reducer.currentReducerState.secret_name ? <p>
+ Your backup of secret <b>"{reducer.currentReducerState.secret_name}"</b> was
successful.
- </p>
- <p>The backup is stored by the following providers:</p>
+ </p> :
+ <p>
+ Your secret was successfully backed up.
+ </p>}
- {details && <ul>
+ {details && <div class="block">
+ <p>The backup is stored by the following providers:</p>
{Object.keys(details).map((x, i) => {
const sd = details[x];
return (
- <li key={i}>
- {x} (Policy version {sd.policy_version})
- </li>
+ <div key={i} class="box">
+ {x}
+ <p>
+ version {sd.policy_version}
+ {sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'}
+ </p>
+ </div>
);
})}
- </ul>}
- <button onClick={() => reducer.reset()}>Back to start</button>
+ </div>}
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={() => reducer.back()}>Back</button>
+ </div>
</AnastasisClientFrame>);
}
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
index 4f186c031..48115c798 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
@@ -16,68 +16,201 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { ChallengeOverviewScreen as TestedComponent } from './ChallengeOverviewScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { RecoveryStates, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen";
export default {
- title: 'Pages/ChallengeOverviewScreen',
+ title: "Pages/recovery/ChallengeOverviewScreen",
component: TestedComponent,
+ args: {
+ order: 5,
+ },
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const OneChallenge = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting,
+export const OneUnsolvedPolicy = createExample(TestedComponent, {
+ ...reducerStatesExample.challengeSelecting,
+ recovery_information: {
+ policies: [[{ uuid: "1" }]],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "just go for it",
+ type: "question",
+ uuid: "1",
+ },
+ ],
+ },
+} as ReducerState);
+
+export const SomePoliciesOneSolved = createExample(TestedComponent, {
+ ...reducerStatesExample.challengeSelecting,
recovery_information: {
- policies: [[{uuid:'1'}]],
- challenges: [{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '1',
- }]
+ policies: [[{ uuid: "1" }, { uuid: "2" }], [{ uuid: "uuid-3" }]],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "this question cost 1 USD",
+ type: "question",
+ uuid: "1",
+ },
+ {
+ cost: "USD:0",
+ instructions: "answering this question is free",
+ type: "question",
+ uuid: "2",
+ },
+ {
+ cost: "USD:1",
+ instructions: "this question is already answered",
+ type: "question",
+ uuid: "uuid-3",
+ },
+ ],
+ },
+ challenge_feedback: {
+ "uuid-3": {
+ state: "solved",
+ },
},
} as ReducerState);
-export const MoreChallenges = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting,
+export const OneBadConfiguredPolicy = createExample(TestedComponent, {
+ ...reducerStatesExample.challengeSelecting,
recovery_information: {
- policies: [[{uuid:'1'}, {uuid:'2'}],[{uuid:'3'}]],
- challenges: [{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '1',
- },{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '2',
- },{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'question',
- uuid: '3',
- }]
+ policies: [[{ uuid: "1" }, { uuid: "2" }]],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "this policy has a missing uuid (the other auth method)",
+ type: "totp",
+ uuid: "1",
+ },
+ ],
},
} as ReducerState);
-export const OneBadConfiguredPolicy = createExample(TestedComponent, {...reducerStatesExample.challengeSelecting,
+export const OnePolicyWithAllTheChallenges = createExample(TestedComponent, {
+ ...reducerStatesExample.challengeSelecting,
recovery_information: {
- policies: [[{uuid:'2'}]],
- challenges: [{
- cost: 'USD:1',
- instructions: 'just go for it',
- type: 'sasd',
- uuid: '1',
- }]
+ policies: [
+ [
+ { uuid: "1" },
+ { uuid: "2" },
+ { uuid: "3" },
+ { uuid: "4" },
+ { uuid: "5" },
+ { uuid: "6" },
+ { uuid: "7" },
+ { uuid: "8" },
+ ],
+ ],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "Does P equals NP?",
+ type: "question",
+ uuid: "1",
+ },
+ {
+ cost: "USD:1",
+ instructions: "SMS to 555-555",
+ type: "sms",
+ uuid: "2",
+ },
+ {
+ cost: "USD:1",
+ instructions: "Email to qwe@asd.com",
+ type: "email",
+ uuid: "3",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'Enter 8 digits code for "Anastasis"',
+ type: "totp",
+ uuid: "4",
+ },
+ {
+ //
+ cost: "USD:0",
+ instructions: "Wire transfer from ASDXCVQWE123123 with holder Florian",
+ type: "iban",
+ uuid: "5",
+ },
+ {
+ cost: "USD:1",
+ instructions: "Join a video call",
+ type: "video", //Enter 8 digits code for "Anastasis"
+ uuid: "7",
+ },
+ {},
+ {
+ cost: "USD:1",
+ instructions: "Letter to address in postal code DE123123",
+ type: "post", //Enter 8 digits code for "Anastasis"
+ uuid: "8",
+ },
+ {
+ cost: "USD:1",
+ instructions: "instruction for an unknown type of challenge",
+ type: "new-type-of-challenge",
+ uuid: "6",
+ },
+ ],
},
} as ReducerState);
-export const NoPolicies = createExample(TestedComponent, reducerStatesExample.challengeSelecting);
+export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.challengeSelecting,
+ recovery_state: RecoveryStates.ChallengeSelecting,
+ recovery_information: {
+ policies: [
+ [
+ { uuid: "1" },
+ { uuid: "2" },
+ { uuid: "3" },
+ { uuid: "4" },
+ { uuid: "5" },
+ { uuid: "6" },
+ { uuid: "7" },
+ { uuid: "8" },
+ { uuid: "9" },
+ { uuid: "10" },
+ ],
+ ],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: 'in state "solved"',
+ type: "question",
+ uuid: "1",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "message"',
+ type: "question",
+ uuid: "2",
+ },
+ ],
+ },
+ challenge_feedback: {
+ 1: { state: "solved" },
+ 2: { state: "message", message: "Security question was not solved correctly" },
+ // FIXME: add missing feedback states here!
+ },
+ } as ReducerState,
+);
+export const NoPolicies = createExample(
+ TestedComponent,
+ reducerStatesExample.challengeSelecting,
+);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
index c9b52e91b..ed34bbde2 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
@@ -1,77 +1,184 @@
+import { ChallengeFeedback, ChallengeFeedbackStatus } from "anastasis-core";
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
+import { authMethods, KnownAuthMethods } from "./authMethod";
+
+function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
+ const { feedback } = props;
+ if (!feedback) {
+ return null;
+ }
+ switch (feedback.state) {
+ case ChallengeFeedbackStatus.Message:
+ return (
+ <div>
+ <p>{feedback.message}</p>
+ </div>
+ );
+ case ChallengeFeedbackStatus.Pending:
+ case ChallengeFeedbackStatus.AuthIban:
+ return null;
+ case ChallengeFeedbackStatus.RateLimitExceeded:
+ return <div>Rate limit exceeded.</div>;
+ case ChallengeFeedbackStatus.Redirect:
+ return <div>Redirect (FIXME: not supported)</div>;
+ case ChallengeFeedbackStatus.Unsupported:
+ return <div>Challenge not supported by client.</div>;
+ case ChallengeFeedbackStatus.TruthUnknown:
+ return <div>Truth unknown</div>;
+ default:
+ return (
+ <div>
+ <pre>{JSON.stringify(feedback)}</pre>
+ </div>
+ );
+ }
+}
export function ChallengeOverviewScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const policies = reducer.currentReducerState.recovery_information?.policies ?? [];
- const chArr = reducer.currentReducerState.recovery_information?.challenges ?? [];
- const challengeFeedback = reducer.currentReducerState?.challenge_feedback;
+ const policies =
+ reducer.currentReducerState.recovery_information?.policies ?? [];
+ const knownChallengesArray =
+ reducer.currentReducerState.recovery_information?.challenges ?? [];
+ const challengeFeedback =
+ reducer.currentReducerState?.challenge_feedback ?? {};
- const challenges: {
+ const knownChallengesMap: {
[uuid: string]: {
type: string;
instructions: string;
cost: string;
+ feedback: ChallengeFeedback | undefined;
};
} = {};
- for (const ch of chArr) {
- challenges[ch.uuid] = {
+ for (const ch of knownChallengesArray) {
+ knownChallengesMap[ch.uuid] = {
type: ch.type,
cost: ch.cost,
instructions: ch.instructions,
+ feedback: challengeFeedback[ch.uuid],
};
}
+ const policiesWithInfo = policies.map((row) => {
+ let isPolicySolved = true;
+ const challenges = row
+ .map(({ uuid }) => {
+ const info = knownChallengesMap[uuid];
+ const isChallengeSolved = info?.feedback?.state === "solved";
+ isPolicySolved = isPolicySolved && isChallengeSolved;
+ return { info, uuid, isChallengeSolved };
+ })
+ .filter((ch) => ch.info !== undefined);
+
+ return { isPolicySolved, challenges };
+ });
+
+ const atLeastThereIsOnePolicySolved =
+ policiesWithInfo.find((p) => p.isPolicySolved) !== undefined;
+
+ const errors = !atLeastThereIsOnePolicySolved
+ ? "Solve one policy before proceeding"
+ : undefined;
return (
- <AnastasisClientFrame title="Recovery: Solve challenges">
- <h2>Policies</h2>
- {!policies.length && <p>
- No policies found
- </p>}
- {policies.map((row, i) => {
- return (
- <div key={i}>
- <h3>Policy #{i + 1}</h3>
- {row.map(column => {
- const ch = challenges[column.uuid];
- if (!ch) return <div>
- There is no challenge for this policy
- </div>
- const feedback = challengeFeedback?.[column.uuid];
- return (
- <div key={column.uuid}
- style={{
- borderLeft: "2px solid gray",
- paddingLeft: "0.5em",
- borderRadius: "0.5em",
- marginTop: "0.5em",
- marginBottom: "0.5em",
- }}
- >
- <h4>
- {ch.type} ({ch.instructions})
- </h4>
- <p>Status: {feedback?.state ?? "unknown"}</p>
- {feedback?.state !== "solved" ? (
- <button
- onClick={() => reducer.transition("select_challenge", {
- uuid: column.uuid,
- })}
- >
- Solve
- </button>
- ) : null}
+ <AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
+ {!policies.length ? (
+ <p class="block">
+ No policies found, try with another version of the secret
+ </p>
+ ) : policies.length === 1 ? (
+ <p class="block">
+ One policy found for this secret. You need to solve all the challenges
+ in order to recover your secret.
+ </p>
+ ) : (
+ <p class="block">
+ We have found {policies.length} polices. You need to solve all the
+ challenges from one policy in order to recover your secret.
+ </p>
+ )}
+ {policiesWithInfo.map((policy, policy_index) => {
+ const tableBody = policy.challenges.map(({ info, uuid }) => {
+ const isFree = !info.cost || info.cost.endsWith(":0");
+ const method = authMethods[info.type as KnownAuthMethods];
+ return (
+ <div
+ key={uuid}
+ class="block"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ }}
+ >
+ <div style={{ display: "flex", alignItems: "center" }}>
+ <span class="icon">{method?.icon}</span>
+ <span>{info.instructions}</span>
</div>
- );
- })}
+ <OverviewFeedbackDisplay feedback={info.feedback} />
+ </div>
+ <div>
+ {method && info.feedback?.state !== "solved" ? (
+ <a
+ class="button"
+ onClick={() =>
+ reducer.transition("select_challenge", { uuid })
+ }
+ >
+ {isFree ? "Solve" : `Pay and Solve`}
+ </a>
+ ) : null}
+ {info.feedback?.state === "solved" ? (
+ <a class="button is-success"> Solved </a>
+ ) : null}
+ </div>
+ </div>
+ );
+ });
+
+ const policyName = policy.challenges
+ .map((x) => x.info.type)
+ .join(" + ");
+ const opa = !atLeastThereIsOnePolicySolved
+ ? undefined
+ : policy.isPolicySolved
+ ? undefined
+ : "0.6";
+ return (
+ <div
+ key={policy_index}
+ class="box"
+ style={{
+ opacity: opa,
+ }}
+ >
+ <h3 class="subtitle">
+ Policy #{policy_index + 1}: {policyName}
+ </h3>
+ {policy.challenges.length === 0 && (
+ <p>This policy doesn't have challenges.</p>
+ )}
+ {policy.challenges.length === 1 && (
+ <p>This policy just have one challenge.</p>
+ )}
+ {policy.challenges.length > 1 && (
+ <p>This policy have {policy.challenges.length} challenges.</p>
+ )}
+ {tableBody}
</div>
);
})}
diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
index adf36980f..e5fe09e99 100644
--- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
@@ -20,17 +20,19 @@
*/
import { createExample, reducerStatesExample } from '../../utils';
-import { CountrySelectionScreen as TestedComponent } from './CountrySelectionScreen';
+import { ChallengePayingScreen as TestedComponent } from './ChallengePayingScreen';
export default {
- title: 'Pages/CountrySelectionScreen',
+ title: 'Pages/recovery/__ChallengePayingScreen',
component: TestedComponent,
+ args: {
+ order: 10,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
},
};
-export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry);
-export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry);
+export const Example = createExample(TestedComponent, reducerStatesExample.challengePaying);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
new file mode 100644
index 000000000..84896a2ec
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
@@ -0,0 +1,33 @@
+import { h, VNode } from "preact";
+import { useAnastasisContext } from "../../context/anastasis";
+import { AnastasisClientFrame } from "./index";
+
+export function ChallengePayingScreen(): VNode {
+ const reducer = useAnastasisContext()
+ if (!reducer) {
+ return <div>no reducer in context</div>
+ }
+ if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
+ return <div>invalid state</div>
+ }
+ const payments = ['']; //reducer.currentReducerState.payments ??
+ return (
+ <AnastasisClientFrame
+ hideNav
+ title="Recovery: Challenge Paying"
+ >
+ <p>
+ Some of the providers require a payment to store the encrypted
+ authentication information.
+ </p>
+ <ul>
+ {payments.map((x, i) => {
+ return <li key={i}>{x}</li>;
+ })}
+ </ul>
+ <button onClick={() => reducer.transition("pay", {})}>
+ Check payment status now
+ </button>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
index aad37cd7f..6bdb3515d 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -19,18 +20,33 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
export default {
- title: 'Pages/ContinentSelectionScreen',
+ title: 'Pages/Location',
component: TestedComponent,
+ args: {
+ order: 2,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
},
};
-export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry);
-export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry);
+export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
+
+export const BackupSelectCountry = createExample(TestedComponent, {
+ ...reducerStatesExample.backupSelectContinent,
+ selected_continent: 'Testcontinent',
+} as ReducerState);
+
+export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);
+
+export const RecoverySelectCountry = createExample(TestedComponent, {
+ ...reducerStatesExample.recoverySelectContinent,
+ selected_continent: 'Testcontinent',
+} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
index ad529a4a7..0e43f982d 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
@@ -1,20 +1,104 @@
+/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame, withProcessLabel } from "./index";
export function ContinentSelectionScreen(): VNode {
const reducer = useAnastasisContext()
+
+ //FIXME: remove this when #7056 is fixed
+ const countryFromReducer = (reducer?.currentReducerState as any).selected_country || ""
+ const [countryCode, setCountryCode] = useState( countryFromReducer )
+
if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) {
return <div />
}
- const sel = (x: string): void => reducer.transition("select_continent", { continent: x });
+ const selectContinent = (continent: string): void => {
+ reducer.transition("select_continent", { continent })
+ };
+ const selectCountry = (country: string): void => {
+ setCountryCode(country)
+ };
+
+
+ const continentList = reducer.currentReducerState.continents || [];
+ const countryList = reducer.currentReducerState.countries || [];
+ const theContinent = reducer.currentReducerState.selected_continent || ""
+ // const cc = reducer.currentReducerState.selected_country || "";
+ const theCountry = countryList.find(c => c.code === countryCode)
+ const selectCountryAction = () => {
+ //selection should be when the select box changes it value
+ if (!theCountry) return;
+ reducer.transition("select_country", {
+ country_code: countryCode,
+ currencies: [theCountry.currency],
+ })
+ }
+
+ // const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
+ // reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
+
+ const errors = !theCountry ? "Select a country" : undefined
+
return (
- <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Continent")}>
- {reducer.currentReducerState.continents.map((x: any) => (
- <button onClick={() => sel(x.name)} key={x.name}>
- {x.name}
- </button>
- ))}
+ <AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Where do you live?")} onNext={selectCountryAction}>
+
+ <div class="columns" >
+ <div class="column is-one-third">
+ <div class="field">
+ <label class="label">Continent</label>
+ <div class="control is-expanded has-icons-left">
+ <div class="select is-fullwidth" >
+ <select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} >
+ <option key="none" disabled selected value=""> Choose a continent </option>
+ {continentList.map(prov => (
+ <option key={prov.name} value={prov.name}>
+ {prov.name}
+ </option>
+ ))}
+ </select>
+ <div class="icon is-small is-left">
+ <i class="mdi mdi-earth" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="field">
+ <label class="label">Country</label>
+ <div class="control is-expanded has-icons-left">
+ <div class="select is-fullwidth" >
+ <select onChange={(e) => selectCountry((e.target as any).value)} disabled={!theContinent} value={theCountry?.code || ""}>
+ <option key="none" disabled selected value=""> Choose a country </option>
+ {countryList.map(prov => (
+ <option key={prov.name} value={prov.code}>
+ {prov.name}
+ </option>
+ ))}
+ </select>
+ <div class="icon is-small is-left">
+ <i class="mdi mdi-earth" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* {theCountry && <div class="field">
+ <label class="label">Available currencies:</label>
+ <div class="control">
+ <input class="input is-small" type="text" readonly value={theCountry.currency} />
+ </div>
+ </div>} */}
+ </div>
+ <div class="column is-two-third">
+ <p>
+ Your location will help us to determine which personal information
+ ask you for the next step.
+ </p>
+ </div>
+ </div>
+
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx
deleted file mode 100644
index 555622c1d..000000000
--- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import { h, VNode } from "preact";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, withProcessLabel } from "./index";
-
-export function CountrySelectionScreen(): VNode {
- const reducer = useAnastasisContext()
- if (!reducer) {
- return <div>no reducer in context</div>
- }
- if (!reducer.currentReducerState || !("countries" in reducer.currentReducerState)) {
- return <div>invalid state</div>
- }
- const sel = (x: any): void => reducer.transition("select_country", {
- country_code: x.code,
- currencies: [x.currency],
- });
- return (
- <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Country")} >
- {reducer.currentReducerState.countries.map((x: any) => (
- <button onClick={() => sel(x)} key={x.name}>
- {x.name} ({x.currency})
- </button>
- ))}
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
new file mode 100644
index 000000000..fc339e48e
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
@@ -0,0 +1,109 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { ReducerState } from 'anastasis-core';
+import { createExample, reducerStatesExample } from '../../utils';
+import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen';
+
+
+export default {
+ title: 'Pages/backup/ReviewPoliciesScreen/EditPoliciesScreen',
+ args: {
+ order: 6,
+ },
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+export const EditingAPolicy = createExample(TestedComponent, {
+ ...reducerStatesExample.policyReview,
+ policies: [{
+ methods: [{
+ authentication_method: 1,
+ provider: 'https://anastasis.demo.taler.net/'
+ }, {
+ authentication_method: 2,
+ provider: 'http://localhost:8086/'
+ }]
+ }, {
+ methods: [{
+ authentication_method: 1,
+ provider: 'http://localhost:8086/'
+ }]
+ }],
+ authentication_methods: [{
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA"
+ }, {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA"
+ }, {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: ""
+ }, {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8"
+ }]
+} as ReducerState, { index : 0});
+
+export const CreatingAPolicy = createExample(TestedComponent, {
+ ...reducerStatesExample.policyReview,
+ policies: [{
+ methods: [{
+ authentication_method: 1,
+ provider: 'https://anastasis.demo.taler.net/'
+ }, {
+ authentication_method: 2,
+ provider: 'http://localhost:8086/'
+ }]
+ }, {
+ methods: [{
+ authentication_method: 1,
+ provider: 'http://localhost:8086/'
+ }]
+ }],
+ authentication_methods: [{
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA"
+ }, {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA"
+ }, {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: ""
+ }, {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8"
+ }]
+} as ReducerState, { index : 3});
+
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
new file mode 100644
index 000000000..85cc96c46
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
@@ -0,0 +1,133 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import { AuthMethod, Policy } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useAnastasisContext } from "../../context/anastasis";
+import { authMethods, KnownAuthMethods } from "./authMethod";
+import { AnastasisClientFrame } from "./index";
+
+export interface ProviderInfo {
+ url: string;
+ cost: string;
+ isFree: boolean;
+}
+
+export type ProviderInfoByType = {
+ [type in KnownAuthMethods]?: ProviderInfo[];
+};
+
+interface Props {
+ index: number;
+ cancel: () => void;
+ confirm: (changes: MethodProvider[]) => void;
+
+}
+
+export interface MethodProvider {
+ authentication_method: number;
+ provider: string;
+}
+
+export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode {
+ const [changedProvider, setChangedProvider] = useState<Array<string>>([])
+
+ const reducer = useAnastasisContext()
+ if (!reducer) {
+ return <div>no reducer in context</div>
+ }
+ if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
+ return <div>invalid state</div>
+ }
+
+ const selectableProviders: ProviderInfoByType = {}
+ const allProviders = Object.entries(reducer.currentReducerState.authentication_providers || {})
+ for (let index = 0; index < allProviders.length; index++) {
+ const [url, status] = allProviders[index]
+ if ("methods" in status) {
+ status.methods.map(m => {
+ const type: KnownAuthMethods = m.type as KnownAuthMethods
+ const values = selectableProviders[type] || []
+ const isFree = !m.usage_fee || m.usage_fee.endsWith(":0")
+ values.push({ url, cost: m.usage_fee, isFree })
+ selectableProviders[type] = values
+ })
+ }
+ }
+
+ const allAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
+ const policies = reducer.currentReducerState.policies ?? [];
+ const policy = policies[policy_index]
+
+ for(let method_index = 0; method_index < allAuthMethods.length; method_index++ ) {
+ policy?.methods.find(m => m.authentication_method === method_index)?.provider
+ }
+
+ function sendChanges(): void {
+ const newMethods: MethodProvider[] = []
+ allAuthMethods.forEach((method, index) => {
+ const oldValue = policy?.methods.find(m => m.authentication_method === index)
+ if (changedProvider[index] === undefined && oldValue !== undefined) {
+ newMethods.push(oldValue)
+ }
+ if (changedProvider[index] !== undefined && changedProvider[index] !== "") {
+ newMethods.push({
+ authentication_method: index,
+ provider: changedProvider[index]
+ })
+ }
+ })
+ confirm(newMethods)
+ }
+
+ return <AnastasisClientFrame hideNav title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}>
+ <section class="section">
+ {!policy ? <p>
+ Creating a new policy #{policy_index}
+ </p> : <p>
+ Editing policy #{policy_index}
+ </p>}
+ {allAuthMethods.map((method, index) => {
+ //take the url from the updated change or from the policy
+ const providerURL = changedProvider[index] === undefined ?
+ policy?.methods.find(m => m.authentication_method === index)?.provider :
+ changedProvider[index];
+
+ const type: KnownAuthMethods = method.type as KnownAuthMethods
+ function changeProviderTo(url: string): void {
+ const copy = [...changedProvider]
+ copy[index] = url
+ setChangedProvider(copy)
+ }
+ return (
+ <div key={index} class="block" style={{ display: 'flex', alignItems: 'center' }}>
+ <span class="icon">
+ {authMethods[type]?.icon}
+ </span>
+ <span>
+ {method.instructions}
+ </span>
+ <span>
+ <span class="select " >
+ <select onChange={(e) => changeProviderTo(e.currentTarget.value)} value={providerURL ?? ""}>
+ <option key="none" value=""> &lt;&lt; off &gt;&gt; </option>
+ {selectableProviders[type]?.map(prov => (
+ <option key={prov.url} value={prov.url}>
+ {prov.url}
+ </option>
+ ))}
+ </select>
+ </span>
+ </span>
+ </div>
+ );
+ })}
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span class="buttons">
+ <button class="button" onClick={() => setChangedProvider([])}>Reset</button>
+ <button class="button is-info" onClick={sendChanges}>Confirm</button>
+ </span>
+ </div>
+ </section>
+ </AnastasisClientFrame>
+}
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
index 1a9462b88..e952ab28d 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
@@ -26,8 +26,11 @@ import { PoliciesPayingScreen as TestedComponent } from './PoliciesPayingScreen'
export default {
- title: 'Pages/PoliciesPayingScreen',
+ title: 'Pages/backup/PoliciesPayingScreen',
component: TestedComponent,
+ args: {
+ order: 8,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
index 8a39cf0e4..a470f5155 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
@@ -13,7 +13,7 @@ export function PoliciesPayingScreen(): VNode {
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
return (
- <AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
+ <AnastasisClientFrame hideNav title="Backup: Recovery Document Payments">
<p>
Some of the providers require a payment to store the encrypted
recovery document.
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
index 0c1842420..0d2ebb778 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
@@ -26,7 +26,10 @@ import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScr
export default {
- title: 'Pages/RecoveryFinishedScreen',
+ title: 'Pages/recovery/FinishedScreen',
+ args: {
+ order: 7,
+ },
component: TestedComponent,
argTypes: {
onUpdate: { action: 'onUpdate' },
@@ -34,7 +37,7 @@ export default {
},
};
-export const NormalEnding = createExample(TestedComponent, {
+export const GoodEnding = createExample(TestedComponent, {
...reducerStatesExample.recoveryFinished,
core_secret: { mime: 'text/plain', value: 'hello' }
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
index 8c8a2c7c8..a61ef9efa 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -15,20 +15,26 @@ export function RecoveryFinishedScreen(): VNode {
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
return <div>invalid state</div>
}
- const encodedSecret = reducer.currentReducerState.core_secret?.value
+ const encodedSecret = reducer.currentReducerState.core_secret
if (!encodedSecret) {
- return <AnastasisClientFrame title="Recovery Problem" hideNext>
+ return <AnastasisClientFrame title="Recovery Problem" hideNav>
<p>
Secret not found
</p>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={() => reducer.back()}>Back</button>
+ </div>
</AnastasisClientFrame>
}
- const secret = bytesToString(decodeCrock(encodedSecret))
+ const secret = bytesToString(decodeCrock(encodedSecret.value))
return (
- <AnastasisClientFrame title="Recovery Finished" hideNext>
+ <AnastasisClientFrame title="Recovery Finished" hideNav>
<p>
Secret: {secret}
</p>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={() => reducer.back()}>Back</button>
+ </div>
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
index b52699e7b..9f7e26c16 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
@@ -26,7 +26,10 @@ import { ReviewPoliciesScreen as TestedComponent } from './ReviewPoliciesScreen'
export default {
- title: 'Pages/ReviewPoliciesScreen',
+ title: 'Pages/backup/ReviewPoliciesScreen',
+ args: {
+ order: 6,
+ },
component: TestedComponent,
argTypes: {
onUpdate: { action: 'onUpdate' },
@@ -40,11 +43,11 @@ export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
methods: [{
authentication_method: 0,
provider: 'asd'
- },{
+ }, {
authentication_method: 1,
provider: 'asd'
}]
- },{
+ }, {
methods: [{
authentication_method: 1,
provider: 'asd'
@@ -55,27 +58,191 @@ export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
export const SomePoliciesWithMethods = createExample(TestedComponent, {
...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 0,
- provider: 'asd'
- },{
- authentication_method: 1,
- provider: 'asd'
- }]
- },{
- methods: [{
- authentication_method: 1,
- provider: 'asd'
- }]
- }],
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/"
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/"
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ },
+ {
+ methods: [
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/"
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/"
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/"
+ }
+ ]
+ }
+ ],
authentication_methods: [{
- challenge: 'asd',
- instructions: 'ins',
- type: 'type',
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA"
+ }, {
+ type: "sms",
+ instructions: "SMS to 555-555",
+ challenge: ""
+ }, {
+ type: "question",
+ instructions: "Does P equal NP?",
+ challenge: "C5SP8"
},{
- challenge: 'asd2',
- instructions: 'ins2',
- type: 'type2',
- }]
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA"
+ }, {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: ""
+ }, {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8"
+}]
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
index b360ccaf0..f93963f67 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
@@ -1,9 +1,13 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
+import { authMethods, KnownAuthMethods } from "./authMethod";
+import { EditPoliciesScreen } from "./EditPoliciesScreen";
import { AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(): VNode {
+ const [editingPolicy, setEditingPolicy] = useState<number | undefined>()
const reducer = useAnastasisContext()
if (!reducer) {
return <div>no reducer in context</div>
@@ -11,42 +15,72 @@ export function ReviewPoliciesScreen(): VNode {
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
}
- const authMethods = reducer.currentReducerState.authentication_methods ?? [];
+
+ const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
+ if (editingPolicy !== undefined) {
+ return (
+ <EditPoliciesScreen
+ index={editingPolicy}
+ cancel={() => setEditingPolicy(undefined)}
+ confirm={async (newMethods) => {
+ await reducer.transition("update_policy", {
+ policy_index: editingPolicy,
+ policy: newMethods,
+ });
+ setEditingPolicy(undefined)
+ }}
+ />
+ )
+ }
+
+ const errors = policies.length < 1 ? 'Need more policies' : undefined
return (
- <AnastasisClientFrame title="Backup: Review Recovery Policies">
+ <AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies">
+ {policies.length > 0 && <p class="block">
+ Based on your configured authentication method you have created, some policies
+ have been configured. In order to recover your secret you have to solve all the
+ challenges of at least one policy.
+ </p>}
+ {policies.length < 1 && <p class="block">
+ No policies had been created. Go back and add more authentication methods.
+ </p>}
+ <div class="block" style={{ justifyContent: 'flex-end' }} >
+ <button class="button is-success" onClick={() => setEditingPolicy(policies.length + 1)}>Add new policy</button>
+ </div>
{policies.map((p, policy_index) => {
const methods = p.methods
- .map(x => authMethods[x.authentication_method] && ({ ...authMethods[x.authentication_method], provider: x.provider }))
+ .map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider }))
.filter(x => !!x)
const policyName = methods.map(x => x.type).join(" + ");
return (
- <div key={policy_index} class="policy">
- <h3>
- Policy #{policy_index + 1}: {policyName}
- </h3>
- Required Authentications:
- {!methods.length && <p>
- No auth method found
- </p>}
- <ul>
+ <div key={policy_index} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <div>
+ <h3 class="subtitle">
+ Policy #{policy_index + 1}: {policyName}
+ </h3>
+ {!methods.length && <p>
+ No auth method found
+ </p>}
{methods.map((m, i) => {
return (
- <li key={i}>
- {m.type} ({m.instructions}) at provider {m.provider}
- </li>
+ <p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}>
+ <span class="icon">
+ {authMethods[m.type as KnownAuthMethods]?.icon}
+ </span>
+ <span>
+ {m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a>
+ </span>
+ </p>
);
})}
- </ul>
- <div>
- <button
- onClick={() => reducer.transition("delete_policy", { policy_index })}
- >
- Delete Policy
- </button>
+ </div>
+ <div style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
+ <button class="button is-info block" onClick={() => setEditingPolicy(policy_index)}>Edit</button>
+ <button class="button is-danger block" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button>
</div>
</div>
);
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
index 18560356a..49dd8fca8 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
@@ -26,8 +26,11 @@ import { SecretEditorScreen as TestedComponent } from './SecretEditorScreen';
export default {
- title: 'Pages/SecretEditorScreen',
+ title: 'Pages/backup/SecretEditorScreen',
component: TestedComponent,
+ args: {
+ order: 7,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
index a5235d66c..1b36a1b21 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
@@ -4,20 +4,21 @@ import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
import {
- AnastasisClientFrame,
- LabeledInput
+ AnastasisClientFrame
} from "./index";
+import { TextInput } from "../../components/fields/TextInput";
+import { FileInput } from "../../components/fields/FileInput";
export function SecretEditorScreen(): VNode {
const reducer = useAnastasisContext()
const [secretValue, setSecretValue] = useState("");
- const currentSecretName = reducer?.currentReducerState
- && ("secret_name" in reducer.currentReducerState)
+ const currentSecretName = reducer?.currentReducerState
+ && ("secret_name" in reducer.currentReducerState)
&& reducer.currentReducerState.secret_name;
const [secretName, setSecretName] = useState(currentSecretName || "");
-
+
if (!reducer) {
return <div>no reducer in context</div>
}
@@ -25,8 +26,8 @@ export function SecretEditorScreen(): VNode {
return <div>invalid state</div>
}
- const secretNext = (): void => {
- reducer.runTransaction(async (tx) => {
+ const secretNext = async (): Promise<void> => {
+ return reducer.runTransaction(async (tx) => {
await tx.transition("enter_secret_name", {
name: secretName,
});
@@ -44,21 +45,29 @@ export function SecretEditorScreen(): VNode {
};
return (
<AnastasisClientFrame
- title="Backup: Provide secret"
+ title="Backup: Provide secret to backup"
onNext={() => secretNext()}
>
<div>
- <LabeledInput
- label="Secret Name:"
+ <TextInput
+ label="Secret's name:"
grabFocus
bind={[secretName, setSecretName]}
/>
</div>
<div>
- <LabeledInput
- label="Secret Value:"
+ <TextInput
+ label="Enter the secret as text:"
bind={[secretValue, setSecretValue]}
/>
+ <div style={{display:'flex',}}>
+ or&nbsp;
+ <FileInput
+ label="click here"
+ bind={[secretValue, setSecretValue]}
+ />
+ &nbsp;to import a file
+ </div>
</div>
</AnastasisClientFrame>
);
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
index e9c597023..6919eebad 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -26,8 +25,11 @@ import { SecretSelectionScreen as TestedComponent } from './SecretSelectionScree
export default {
- title: 'Pages/SecretSelectionScreen',
+ title: 'Pages/recovery/SecretSelectionScreen',
component: TestedComponent,
+ args: {
+ order: 4,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
@@ -37,7 +39,7 @@ export default {
export const Example = createExample(TestedComponent, {
...reducerStatesExample.secretSelection,
recovery_document: {
- provider_url: 'http://anastasis.url/',
+ provider_url: 'https://kudos.demo.anastasis.lu/',
secret_name: 'secretName',
version: 1,
},
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index 903f57868..8aa5ed2f7 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -1,19 +1,17 @@
-/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton";
+import { NumberInput } from "../../components/fields/NumberInput";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): VNode {
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
- const [otherProvider, setOtherProvider] = useState<string>("");
const reducer = useAnastasisContext()
- const currentVersion = reducer?.currentReducerState
+ const currentVersion = (reducer?.currentReducerState
&& ("recovery_document" in reducer.currentReducerState)
- && reducer.currentReducerState.recovery_document?.version;
-
- const [otherVersion, setOtherVersion] = useState<number>(currentVersion || 0);
+ && reducer.currentReducerState.recovery_document?.version) || 0;
if (!reducer) {
return <div>no reducer in context</div>
@@ -22,9 +20,9 @@ export function SecretSelectionScreen(): VNode {
return <div>invalid state</div>
}
- function selectVersion(p: string, n: number): void {
- if (!reducer) return;
- reducer.runTransaction(async (tx) => {
+ async function doSelectVersion(p: string, n: number): Promise<void> {
+ if (!reducer) return Promise.resolve();
+ return reducer.runTransaction(async (tx) => {
await tx.transition("change_version", {
version: n,
provider_url: p,
@@ -33,55 +31,136 @@ export function SecretSelectionScreen(): VNode {
});
}
+ const providerList = Object.keys(reducer.currentReducerState.authentication_providers ?? {})
const recoveryDocument = reducer.currentReducerState.recovery_document
+
if (!recoveryDocument) {
- return (
- <AnastasisClientFrame hideNav title="Recovery: Problem">
- <p>No recovery document found</p>
- </AnastasisClientFrame>
- )
+ return <ChooseAnotherProviderScreen
+ providers={providerList} selected=""
+ onChange={(newProv) => doSelectVersion(newProv, 0)}
+ />
}
+
if (selectingVersion) {
- return (
- <AnastasisClientFrame hideNav title="Recovery: Select secret">
- <p>Select a different version of the secret</p>
- <select onChange={(e) => setOtherProvider((e.target as any).value)}>
- {Object.keys(reducer.currentReducerState.authentication_providers ?? {}).map(
- (x, i) => (
- <option key={i} selected={x === recoveryDocument.provider_url} value={x}>
- {x}
- </option>
- )
- )}
- </select>
- <div>
- <input
- value={otherVersion}
- onChange={(e) => setOtherVersion(Number((e.target as HTMLInputElement).value))}
- type="number" />
- <button onClick={() => selectVersion(otherProvider, otherVersion)}>
- Use this version
- </button>
+ return <SelectOtherVersionProviderScreen providers={providerList}
+ provider={recoveryDocument.provider_url} version={recoveryDocument.version}
+ onCancel={() => setSelectingVersion(false)}
+ onConfirm={doSelectVersion}
+ />
+ }
+
+ return (
+ <AnastasisClientFrame title="Recovery: Select secret">
+ <div class="columns">
+ <div class="column">
+ <div class="box" style={{ border: '2px solid green' }}>
+ <h1 class="subtitle">{recoveryDocument.provider_url}</h1>
+ <div class="block">
+ {currentVersion === 0 ? <p>
+ Set to recover the latest version
+ </p> : <p>
+ Set to recover the version number {currentVersion}
+ </p>}
+ </div>
+ <div class="buttons is-right">
+ <button class="button" onClick={(e) => setSelectingVersion(true)}>Change secret's version</button>
+ </div>
+ </div>
</div>
- <div>
- <button onClick={() => selectVersion(otherProvider, 0)}>
- Use latest version
- </button>
+ <div class="column">
+ <p>Secret found, you can select another version or continue to the challenges solving</p>
</div>
- <div>
- <button onClick={() => setSelectingVersion(false)}>Cancel</button>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+
+
+function ChooseAnotherProviderScreen({ providers, selected, onChange }: { selected: string; providers: string[]; onChange: (prov: string) => void }): VNode {
+ return (
+ <AnastasisClientFrame hideNext="Recovery document not found" title="Recovery: Problem">
+ <p>No recovery document found, try with another provider</p>
+ <div class="field">
+ <label class="label">Provider</label>
+ <div class="control is-expanded has-icons-left">
+ <div class="select is-fullwidth">
+ <select onChange={(e) => onChange(e.currentTarget.value)} value={selected}>
+ <option key="none" disabled selected value=""> Choose a provider </option>
+ {providers.map(prov => (
+ <option key={prov} value={prov}>
+ {prov}
+ </option>
+ ))}
+ </select>
+ <div class="icon is-small is-left">
+ <i class="mdi mdi-earth" />
+ </div>
+ </div>
</div>
- </AnastasisClientFrame>
- );
- }
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+
+function SelectOtherVersionProviderScreen({ providers, provider, version, onConfirm, onCancel }: { onCancel: () => void; provider: string; version: number; providers: string[]; onConfirm: (prov: string, v: number) => Promise<void>; }): VNode {
+ const [otherProvider, setOtherProvider] = useState<string>(provider);
+ const [otherVersion, setOtherVersion] = useState(`${version}`);
+
return (
- <AnastasisClientFrame title="Recovery: Select secret">
- <p>Provider: {recoveryDocument.provider_url}</p>
- <p>Secret version: {recoveryDocument.version}</p>
- <p>Secret name: {recoveryDocument.secret_name}</p>
- <button onClick={() => setSelectingVersion(true)}>
- Select different secret
- </button>
+ <AnastasisClientFrame hideNav title="Recovery: Select secret">
+ <div class="columns">
+ <div class="column">
+ <div class="box">
+ <h1 class="subtitle">Provider {otherProvider}</h1>
+ <div class="block">
+ {version === 0 ? <p>
+ Set to recover the latest version
+ </p> : <p>
+ Set to recover the version number {version}
+ </p>}
+ <p>Specify other version below or use the latest</p>
+ </div>
+
+ <div class="field">
+ <label class="label">Provider</label>
+ <div class="control is-expanded has-icons-left">
+ <div class="select is-fullwidth">
+ <select onChange={(e) => setOtherProvider(e.currentTarget.value)} value={otherProvider}>
+ <option key="none" disabled selected value=""> Choose a provider </option>
+ {providers.map(prov => (
+ <option key={prov} value={prov}>
+ {prov}
+ </option>
+ ))}
+ </select>
+ <div class="icon is-small is-left">
+ <i class="mdi mdi-earth" />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="container">
+ <NumberInput
+ label="Version"
+ placeholder="version number to recover"
+ grabFocus
+ bind={[otherVersion, setOtherVersion]} />
+ </div>
+ </div>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={onCancel}>Cancel</button>
+ <div class="buttons">
+ <AsyncButton class="button" onClick={() => onConfirm(otherProvider, 0)}>Use latest</AsyncButton>
+ <AsyncButton class="button is-info" onClick={() => onConfirm(otherProvider, parseInt(otherVersion, 10))}>Confirm</AsyncButton>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ .
+ </div>
+ </div>
+
</AnastasisClientFrame>
);
+
}
diff --git a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx
deleted file mode 100644
index 2c27895c2..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveEmailEntry.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveEmailEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", {
- answer,
- })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx b/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx
deleted file mode 100644
index 1a824acb8..000000000
--- a/packages/anastasis-webui/src/pages/home/SolvePostEntry.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolvePostEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", { answer })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx
deleted file mode 100644
index 72dadbe89..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveQuestionEntry.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveQuestionEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", { answer })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>Question: {challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
index 69af9be42..cb6561b3f 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
@@ -26,8 +26,11 @@ import { SolveScreen as TestedComponent } from './SolveScreen';
export default {
- title: 'Pages/SolveScreen',
+ title: 'Pages/recovery/SolveScreen',
component: TestedComponent,
+ args: {
+ order: 6,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
@@ -41,7 +44,7 @@ export const NotSupportedChallenge = createExample(TestedComponent, {
recovery_information: {
challenges: [{
cost: 'USD:1',
- instructions: 'follow htis instructions',
+ instructions: 'does P equals NP?',
type: 'chall-type',
uuid: 'ASDASDSAD!1'
}],
@@ -55,7 +58,7 @@ export const MismatchedChallengeId = createExample(TestedComponent, {
recovery_information: {
challenges: [{
cost: 'USD:1',
- instructions: 'follow htis instructions',
+ instructions: 'does P equals NP?',
type: 'chall-type',
uuid: 'ASDASDSAD!1'
}],
@@ -69,7 +72,7 @@ export const SmsChallenge = createExample(TestedComponent, {
recovery_information: {
challenges: [{
cost: 'USD:1',
- instructions: 'follow htis instructions',
+ instructions: 'SMS to 555-5555',
type: 'sms',
uuid: 'ASDASDSAD!1'
}],
@@ -83,7 +86,7 @@ export const QuestionChallenge = createExample(TestedComponent, {
recovery_information: {
challenges: [{
cost: 'USD:1',
- instructions: 'follow htis instructions',
+ instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
@@ -97,7 +100,7 @@ export const EmailChallenge = createExample(TestedComponent, {
recovery_information: {
challenges: [{
cost: 'USD:1',
- instructions: 'follow htis instructions',
+ instructions: 'Email to sebasjm@some-domain.com',
type: 'email',
uuid: 'ASDASDSAD!1'
}],
@@ -111,7 +114,7 @@ export const PostChallenge = createExample(TestedComponent, {
recovery_information: {
challenges: [{
cost: 'USD:1',
- instructions: 'follow htis instructions',
+ instructions: 'Letter to address in postal code ABC123',
type: 'post',
uuid: 'ASDASDSAD!1'
}],
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
index 05ae50b48..bc1a88db3 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
@@ -1,30 +1,93 @@
-import { h, VNode } from "preact";
-import { ChallengeFeedback, ChallengeInfo } from "../../../../anastasis-core/lib";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AnastasisClientFrame } from ".";
+import {
+ ChallengeFeedback,
+ ChallengeFeedbackStatus,
+ ChallengeInfo,
+} from "../../../../anastasis-core/lib";
+import { AsyncButton } from "../../components/AsyncButton";
+import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis";
-import { SolveEmailEntry } from "./SolveEmailEntry";
-import { SolvePostEntry } from "./SolvePostEntry";
-import { SolveQuestionEntry } from "./SolveQuestionEntry";
-import { SolveSmsEntry } from "./SolveSmsEntry";
-import { SolveUnsupportedEntry } from "./SolveUnsupportedEntry";
+
+function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
+ const { feedback } = props;
+ if (!feedback) {
+ return null;
+ }
+ switch (feedback.state) {
+ case ChallengeFeedbackStatus.Message:
+ return (
+ <div>
+ <p>{feedback.message}</p>
+ </div>
+ );
+ case ChallengeFeedbackStatus.Pending:
+ case ChallengeFeedbackStatus.AuthIban:
+ return null;
+ case ChallengeFeedbackStatus.RateLimitExceeded:
+ return <div>Rate limit exceeded.</div>;
+ case ChallengeFeedbackStatus.Redirect:
+ return <div>Redirect (FIXME: not supported)</div>;
+ case ChallengeFeedbackStatus.Unsupported:
+ return <div>Challenge not supported by client.</div>;
+ case ChallengeFeedbackStatus.TruthUnknown:
+ return <div>Truth unknown</div>;
+ default:
+ return (
+ <div>
+ <pre>{JSON.stringify(feedback)}</pre>
+ </div>
+ );
+ }
+}
export function SolveScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
+ const [answer, setAnswer] = useState("");
if (!reducer) {
- return <div>no reducer in context</div>
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
}
if (!reducer.currentReducerState.recovery_information) {
- return <div>no recovery information found</div>
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
}
if (!reducer.currentReducerState.selected_challenge_uuid) {
- return <div>no selected uuid</div>
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={() => reducer.back()}>Back</button>
+ </div>
+ </AnastasisClientFrame>
+ );
}
+
const chArr = reducer.currentReducerState.recovery_information.challenges;
- const challengeFeedback = reducer.currentReducerState.challenge_feedback ?? {};
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
const challenges: {
[uuid: string]: ChallengeInfo;
@@ -39,16 +102,137 @@ export function SolveScreen(): VNode {
email: SolveEmailEntry,
post: SolvePostEntry,
};
- const SolveDialog = dialogMap[selectedChallenge?.type] ?? SolveUnsupportedEntry;
+ const SolveDialog =
+ selectedChallenge === undefined
+ ? SolveUndefinedEntry
+ : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
return (
- <SolveDialog
- challenge={selectedChallenge}
- feedback={challengeFeedback[selectedUuid]} />
+ <AnastasisClientFrame hideNav title="Recovery: Solve challenge">
+ <SolveOverviewFeedbackDisplay
+ feedback={challengeFeedback[selectedUuid]}
+ />
+ <SolveDialog
+ id={selectedUuid}
+ answer={answer}
+ setAnswer={setAnswer}
+ challenge={selectedChallenge}
+ feedback={challengeFeedback[selectedUuid]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ </div>
+ </AnastasisClientFrame>
);
}
export interface SolveEntryProps {
+ id: string;
challenge: ChallengeInfo;
feedback?: ChallengeFeedback;
+ answer: string;
+ setAnswer: (s: string) => void;
}
+function SolveSmsEntry({
+ challenge,
+ answer,
+ setAnswer,
+}: SolveEntryProps): VNode {
+ return (
+ <Fragment>
+ <p>
+ An sms has been sent to "<b>{challenge.instructions}</b>". Type the code
+ below
+ </p>
+ <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+ </Fragment>
+ );
+}
+function SolveQuestionEntry({
+ challenge,
+ answer,
+ setAnswer,
+}: SolveEntryProps): VNode {
+ return (
+ <Fragment>
+ <p>Type the answer to the following question:</p>
+ <pre>{challenge.instructions}</pre>
+ <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+ </Fragment>
+ );
+}
+
+function SolvePostEntry({
+ challenge,
+ answer,
+ setAnswer,
+}: SolveEntryProps): VNode {
+ return (
+ <Fragment>
+ <p>
+ instruction for post type challenge "<b>{challenge.instructions}</b>"
+ </p>
+ <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+ </Fragment>
+ );
+}
+
+function SolveEmailEntry({
+ challenge,
+ answer,
+ setAnswer,
+}: SolveEntryProps): VNode {
+ return (
+ <Fragment>
+ <p>
+ An email has been sent to "<b>{challenge.instructions}</b>". Type the
+ code below
+ </p>
+ <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+ </Fragment>
+ );
+}
+
+function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
+ return (
+ <Fragment>
+ <p>
+ The challenge selected is not supported for this UI. Please update this
+ version or try using another policy.
+ </p>
+ <p>
+ <b>Challenge type:</b> {props.challenge.type}
+ </p>
+ </Fragment>
+ );
+}
+function SolveUndefinedEntry(props: SolveEntryProps): VNode {
+ return (
+ <Fragment>
+ <p>
+ There is no challenge information for id <b>"{props.id}"</b>. Try
+ resetting the recovery session.
+ </p>
+ </Fragment>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx
deleted file mode 100644
index 163e0d1f3..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveSmsEntry.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAnastasisContext } from "../../context/anastasis";
-import { AnastasisClientFrame, LabeledInput } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveSmsEntry({ challenge, feedback }: SolveEntryProps): VNode {
- const [answer, setAnswer] = useState("");
- const reducer = useAnastasisContext()
- const next = (): void => {
- if (reducer) reducer.transition("solve_challenge", {
- answer,
- })
- };
- return (
- <AnastasisClientFrame
- title="Recovery: Solve challenge"
- onNext={() => next()}
- >
- <p>Feedback: {JSON.stringify(feedback)}</p>
- <p>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx b/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx
deleted file mode 100644
index 7f538d249..000000000
--- a/packages/anastasis-webui/src/pages/home/SolveUnsupportedEntry.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { h, VNode } from "preact";
-import { AnastasisClientFrame } from "./index";
-import { SolveEntryProps } from "./SolveScreen";
-
-export function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
- return (
- <AnastasisClientFrame hideNext title="Recovery: Solve challenge">
- <p>{JSON.stringify(props.challenge)}</p>
- <p>Challenge not supported.</p>
- </AnastasisClientFrame>
- );
-}
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
index ad84cd8f2..657a2dd74 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
@@ -26,6 +26,9 @@ import { StartScreen as TestedComponent } from './StartScreen';
export default {
title: 'Pages/StartScreen',
component: TestedComponent,
+ args: {
+ order: 1,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
index 6625ec5b8..d53df4cae 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
@@ -10,24 +10,29 @@ export function StartScreen(): VNode {
}
return (
<AnastasisClientFrame hideNav title="Home">
- <div>
- <section class="section is-main-section">
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
- <div class="buttons is-right">
- <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
- Backup
- </button>
+ <div class="buttons">
+ <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
+ <div class="icon"><i class="mdi mdi-arrow-up" /></div>
+ <span>Backup a secret</span>
+ </button>
- <button class="button is-info" onClick={() => reducer.startRecover()}>Recover</button>
- </div>
+ <button class="button is-info" onClick={() => reducer.startRecover()}>
+ <div class="icon"><i class="mdi mdi-arrow-down" /></div>
+ <span>Recover a secret</span>
+ </button>
- </div>
- <div class="column" />
+ {/* <button class="button">
+ <div class="icon"><i class="mdi mdi-file" /></div>
+ <span>Restore a session</span>
+ </button> */}
</div>
- </section>
+
+ </div>
+ <div class="column" />
</div>
</AnastasisClientFrame>
);
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
index e2f3d521e..7568ccd69 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
@@ -25,8 +25,11 @@ import { TruthsPayingScreen as TestedComponent } from './TruthsPayingScreen';
export default {
- title: 'Pages/TruthsPayingScreen',
+ title: 'Pages/backup/__TruthsPayingScreen',
component: TestedComponent,
+ args: {
+ order: 10,
+ },
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
index 319f590a0..0b32e0db5 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
@@ -13,8 +13,8 @@ export function TruthsPayingScreen(): VNode {
const payments = reducer.currentReducerState.payments ?? [];
return (
<AnastasisClientFrame
- hideNext
- title="Backup: Authentication Storage Payments"
+ hideNext={"FIXME"}
+ title="Backup: Truths Paying"
>
<p>
Some of the providers require a payment to store the encrypted
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
new file mode 100644
index 000000000..e178a4955
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createExample, reducerStatesExample } from '../../../utils';
+import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+
+
+export default {
+ title: 'Pages/backup/authMethods/email',
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+const type: KnownAuthMethods = 'email'
+
+export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: []
+});
+
+export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Email to sebasjm@email.com ',
+ remove: () => null
+ }]
+});
+
+export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Email to sebasjm@email.com',
+ remove: () => null
+ },{
+ challenge: 'qwe',
+ type,
+ instructions: 'Email to someone@sebasjm.com',
+ remove: () => null
+ }]
+});
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
new file mode 100644
index 000000000..1a6be1b61
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AnastasisClientFrame } from "../index";
+import { TextInput } from "../../../components/fields/TextInput";
+import { EmailInput } from "../../../components/fields/EmailInput";
+
+const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+
+export function AuthMethodEmailSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode {
+ const [email, setEmail] = useState("");
+ const addEmailAuth = (): void => addAuthMethod({
+ authentication_method: {
+ type: "email",
+ instructions: `Email to ${email}`,
+ challenge: encodeCrock(stringToBytes(email)),
+ },
+ });
+ const emailError = !EMAIL_PATTERN.test(email) ? 'Email address is not valid' : undefined
+ const errors = !email ? 'Add your email' : emailError
+
+ return (
+ <AnastasisClientFrame hideNav title="Add email authentication">
+ <p>
+ For email authentication, you need to provide an email address. When
+ recovering your secret, you will need to enter the code you receive by
+ email.
+ </p>
+ <div>
+ <EmailInput
+ label="Email address"
+ error={emailError}
+ placeholder="email@domain.com"
+ bind={[email, setEmail]} />
+ </div>
+ {configured.length > 0 && <section class="section">
+ <div class="block">
+ Your emails:
+ </div><div class="block">
+ {configured.map((c, i) => {
+ return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
+ <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
+ </div>
+ })}
+ </div></section>}
+ <div>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span data-tooltip={errors}>
+ <button class="button is-info" disabled={errors !== undefined} onClick={addEmailAuth}>Add</button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
new file mode 100644
index 000000000..71f618646
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
@@ -0,0 +1,65 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createExample, reducerStatesExample } from '../../../utils';
+import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+
+
+export default {
+ title: 'Pages/backup/authMethods/IBAN',
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+const type: KnownAuthMethods = 'iban'
+
+export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: []
+});
+
+export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
+ remove: () => null
+ }]
+});
+export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Wire transfer from QWEASD123123 with holder Javier',
+ remove: () => null
+ },{
+ challenge: 'qwe',
+ type,
+ instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
+ remove: () => null
+ }]
+},);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
new file mode 100644
index 000000000..c9edbfa07
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
@@ -0,0 +1,68 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ canonicalJson,
+ encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { TextInput } from "../../../components/fields/TextInput";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AnastasisClientFrame } from "../index";
+
+export function AuthMethodIbanSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+ const [name, setName] = useState("");
+ const [account, setAccount] = useState("");
+ const addIbanAuth = (): void => addAuthMethod({
+ authentication_method: {
+ type: "iban",
+ instructions: `Wire transfer from ${account} with holder ${name}`,
+ challenge: encodeCrock(stringToBytes(canonicalJson({
+ name, account
+ }))),
+ },
+ });
+ const errors = !name ? 'Add an account name' : (
+ !account ? 'Add an account IBAN number' : undefined
+ )
+ return (
+ <AnastasisClientFrame hideNav title="Add bank transfer authentication">
+ <p>
+ For bank transfer authentication, you need to provide a bank
+ account (account holder name and IBAN). When recovering your
+ secret, you will be asked to pay the recovery fee via bank
+ transfer from the account you provided here.
+ </p>
+ <div>
+ <TextInput
+ label="Bank account holder name"
+ grabFocus
+ placeholder="John Smith"
+ bind={[name, setName]} />
+ <TextInput
+ label="IBAN"
+ placeholder="DE91100000000123456789"
+ bind={[account, setAccount]} />
+ </div>
+ {configured.length > 0 && <section class="section">
+ <div class="block">
+ Your bank accounts:
+ </div><div class="block">
+ {configured.map((c, i) => {
+ return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
+ <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
+ </div>
+ })}
+ </div></section>}
+ <div>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span data-tooltip={errors}>
+ <button class="button is-info" disabled={errors !== undefined} onClick={addIbanAuth}>Add</button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
new file mode 100644
index 000000000..0f1c17495
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createExample, reducerStatesExample } from '../../../utils';
+import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+
+
+export default {
+ title: 'Pages/backup/authMethods/Post',
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+const type: KnownAuthMethods = 'post'
+
+export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: []
+});
+
+export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Letter to address in postal code QWE456',
+ remove: () => null
+ }]
+});
+
+export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Letter to address in postal code QWE456',
+ remove: () => null
+ },{
+ challenge: 'qwe',
+ type,
+ instructions: 'Letter to address in postal code ABC123',
+ remove: () => null
+ }]
+});
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
new file mode 100644
index 000000000..bfeaaa832
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
@@ -0,0 +1,102 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ canonicalJson, encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { TextInput } from "../../../components/fields/TextInput";
+import { AnastasisClientFrame } from "..";
+
+export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+ const [fullName, setFullName] = useState("");
+ const [street, setStreet] = useState("");
+ const [city, setCity] = useState("");
+ const [postcode, setPostcode] = useState("");
+ const [country, setCountry] = useState("");
+
+ const addPostAuth = () => {
+ const challengeJson = {
+ full_name: fullName,
+ street,
+ city,
+ postcode,
+ country,
+ };
+ addAuthMethod({
+ authentication_method: {
+ type: "post",
+ instructions: `Letter to address in postal code ${postcode}`,
+ challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
+ },
+ });
+ };
+
+ const errors = !fullName ? 'The full name is missing' : (
+ !street ? 'The street is missing' : (
+ !city ? 'The city is missing' : (
+ !postcode ? 'The postcode is missing' : (
+ !country ? 'The country is missing' : undefined
+ )
+ )
+ )
+ )
+ return (
+ <AnastasisClientFrame hideNav title="Add postal authentication">
+ <p>
+ For postal letter authentication, you need to provide a postal
+ address. When recovering your secret, you will be asked to enter a
+ code that you will receive in a letter to that address.
+ </p>
+ <div>
+ <TextInput
+ grabFocus
+ label="Full Name"
+ bind={[fullName, setFullName]}
+ />
+ </div>
+ <div>
+ <TextInput
+ label="Street"
+ bind={[street, setStreet]}
+ />
+ </div>
+ <div>
+ <TextInput
+ label="City" bind={[city, setCity]}
+ />
+ </div>
+ <div>
+ <TextInput
+ label="Postal Code" bind={[postcode, setPostcode]}
+ />
+ </div>
+ <div>
+ <TextInput
+ label="Country"
+ bind={[country, setCountry]}
+ />
+ </div>
+
+ {configured.length > 0 && <section class="section">
+ <div class="block">
+ Your postal code:
+ </div><div class="block">
+ {configured.map((c, i) => {
+ return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
+ <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
+ </div>
+ })}
+ </div>
+ </section>}
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span data-tooltip={errors}>
+ <button class="button is-info" disabled={errors !== undefined} onClick={addPostAuth}>Add</button>
+ </span>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
new file mode 100644
index 000000000..3ba4a84ca
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createExample, reducerStatesExample } from '../../../utils';
+import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+
+
+export default {
+ title: 'Pages/backup/authMethods/Question',
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+const type: KnownAuthMethods = 'question'
+
+export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: []
+});
+
+export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Is integer factorization polynomial? (non-quantum computer)',
+ remove: () => null
+ }]
+});
+
+export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Does P equal NP?',
+ remove: () => null
+ },{
+ challenge: 'asd',
+ type,
+ instructions: 'Are continuous groups automatically differential groups?',
+ remove: () => null
+ }]
+});
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
new file mode 100644
index 000000000..04fa00d59
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
@@ -0,0 +1,71 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AnastasisClientFrame } from "../index";
+import { TextInput } from "../../../components/fields/TextInput";
+
+export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode {
+ const [questionText, setQuestionText] = useState("");
+ const [answerText, setAnswerText] = useState("");
+ const addQuestionAuth = (): void => addAuthMethod({
+ authentication_method: {
+ type: "question",
+ instructions: questionText,
+ challenge: encodeCrock(stringToBytes(answerText)),
+ },
+ });
+
+ const errors = !questionText ? "Add your security question" : (
+ !answerText ? 'Add the answer to your question' : undefined
+ )
+ return (
+ <AnastasisClientFrame hideNav title="Add Security Question">
+ <div>
+ <p>
+ For2 security question authentication, you need to provide a question
+ and its answer. When recovering your secret, you will be shown the
+ question and you will need to type the answer exactly as you typed it
+ here.
+ </p>
+ <div>
+ <TextInput
+ label="Security question"
+ grabFocus
+ placeholder="Your question"
+ bind={[questionText, setQuestionText]} />
+ </div>
+ <div>
+ <TextInput
+ label="Answer"
+ placeholder="Your answer"
+ bind={[answerText, setAnswerText]}
+ />
+ </div>
+
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span data-tooltip={errors}>
+ <button class="button is-info" disabled={errors !== undefined} onClick={addQuestionAuth}>Add</button>
+ </span>
+ </div>
+
+ {configured.length > 0 && <section class="section">
+ <div class="block">
+ Your security questions:
+ </div><div class="block">
+ {configured.map((c, i) => {
+ return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
+ <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
+ </div>
+ })}
+ </div></section>}
+ </div>
+ </AnastasisClientFrame >
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
new file mode 100644
index 000000000..ae8297ef7
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createExample, reducerStatesExample } from '../../../utils';
+import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+
+
+export default {
+ title: 'Pages/backup/authMethods/Sms',
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+const type: KnownAuthMethods = 'sms'
+
+export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: []
+});
+
+export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'SMS to +11-1234-2345',
+ remove: () => null
+ }]
+});
+
+export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'SMS to +11-1234-2345',
+ remove: () => null
+ },{
+ challenge: 'qwe',
+ type,
+ instructions: 'SMS to +11-5555-2345',
+ remove: () => null
+ }]
+});
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
new file mode 100644
index 000000000..9e85af2b2
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
@@ -0,0 +1,63 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { NumberInput } from "../../../components/fields/NumberInput";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AnastasisClientFrame } from "../index";
+
+export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+ const [mobileNumber, setMobileNumber] = useState("");
+ const addSmsAuth = (): void => {
+ addAuthMethod({
+ authentication_method: {
+ type: "sms",
+ instructions: `SMS to ${mobileNumber}`,
+ challenge: encodeCrock(stringToBytes(mobileNumber)),
+ },
+ });
+ };
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+ const errors = !mobileNumber ? 'Add a mobile number' : undefined
+ return (
+ <AnastasisClientFrame hideNav title="Add SMS authentication">
+ <div>
+ <p>
+ For SMS authentication, you need to provide a mobile number. When
+ recovering your secret, you will be asked to enter the code you
+ receive via SMS.
+ </p>
+ <div class="container">
+ <NumberInput
+ label="Mobile number"
+ placeholder="Your mobile number"
+ grabFocus
+ bind={[mobileNumber, setMobileNumber]} />
+ </div>
+ {configured.length > 0 && <section class="section">
+ <div class="block">
+ Your mobile numbers:
+ </div><div class="block">
+ {configured.map((c, i) => {
+ return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p>
+ <div><button class="button is-danger" onClick={c.remove}>Delete</button></div>
+ </div>
+ })}
+ </div></section>}
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span data-tooltip={errors}>
+ <button class="button is-info" disabled={errors !== undefined} onClick={addSmsAuth}>Add</button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
new file mode 100644
index 000000000..4e46b600e
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createExample, reducerStatesExample } from '../../../utils';
+import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+
+
+export default {
+ title: 'Pages/backup/authMethods/TOTP',
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+const type: KnownAuthMethods = 'totp'
+
+export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: []
+});
+export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis"',
+ remove: () => null
+ }]
+});
+export const WithMoreExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis1"',
+ remove: () => null
+ },{
+ challenge: 'qwe',
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis2"',
+ remove: () => null
+ }]
+});
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
new file mode 100644
index 000000000..fd0bd0224
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
@@ -0,0 +1,81 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useMemo, useState } from "preact/hooks";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AnastasisClientFrame } from "../index";
+import { TextInput } from "../../../components/fields/TextInput";
+import { QR } from "../../../components/QR";
+import { base32enc, computeTOTPandCheck } from "./totp";
+
+export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+ const [name, setName] = useState("anastasis");
+ const [test, setTest] = useState("");
+ const digits = 8
+ const secretKey = useMemo(() => {
+ const array = new Uint8Array(32)
+ return window.crypto.getRandomValues(array)
+ }, [])
+ const secret32 = base32enc(secretKey);
+ const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`
+
+ const addTotpAuth = (): void => addAuthMethod({
+ authentication_method: {
+ type: "totp",
+ instructions: `Enter ${digits} digits code for "${name}"`,
+ challenge: encodeCrock(stringToBytes(totpURL)),
+ },
+ });
+
+ const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
+
+ const errors = !name ? 'The TOTP name is missing' : (
+ !testCodeMatches ? 'The test code doesnt match' : undefined
+ );
+ return (
+ <AnastasisClientFrame hideNav title="Add TOTP authentication">
+ <p>
+ For Time-based One-Time Password (TOTP) authentication, you need to set
+ a name for the TOTP secret. Then, you must scan the generated QR code
+ with your TOTP App to import the TOTP secret into your TOTP App.
+ </p>
+ <div class="block">
+ <TextInput
+ label="TOTP Name"
+ grabFocus
+ bind={[name, setName]} />
+ </div>
+ <div style={{ height: 300 }}>
+ <QR text={totpURL} />
+ </div>
+ <p>
+ After scanning the code with your TOTP App, test it in the input below.
+ </p>
+ <TextInput
+ label="Test code"
+ bind={[test, setTest]} />
+ {configured.length > 0 && <section class="section">
+ <div class="block">
+ Your TOTP numbers:
+ </div><div class="block">
+ {configured.map((c, i) => {
+ return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p>
+ <div><button class="button is-danger" onClick={c.remove}>Delete</button></div>
+ </div>
+ })}
+ </div></section>}
+ <div>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <span data-tooltip={errors}>
+ <button class="button is-info" disabled={errors !== undefined} onClick={addTotpAuth}>Add</button>
+ </span>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
new file mode 100644
index 000000000..3c4c7bf39
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/camelcase */
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createExample, reducerStatesExample } from '../../../utils';
+import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+import logoImage from '../../../assets/logo.jpeg'
+
+export default {
+ title: 'Pages/backup/authMethods/Video',
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: 'onUpdate' },
+ onBack: { action: 'onBack' },
+ },
+};
+
+const type: KnownAuthMethods = 'video'
+
+export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: []
+});
+
+export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: logoImage,
+ remove: () => null
+ }]
+});
+
+export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
+ configured: [{
+ challenge: 'qwe',
+ type,
+ instructions: logoImage,
+ remove: () => null
+ },{
+ challenge: 'qwe',
+ type,
+ instructions: logoImage,
+ remove: () => null
+ }]
+});
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
new file mode 100644
index 000000000..8be999b3f
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
@@ -0,0 +1,56 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ encodeCrock,
+ stringToBytes
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { ImageInput } from "../../../components/fields/ImageInput";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AnastasisClientFrame } from "../index";
+
+export function AuthMethodVideoSetup({cancel, addAuthMethod, configured}: AuthMethodSetupProps): VNode {
+ const [image, setImage] = useState("");
+ const addVideoAuth = (): void => {
+ addAuthMethod({
+ authentication_method: {
+ type: "video",
+ instructions: 'Join a video call',
+ challenge: encodeCrock(stringToBytes(image)),
+ },
+ })
+ };
+ return (
+ <AnastasisClientFrame hideNav title="Add video authentication">
+ <p>
+ For video identification, you need to provide a passport-style
+ photograph. When recovering your secret, you will be asked to join a
+ video call. During that call, a human will use the photograph to
+ verify your identity.
+ </p>
+ <div style={{textAlign:'center'}}>
+ <ImageInput
+ label="Choose photograph"
+ grabFocus
+ bind={[image, setImage]} />
+ </div>
+ {configured.length > 0 && <section class="section">
+ <div class="block">
+ Your photographs:
+ </div><div class="block">
+ {configured.map((c, i) => {
+ return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <img style={{ marginTop: 'auto', marginBottom: 'auto', width: 100, height:100, border: 'solid 1px black' }} src={c.instructions} />
+ <div style={{marginTop: 'auto', marginBottom: 'auto'}}><button class="button is-danger" onClick={c.remove}>Delete</button></div>
+ </div>
+ })}
+ </div></section>}
+ <div>
+ <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
+ <button class="button" onClick={cancel}>Cancel</button>
+ <button class="button is-info" onClick={addVideoAuth}>Add</button>
+ </div>
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
new file mode 100644
index 000000000..7b0cce883
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
@@ -0,0 +1,69 @@
+import { h, VNode } from "preact";
+import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+
+import { AuthMethodEmailSetup as EmailScreen } from "./AuthMethodEmailSetup";
+import { AuthMethodIbanSetup as IbanScreen } from "./AuthMethodIbanSetup";
+import { AuthMethodPostSetup as PostalScreen } from "./AuthMethodPostSetup";
+import { AuthMethodQuestionSetup as QuestionScreen } from "./AuthMethodQuestionSetup";
+import { AuthMethodSmsSetup as SmsScreen } from "./AuthMethodSmsSetup";
+import { AuthMethodTotpSetup as TotpScreen } from "./AuthMethodTotpSetup";
+import { AuthMethodVideoSetup as VideScreen } from "./AuthMethodVideoSetup";
+import postalIcon from '../../../assets/icons/auth_method/postal.svg';
+import questionIcon from '../../../assets/icons/auth_method/question.svg';
+import smsIcon from '../../../assets/icons/auth_method/sms.svg';
+import videoIcon from '../../../assets/icons/auth_method/video.svg';
+
+interface AuthMethodConfiguration {
+ icon: VNode;
+ label: string;
+ screen: (props: AuthMethodSetupProps) => VNode;
+ skip?: boolean;
+}
+export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
+
+type KnowMethodConfig = {
+ [name in KnownAuthMethods]: AuthMethodConfiguration;
+};
+
+export const authMethods: KnowMethodConfig = {
+ question: {
+ icon: <img src={questionIcon} />,
+ label: "Question",
+ screen: QuestionScreen
+ },
+ sms: {
+ icon: <img src={smsIcon} />,
+ label: "SMS",
+ screen: SmsScreen
+ },
+ email: {
+ icon: <i class="mdi mdi-email" />,
+ label: "Email",
+ screen: EmailScreen
+
+ },
+ iban: {
+ icon: <i class="mdi mdi-bank" />,
+ label: "IBAN",
+ screen: IbanScreen
+
+ },
+ post: {
+ icon: <img src={postalIcon} />,
+ label: "Physical mail",
+ screen: PostalScreen
+
+ },
+ totp: {
+ icon: <i class="mdi mdi-devices" />,
+ label: "TOTP",
+ screen: TotpScreen
+
+ },
+ video: {
+ icon: <img src={videoIcon} />,
+ label: "Video",
+ screen: VideScreen,
+ skip: true,
+ }
+} \ No newline at end of file
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
new file mode 100644
index 000000000..0bc3feaf8
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
@@ -0,0 +1,56 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import jssha from 'jssha'
+
+const SEARCH_RANGE = 16
+const timeStep = 30
+
+export function computeTOTPandCheck(secretKey: Uint8Array, digits: number, code: number): boolean {
+ const now = new Date().getTime()
+ const epoch = Math.floor(Math.round(now / 1000.0) / timeStep);
+
+ for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) {
+ const movingFactor = (epoch + ms).toString(16).padStart(16, "0");
+
+ const hmacSha = new jssha('SHA-1', 'HEX', { hmacKey: { value: secretKey, format: 'UINT8ARRAY' } });
+ hmacSha.update(movingFactor);
+ const hmac_text = hmacSha.getHMAC('UINT8ARRAY');
+
+ const offset = (hmac_text[hmac_text.length - 1] & 0xf)
+
+ const otp = ((
+ (hmac_text[offset + 0] << 24) +
+ (hmac_text[offset + 1] << 16) +
+ (hmac_text[offset + 2] << 8) +
+ (hmac_text[offset + 3])
+ ) & 0x7fffffff) % Math.pow(10, digits)
+
+ if (otp == code) return true
+ }
+ return false
+}
+
+const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split('')
+export function base32enc(buffer: Uint8Array): string {
+ let rpos = 0
+ let bits = 0
+ let vbit = 0
+
+ let result = ""
+ while ((rpos < buffer.length) || (vbit > 0)) {
+ if ((rpos < buffer.length) && (vbit < 5)) {
+ bits = (bits << 8) | buffer[rpos++];
+ vbit += 8;
+ }
+ if (vbit < 5) {
+ bits <<= (5 - vbit);
+ vbit = 5;
+ }
+ result += encTable__[(bits >> (vbit - 5)) & 31];
+ vbit -= 5;
+ }
+ return result
+}
+
+// const array = new Uint8Array(256)
+// const secretKey = window.crypto.getRandomValues(array)
+// console.log(base32enc(secretKey))
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 4cec47ec8..07bc7c604 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -11,11 +11,11 @@ import {
VNode
} from "preact";
import {
- useErrorBoundary,
- useLayoutEffect,
- useRef
+ useErrorBoundary
} from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton";
import { Menu } from "../../components/menu";
+import { Notifications } from "../../components/Notifications";
import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
import {
AnastasisReducerApi,
@@ -25,8 +25,8 @@ import { AttributeEntryScreen } from "./AttributeEntryScreen";
import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
import { BackupFinishedScreen } from "./BackupFinishedScreen";
import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen";
+import { ChallengePayingScreen } from "./ChallengePayingScreen";
import { ContinentSelectionScreen } from "./ContinentSelectionScreen";
-import { CountrySelectionScreen } from "./CountrySelectionScreen";
import { PoliciesPayingScreen } from "./PoliciesPayingScreen";
import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen";
import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen";
@@ -61,7 +61,7 @@ interface AnastasisClientFrameProps {
/**
* Hide only the "next" button.
*/
- hideNext?: boolean;
+ hideNext?: string;
}
function ErrorBoundary(props: {
@@ -96,11 +96,11 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
- const next = (): void => {
+ const next = async (): Promise<void> => {
if (props.onNext) {
- props.onNext();
+ await props.onNext();
} else {
- reducer.transition("next", {});
+ await reducer.transition("next", {});
}
};
const handleKeyPress = (
@@ -112,18 +112,18 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
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={() => reducer.back()}>Back</button>
+ <AsyncButton class="button is-info" data-tooltip={props.hideNext} onClick={next} disabled={props.hideNext !== undefined}>Next</AsyncButton>
</div>
) : null}
- </div>
+ </section>
</div>
</Fragment>
);
@@ -140,7 +140,7 @@ const AnastasisClient: FunctionalComponent = () => {
);
};
-const AnastasisClientImpl: FunctionalComponent = () => {
+function AnastasisClientImpl(): VNode {
const reducer = useAnastasisContext()
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
@@ -153,18 +153,12 @@ const AnastasisClientImpl: FunctionalComponent = () => {
if (
state.backup_state === BackupStates.ContinentSelecting ||
- state.recovery_state === RecoveryStates.ContinentSelecting
- ) {
- return (
- <ContinentSelectionScreen />
- );
- }
- if (
+ state.recovery_state === RecoveryStates.ContinentSelecting ||
state.backup_state === BackupStates.CountrySelecting ||
state.recovery_state === RecoveryStates.CountrySelecting
) {
return (
- <CountrySelectionScreen />
+ <ContinentSelectionScreen />
);
}
if (
@@ -222,7 +216,9 @@ const AnastasisClientImpl: FunctionalComponent = () => {
<RecoveryFinishedScreen />
);
}
-
+ if (state.recovery_state === RecoveryStates.ChallengePaying) {
+ return <ChallengePayingScreen />;
+ }
console.log("unknown state", reducer.currentReducerState);
return (
<AnastasisClientFrame hideNav title="Bug">
@@ -232,32 +228,6 @@ const AnastasisClientImpl: FunctionalComponent = () => {
</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>
- );
}
/**
@@ -266,13 +236,11 @@ export function LabeledInput(props: LabeledInputProps): VNode {
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>
+ return (<Notifications removeNotification={reducer.dismissError} notifications={[{
+ type: "ERROR",
+ message: `Error code: ${reducer.currentError.code}`,
+ description: reducer.currentError.hint
+ }]} />
);
}