anastasis-webui: ui tweaks

This commit is contained in:
Florian Dold 2021-11-08 17:09:26 +01:00
parent 6a0c5263bb
commit 4dd5b75cfa
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 191 additions and 113 deletions

View File

@ -1,53 +1,62 @@
import { AuthMethod } from "anastasis-core"; import { AuthMethod, ReducerStateBackup } from "anastasis-core";
import { ComponentChildren, Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, AuthMethodSetupProps, AuthMethodWithRemove, KnownAuthMethods } from "./authMethod"; import {
authMethods,
AuthMethodSetupProps,
AuthMethodWithRemove,
KnownAuthMethods,
} from "./authMethod";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
export function AuthenticationEditorScreen(): VNode { export function AuthenticationEditorScreen(): VNode {
const [noProvidersAck, setNoProvidersAck] = useState(false) const [noProvidersAck, setNoProvidersAck] = useState(false);
const [selectedMethod, setSelectedMethod] = useState<KnownAuthMethods | undefined>(undefined); const [selectedMethod, setSelectedMethod] = useState<
KnownAuthMethods | undefined
>(undefined);
const [tooFewAuths, setTooFewAuths] = useState(false);
// const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined) // const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
const reducer = useAnastasisContext();
const reducer = useAnastasisContext()
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div> return <div>no reducer in context</div>;
} }
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { if (
return <div>invalid state</div> !reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
} }
const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? []; const configuredAuthMethods: AuthMethod[] =
reducer.currentReducerState.authentication_methods ?? [];
const haveMethodsConfigured = configuredAuthMethods.length > 0; const haveMethodsConfigured = configuredAuthMethods.length > 0;
function removeByIndex(index: number): void { function removeByIndex(index: number): void {
if (reducer) reducer.transition("delete_authentication", { if (reducer)
authentication_method: index, reducer.transition("delete_authentication", {
}) authentication_method: index,
});
} }
const camByType: { [s: string]: AuthMethodWithRemove[] } = {} const camByType: { [s: string]: AuthMethodWithRemove[] } = {};
for (let index = 0; index < configuredAuthMethods.length; index++) { for (let index = 0; index < configuredAuthMethods.length; index++) {
const cam = { const cam = {
...configuredAuthMethods[index], ...configuredAuthMethods[index],
remove: () => removeByIndex(index) remove: () => removeByIndex(index),
} };
const prevValue = camByType[cam.type] || [] const prevValue = camByType[cam.type] || [];
prevValue.push(cam) prevValue.push(cam);
camByType[cam.type] = prevValue; camByType[cam.type] = prevValue;
} }
const providers = reducer.currentReducerState.authentication_providers!; const providers = reducer.currentReducerState.authentication_providers!;
const authAvailableSet = new Set<string>(); const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) { for (const provKey of Object.keys(providers)) {
const p = providers[provKey]; const p = providers[provKey];
if ("http_status" in p && (!("error_code" in p)) && p.methods) { if ("http_status" in p && !("error_code" in p) && p.methods) {
for (const meth of p.methods) { for (const meth of p.methods) {
authAvailableSet.add(meth.type); authAvailableSet.add(meth.type);
} }
@ -61,102 +70,147 @@ export function AuthenticationEditorScreen(): VNode {
setSelectedMethod(undefined); setSelectedMethod(undefined);
}; };
const AuthSetup = authMethods[selectedMethod].setup ?? AuthMethodNotImplemented; const AuthSetup =
return (<Fragment> authMethods[selectedMethod].setup ?? AuthMethodNotImplemented;
<AuthSetup return (
cancel={cancel} <Fragment>
configured={camByType[selectedMethod] || []} <AuthSetup
addAuthMethod={addMethod} cancel={cancel}
method={selectedMethod} /> configured={camByType[selectedMethod] || []}
addAuthMethod={addMethod}
method={selectedMethod}
/>
{!authAvailableSet.has(selectedMethod) && <ConfirmModal active {!authAvailableSet.has(selectedMethod) && (
onCancel={cancel} description="No providers founds" label="Add a provider manually" <ConfirmModal
onConfirm={() => { active
null onCancel={cancel}
}} description="No providers founds"
> label="Add a provider manually"
We have found no trusted cloud providers for your recovery secret. You can add a provider manually. onConfirm={() => {
To add a provider you must know the provider URL (e.g. https://provider.com) null;
<p> }}
<a>More about cloud providers</a> >
</p> <p>
</ConfirmModal>} We have found no Anastasis providers that support this
authentication method. You can add a provider manually. To add a
</Fragment> provider you must know the provider URL (e.g.
https://provider.com)
</p>
<p>
<a>Learn more about Anastasis providers</a>
</p>
</ConfirmModal>
)}
</Fragment>
); );
} }
function MethodButton(props: { method: KnownAuthMethods }): VNode { function MethodButton(props: { method: KnownAuthMethods }): VNode {
if (authMethods[props.method].skip) return <div /> if (authMethods[props.method].skip) return <div />;
return ( return (
<div class="block"> <div class="block">
<button <button
style={{ justifyContent: 'space-between' }} style={{ justifyContent: "space-between" }}
class="button is-fullwidth" class="button is-fullwidth"
onClick={() => { onClick={() => {
setSelectedMethod(props.method); setSelectedMethod(props.method);
}} }}
> >
<div style={{ display: 'flex' }}> <div style={{ display: "flex" }}>
<span class="icon "> <span class="icon ">{authMethods[props.method].icon}</span>
{authMethods[props.method].icon} {authAvailableSet.has(props.method) ? (
</span> <span>Add a {authMethods[props.method].label} challenge</span>
{authAvailableSet.has(props.method) ? ) : (
<span> <span>Add a {authMethods[props.method].label} provider</span>
Add a {authMethods[props.method].label} challenge )}
</span> :
<span>
Add a {authMethods[props.method].label} provider
</span>
}
</div> </div>
{!authAvailableSet.has(props.method) && {!authAvailableSet.has(props.method) && (
<span class="icon has-text-danger" > <span class="icon has-text-danger">
<i class="mdi mdi-exclamation-thick" /> <i class="mdi mdi-exclamation-thick" />
</span> </span>
} )}
{camByType[props.method] && {camByType[props.method] && (
<span class="tag is-info" > <span class="tag is-info">{camByType[props.method].length}</span>
{camByType[props.method].length} )}
</span>
}
</button> </button>
</div> </div>
); );
} }
const errors = !haveMethodsConfigured ? "There is not enough authentication methods." : undefined; const errors = !haveMethodsConfigured
? "There is not enough authentication methods."
: undefined;
const handleNext = async () => {
const st = reducer.currentReducerState as ReducerStateBackup;
if ((st.authentication_methods ?? []).length <= 2) {
setTooFewAuths(true);
} else {
await reducer.transition("next", {});
}
};
return ( return (
<AnastasisClientFrame title="Backup: Configure Authentication Methods" hideNext={errors}> <AnastasisClientFrame
title="Backup: Configure Authentication Methods"
hideNext={errors}
onNext={handleNext}
>
<div class="columns"> <div class="columns">
<div class="column is-half"> <div class="column is-half">
<div> <div>
{getKeys(authMethods).map(method => <MethodButton key={method} method={method} />)} {getKeys(authMethods).map((method) => (
<MethodButton key={method} method={method} />
))}
</div> </div>
{authAvailableSet.size === 0 && <ConfirmModal active={!noProvidersAck} {tooFewAuths ? (
onCancel={() => setNoProvidersAck(true)} description="No providers founds" label="Add a provider manually" <ConfirmModal
onConfirm={() => { active={tooFewAuths}
null onCancel={() => setTooFewAuths(false)}
}} description="Too few auth methods configured"
> label="Proceed anyway"
We have found no trusted cloud providers for your recovery secret. You can add a provider manually. onConfirm={() => reducer.transition("next", {})}
To add a provider you must know the provider URL (e.g. https://provider.com) >
<p> You have selected fewer than three authentication methods. We
<a>More about cloud providers</a> recommend that you add at least three.
</p> </ConfirmModal>
</ConfirmModal>} ) : null}
{authAvailableSet.size === 0 && (
<ConfirmModal
active={!noProvidersAck}
onCancel={() => setNoProvidersAck(true)}
description="No providers founds"
label="Add a provider manually"
onConfirm={() => {
null;
}}
>
<p>
We have found no Anastasis providers for your chosen country /
currency. You can add a providers manually. To add a provider
you must know the provider URL (e.g. https://provider.com)
</p>
<p>
<a>Learn more about Anastasis providers</a>
</p>
</ConfirmModal>
)}
</div> </div>
<div class="column is-half"> <div class="column is-half">
<p class="block"> <p class="block">
When recovering your wallet, you will be asked to verify your identity via the methods you configure here. When recovering your wallet, you will be asked to verify your
The list of authentication method is defined by the backup provider list. identity via the methods you configure here. The list of
authentication method is defined by the backup provider list.
</p> </p>
<p class="block"> <p class="block">
<button class="button is-info">Manage the backup provider's list</button> <button class="button is-info">
Manage the backup provider's list
</button>
</p> </p>
{authAvailableSet.size > 0 && <p class="block"> {authAvailableSet.size > 0 && (
We couldn't find provider for some of the authentication methods. <p class="block">
</p>} We couldn't find provider for some of the authentication methods.
</p>
)}
</div> </div>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
@ -172,30 +226,54 @@ function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
); );
} }
function ConfirmModal({
function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode { active,
return <div class={active ? "modal is-active" : "modal"}> description,
<div class="modal-background " onClick={onCancel} /> onCancel,
<div class="modal-card" style={{ maxWidth: 700 }}> onConfirm,
<header class="modal-card-head"> children,
{!description ? null : <p class="modal-card-title"><b>{description}</b></p>} danger,
<button class="delete " aria-label="close" onClick={onCancel} /> disabled,
</header> label = "Confirm",
<section class="modal-card-body"> }: ConfirmModelProps): VNode {
{children} return (
</section> <div class={active ? "modal is-active" : "modal"}>
<footer class="modal-card-foot"> <div class="modal-background " onClick={onCancel} />
<button class="button" onClick={onCancel} >Dismiss</button> <div class="modal-card" style={{ maxWidth: 700 }}>
<div class="buttons is-right" style={{ width: '100%' }}> <header class="modal-card-head">
<button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} >{label}</button> {!description ? null : (
</div> <p class="modal-card-title">
</footer> <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> </div>
<button class="modal-close is-large " aria-label="close" onClick={onCancel} /> );
</div>
} }
interface Props { interface ConfirmModelProps {
active?: boolean; active?: boolean;
description?: string; description?: string;
onCancel?: () => void; onCancel?: () => void;

View File

@ -32,7 +32,7 @@ export function ContinentSelectionScreen(): VNode {
const theContinent = reducer.currentReducerState.selected_continent || ""; const theContinent = reducer.currentReducerState.selected_continent || "";
// const cc = reducer.currentReducerState.selected_country || ""; // const cc = reducer.currentReducerState.selected_country || "";
const theCountry = countryList.find((c) => c.code === countryCode); const theCountry = countryList.find((c) => c.code === countryCode);
const selectCountryAction = () => { const selectCountryAction = async () => {
//selection should be when the select box changes it value //selection should be when the select box changes it value
if (!theCountry) return; if (!theCountry) return;
reducer.transition("select_country", { reducer.transition("select_country", {
@ -123,8 +123,8 @@ export function ContinentSelectionScreen(): VNode {
</div> </div>
<div class="column is-two-third"> <div class="column is-two-third">
<p> <p>
Your choice will help us with asking the right information to unique Your selection will help us ask right information to uniquely
identify you when you want to recover your backed up secrets. identify you when you want to recover your secret again.
</p> </p>
<p> <p>
Choose the country that issued most of your long-term legal Choose the country that issued most of your long-term legal

View File

@ -48,7 +48,7 @@ export function withProcessLabel(
} }
interface AnastasisClientFrameProps { interface AnastasisClientFrameProps {
onNext?(): void; onNext?(): Promise<void>;
/** /**
* Override for the "back" functionality. * Override for the "back" functionality.
*/ */