feedback from meeting and editing policy

This commit is contained in:
Sebastian 2021-11-03 17:30:11 -03:00
parent 9fb6536fbc
commit a82b5a6992
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
24 changed files with 657 additions and 221 deletions

View File

@ -4,9 +4,9 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "preact build", "build": "preact build --no-sw --no-esm",
"serve": "sirv build --port 8080 --cors --single", "serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch", "dev": "preact watch --no-sw --no-esm",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"test": "jest ./tests", "test": "jest ./tests",
"build-storybook": "build-storybook", "build-storybook": "build-storybook",

View File

@ -0,0 +1,51 @@
/*
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 { ComponentChildren, h, VNode } from "preact";
// import { LoadingModal } from "../modal";
import { useAsync } from "../hooks/async";
// import { Translate } from "../../i18n";
type Props = {
children: ComponentChildren;
disabled: boolean;
onClick?: () => Promise<void>;
[rest: string]: any;
};
export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode {
const { isLoading, request } = useAsync(onClick);
// if (isSlow) {
// return <LoadingModal onCancel={cancel} />;
// }
console.log(isLoading)
if (isLoading) {
return <button class="button">Loading...</button>;
}
return <span {...rest}>
<button class="button is-info" onClick={request} disabled={disabled}>
{children}
</button>
</span>;
}

View File

@ -1,4 +1,4 @@
import { format } from "date-fns"; import { format, isAfter, parse, sub, subYears } from "date-fns";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { DatePicker } from "../picker/DatePicker"; import { DatePicker } from "../picker/DatePicker";
@ -19,16 +19,14 @@ export function DateInput(props: DateInputProps): VNode {
inputRef.current?.focus(); inputRef.current?.focus();
} }
}, [props.grabFocus]); }, [props.grabFocus]);
const [opened, setOpened2] = useState(false) const [opened, setOpened] = useState(false)
function setOpened(v: boolean): void {
console.log('dale', v)
setOpened2(v)
}
const value = props.bind[0] || ""; const value = props.bind[0] || "";
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false)
const showError = dirty && props.error const showError = dirty && props.error
const calendar = subYears(new Date(), 30)
return <div class="field"> return <div class="field">
<label class="label"> <label class="label">
{props.label} {props.label}
@ -36,27 +34,37 @@ export function DateInput(props: DateInputProps): VNode {
<i class="mdi mdi-information" /> <i class="mdi mdi-information" />
</span>} </span>}
</label> </label>
<div class="control has-icons-right"> <div class="control">
<input <div class="field has-addons">
type="text" <p class="control">
class={showError ? 'input is-danger' : 'input'} <input
readonly type="text"
onFocus={() => { setOpened(true) } } class={showError ? 'input is-danger' : 'input'}
value={value} value={value}
ref={inputRef} /> onChange={(e) => {
const text = e.currentTarget.value
<span class="control icon is-right"> setDirty(true)
<span class="icon"><i class="mdi mdi-calendar" /></span> props.bind[1](text);
</span> }}
ref={inputRef} />
</p>
<p class="control">
<a class="button" onClick={() => { setOpened(true) }}>
<span class="icon"><i class="mdi mdi-calendar" /></span>
</a>
</p>
</div>
</div> </div>
<p class="help">Using the format yyyy-mm-dd</p>
{showError && <p class="help is-danger">{props.error}</p>} {showError && <p class="help is-danger">{props.error}</p>}
<DatePicker <DatePicker
opened={opened} opened={opened}
initialDate={calendar}
years={props.years} years={props.years}
closeFunction={() => setOpened(false)} closeFunction={() => setOpened(false)}
dateReceiver={(d) => { dateReceiver={(d) => {
setDirty(true) setDirty(true)
const v = format(d, 'yyyy/MM/dd') const v = format(d, 'yyyy-MM-dd')
props.bind[1](v); props.bind[1](v);
}} }}
/> />

View File

@ -49,7 +49,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
</a> </a>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
<LangSelector /> {/* <LangSelector /> */}
</div> </div>
</div> </div>
</div> </div>

View File

@ -39,9 +39,9 @@ export function Sidebar({ mobile }: Props): VNode {
return ( return (
<aside class="aside is-placed-left is-expanded"> <aside class="aside is-placed-left is-expanded">
{mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}> {/* {mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}>
<LangSelector /> <LangSelector />
</div>} </div>} */}
<div class="aside-tools"> <div class="aside-tools">
<div class="aside-tools-label"> <div class="aside-tools-label">
<div><b>Anastasis</b> Reducer</div> <div><b>Anastasis</b> Reducer</div>
@ -68,7 +68,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}> reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span> <span class="menu-item-label"><Translate>Location</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
@ -85,7 +85,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Policies reviewing</Translate></span> <span class="menu-item-label"><Translate>Policies</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
@ -94,12 +94,12 @@ export function Sidebar({ mobile }: Props): VNode {
<span class="menu-item-label"><Translate>Secret input</Translate></span> <span class="menu-item-label"><Translate>Secret input</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}> {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span> <span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div> </div>
</li> </li> */}
<li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}> <li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
@ -116,7 +116,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting || <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ||
reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}> reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span> <span class="menu-item-label"><Translate>Location</Translate></span>
</div> </div>
</li> </li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}> <li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>

View File

@ -24,6 +24,7 @@ import { h, Component } from "preact";
interface Props { interface Props {
closeFunction?: () => void; closeFunction?: () => void;
dateReceiver?: (d: Date) => void; dateReceiver?: (d: Date) => void;
initialDate?: Date;
years?: Array<number>; years?: Array<number>;
opened?: boolean; opened?: boolean;
} }
@ -213,8 +214,8 @@ export class DatePicker extends Component<Props, State> {
// } // }
} }
constructor() { constructor(props) {
super(); super(props);
this.closeDatePicker = this.closeDatePicker.bind(this); this.closeDatePicker = this.closeDatePicker.bind(this);
this.dayClicked = this.dayClicked.bind(this); this.dayClicked = this.dayClicked.bind(this);
@ -226,11 +227,12 @@ export class DatePicker extends Component<Props, State> {
this.toggleYearSelector = this.toggleYearSelector.bind(this); this.toggleYearSelector = this.toggleYearSelector.bind(this);
this.displaySelectedMonth = this.displaySelectedMonth.bind(this); this.displaySelectedMonth = this.displaySelectedMonth.bind(this);
const initial = props.initialDate || now;
this.state = { this.state = {
currentDate: now, currentDate: initial,
displayedMonth: now.getMonth(), displayedMonth: initial.getMonth(),
displayedYear: now.getFullYear(), displayedYear: initial.getFullYear(),
selectYearMode: false selectYearMode: false
} }
} }

View File

@ -0,0 +1,77 @@
/*
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 { useState } from "preact/hooks";
// import { cancelPendingRequest } from "./backend";
export interface Options {
slowTolerance: number;
}
export interface AsyncOperationApi<T> {
request: (...a: any) => void;
cancel: () => void;
data: T | undefined;
isSlow: boolean;
isLoading: boolean;
error: string | undefined;
}
export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> {
const [data, setData] = useState<T | undefined>(undefined);
const [isLoading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(undefined);
const [isSlow, setSlow] = useState(false)
const request = async (...args: any) => {
if (!fn) return;
setLoading(true);
console.log("loading true")
const handler = setTimeout(() => {
setSlow(true)
}, tooLong)
try {
const result = await fn(...args);
console.log(result)
setData(result);
} catch (error) {
setError(error);
}
setLoading(false);
setSlow(false)
clearTimeout(handler)
};
function cancel() {
// cancelPendingRequest()
setLoading(false);
setSlow(false)
}
return {
request,
cancel,
data,
isSlow,
isLoading,
error
};
}

View File

@ -52,8 +52,8 @@ export const Backup = createExample(TestedComponent, {
uuid: 'asdasdsa2', uuid: 'asdasdsa2',
widget: 'wid', widget: 'wid',
}, { }, {
name: 'date', name: 'birthdate',
label: 'third', label: 'birthdate',
type: 'date', type: 'date',
uuid: 'asdasdsa3', uuid: 'asdasdsa3',
widget: 'calendar', widget: 'calendar',

View File

@ -7,6 +7,7 @@ import { AnastasisClientFrame, withProcessLabel } from "./index";
import { TextInput } from "../../components/fields/TextInput"; import { TextInput } from "../../components/fields/TextInput";
import { DateInput } from "../../components/fields/DateInput"; import { DateInput } from "../../components/fields/DateInput";
import { NumberInput } from "../../components/fields/NumberInput"; import { NumberInput } from "../../components/fields/NumberInput";
import { isAfter, parse } from "date-fns";
export function AttributeEntryScreen(): VNode { export function AttributeEntryScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext()
@ -46,15 +47,14 @@ export function AttributeEntryScreen(): VNode {
identity_attributes: attrs, identity_attributes: attrs,
})} })}
> >
<div class="columns"> <div class="columns" style={{ maxWidth: 'unset' }}>
<div class="column is-half"> <div class="column is-one-third">
{fieldList} {fieldList}
</div> </div>
<div class="column is-half" > <div class="column is-two-third" >
<p>This personal information will help to locate your secret.</p> <p>This personal information will help to locate your secret.</p>
<h1><b>This stay private</b></h1> <h1 class="title">This stays private</h1>
<p>The information you have entered here: <p>The information you have entered here:</p>
</p>
<ul> <ul>
<li> <li>
<span class="icon is-right"> <span class="icon is-right">
@ -111,15 +111,17 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
bind={[props.value, props.setValue]} bind={[props.value, props.setValue]}
/> />
} }
<span> <div class="block">
This stays private
<span class="icon is-right"> <span class="icon is-right">
<i class="mdi mdi-eye-off" /> <i class="mdi mdi-eye-off" />
</span> </span>
This stay private </div>
</span>
</div> </div>
); );
} }
const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/
function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined { function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined {
const pattern = spec['validation-regex'] const pattern = spec['validation-regex']
@ -136,5 +138,22 @@ function checkIfValid(value: string, spec: UserAttributeSpec): string | undefine
if (!optional && !value) { if (!optional && !value) {
return 'This value is required' 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 return undefined
} }

View File

@ -142,6 +142,10 @@ export function AuthenticationEditorScreen(): VNode {
</div> </div>
<div class="column is-half"> <div class="column is-half">
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 identity via the methods you configure here.
<b>Explain the exclamation marks</b>
<a>Explain how to add providers</a>
</div> </div>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>

View File

@ -23,7 +23,7 @@ export function BackupFinishedScreen(): VNode {
</p>} </p>}
{details && <div class="block"> {details && <div class="block">
<p>The backup is stored by the following providers:</p> <p>The backup is stored by the following providers:</p>
{Object.keys(details).map((x, i) => { {Object.keys(details).map((x, i) => {
const sd = details[x]; const sd = details[x];
return ( return (
@ -31,11 +31,14 @@ export function BackupFinishedScreen(): VNode {
{x} {x}
<p> <p>
version {sd.policy_version} version {sd.policy_version}
{sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd/MM/yyyy')}` : ' without expiration date'} {sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'}
</p> </p>
</div> </div>
); );
})} })}
</div>} </div>}
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
</div>
</AnastasisClientFrame>); </AnastasisClientFrame>);
} }

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/camelcase */
/* /*
This file is part of GNU Taler This file is part of GNU Taler
(C) 2021 Taler Systems S.A. (C) 2021 Taler Systems S.A.
@ -19,12 +20,13 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from '../../utils';
import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen'; import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
export default { export default {
title: 'Pages/ContinentSelectionScreen', title: 'Pages/Location',
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 2, order: 2,
@ -35,6 +37,16 @@ export default {
}, },
}; };
export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent); export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent); 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);

View File

@ -36,20 +36,21 @@ export function ContinentSelectionScreen(): VNode {
}) })
} }
const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || // const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting; // reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
const errors = !theCountry ? "Select a country" : undefined const errors = !theCountry ? "Select a country" : undefined
return ( return (
<AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Select location")} onNext={selectCountryAction}> <AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Where do you live?")} onNext={selectCountryAction}>
<div class="columns">
<div class="column is-half"> <div class="columns" >
<div class="column is-one-third">
<div class="field"> <div class="field">
<label class="label">Continent</label> <label class="label">Continent</label>
<div class="control has-icons-left"> <div class="control is-expanded has-icons-left">
<div class="select " > <div class="select is-fullwidth" >
<select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} disabled={!step1}> <select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} >
<option key="none" disabled selected value=""> Choose a continent </option> <option key="none" disabled selected value=""> Choose a continent </option>
{continentList.map(prov => ( {continentList.map(prov => (
<option key={prov.name} value={prov.name}> <option key={prov.name} value={prov.name}>
@ -61,18 +62,13 @@ export function ContinentSelectionScreen(): VNode {
<i class="mdi mdi-earth" /> <i class="mdi mdi-earth" />
</div> </div>
</div> </div>
{!step1 && <span class="control">
<a class="button is-danger" onClick={() => reducer.back()}>
X
</a>
</span>}
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Country</label> <label class="label">Country</label>
<div class="control has-icons-left"> <div class="control is-expanded has-icons-left">
<div class="select" > <div class="select is-fullwidth" >
<select onChange={(e) => selectCountry((e.target as any).value)} disabled={!theContinent} value={theCountry?.code || ""}> <select onChange={(e) => selectCountry((e.target as any).value)} disabled={!theContinent} value={theCountry?.code || ""}>
<option key="none" disabled selected value=""> Choose a country </option> <option key="none" disabled selected value=""> Choose a country </option>
{countryList.map(prov => ( {countryList.map(prov => (
@ -88,17 +84,17 @@ export function ContinentSelectionScreen(): VNode {
</div> </div>
</div> </div>
{theCountry && <div class="field"> {/* {theCountry && <div class="field">
<label class="label">Available currencies:</label> <label class="label">Available currencies:</label>
<div class="control"> <div class="control">
<input class="input is-small" type="text" readonly value={theCountry.currency} /> <input class="input is-small" type="text" readonly value={theCountry.currency} />
</div> </div>
</div>} </div>} */}
</div> </div>
<div class="column is-half"> <div class="column is-two-third">
<p> <p>
A location will help to define a common information that will be use to locate your secret and a currency Your location will help us to determine which personal information
for payments if needed. ask you for the next step.
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,39 +0,0 @@
/*
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 { CountrySelectionScreen as TestedComponent } from './CountrySelectionScreen';
export default {
title: 'Pages/CountrySelectionScreen',
component: TestedComponent,
args: {
order: 3,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
},
};
export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry);
export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry);

View File

@ -1,31 +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={"FIXME"} title={withProcessLabel(reducer, "Select Country")} >
<div style={{ display: 'flex', flexDirection: 'column' }}>
{reducer.currentReducerState.countries!.map((x: any) => (
<div key={x.name}>
<button class="button" onClick={() => sel(x)} >
{x.name} ({x.currency})
</button>
</div>
))}
</div>
</AnastasisClientFrame>
);
}

View File

@ -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});

View File

@ -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>
}

View File

@ -233,16 +233,16 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
instructions: "Does P equal NP?", instructions: "Does P equal NP?",
challenge: "C5SP8" challenge: "C5SP8"
},{ },{
type: "email", type: "totp",
instructions: "Email to qwe@asd.com", instructions: "Response code for 'Anastasis'",
challenge: "E5VPA" challenge: "E5VPA"
}, { }, {
type: "sms", type: "sms",
instructions: "SMS to 555-555", instructions: "SMS to 6666-6666",
challenge: "" challenge: ""
}, { }, {
type: "question", type: "question",
instructions: "Does P equal NP?", instructions: "How did the chicken cross the road?",
challenge: "C5SP8" challenge: "C5SP8"
}] }]
} as ReducerState); } as ReducerState);

View File

@ -1,10 +1,14 @@
/* eslint-disable @typescript-eslint/camelcase */ /* eslint-disable @typescript-eslint/camelcase */
import { AuthMethod } from "anastasis-core";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
import { authMethods, KnownAuthMethods } from "./authMethod"; import { authMethods, KnownAuthMethods } from "./authMethod";
import { EditPoliciesScreen } from "./EditPoliciesScreen";
import { AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(): VNode { export function ReviewPoliciesScreen(): VNode {
const [editingPolicy, setEditingPolicy] = useState<number | 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>
@ -12,20 +16,44 @@ export function ReviewPoliciesScreen(): VNode {
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div> return <div>invalid state</div>
} }
const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? []; const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? []; const policies = reducer.currentReducerState.policies ?? [];
if (editingPolicy !== undefined) {
return (
<EditPoliciesScreen
index={editingPolicy}
cancel={() => setEditingPolicy(undefined)}
confirm={(newMethods) => {
reducer.runTransaction(async (tx) => {
await tx.transition("delete_policy", {
policy_index: editingPolicy
});
await tx.transition("add_policy", {
policy: newMethods
});
});
setEditingPolicy(undefined)
}}
/>
)
}
const errors = policies.length < 1 ? 'Need more policies' : undefined const errors = policies.length < 1 ? 'Need more policies' : undefined
return ( return (
<AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies"> <AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies">
{policies.length > 0 && <p class="block"> {policies.length > 0 && <p class="block">
Based on your configured authentication method you have created, some policies 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 have been configured. In order to recover your secret you have to solve all the
challenges of at least one policy. challenges of at least one policy.
</p> } </p>}
{policies.length < 1 && <p class="block"> {policies.length < 1 && <p class="block">
No policies had been created. Go back and add more authentication methods. No policies had been created. Go back and add more authentication methods.
</p> } </p>}
<div class="block" onClick={() => setEditingPolicy(policies.length + 1)}>
<button class="button is-success">Add new policy</button>
</div>
{policies.map((p, policy_index) => { {policies.map((p, policy_index) => {
const methods = p.methods const methods = p.methods
.map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider })) .map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider }))
@ -44,18 +72,21 @@ export function ReviewPoliciesScreen(): VNode {
</p>} </p>}
{methods.map((m, i) => { {methods.map((m, i) => {
return ( return (
<p key={i} class="block" style={{display:'flex', alignItems:'center'}}> <p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}>
<span class="icon"> <span class="icon">
{authMethods[m.type as KnownAuthMethods]?.icon} {authMethods[m.type as KnownAuthMethods]?.icon}
</span> </span>
<span> <span>
{m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a> {m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a>
</span> </span>
</p> </p>
); );
})} })}
</div> </div>
<div style={{ marginTop: 'auto', marginBottom: 'auto' }}><button class="button is-danger" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</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> </div>
); );
})} })}

View File

@ -10,33 +10,29 @@ export function StartScreen(): VNode {
} }
return ( return (
<AnastasisClientFrame hideNav title="Home"> <AnastasisClientFrame hideNav title="Home">
<div> <div class="columns">
<section class="section is-main-section"> <div class="column" />
<div class="columns"> <div class="column is-four-fifths">
<div class="column" />
<div class="column is-four-fifths">
<div class="buttons"> <div class="buttons">
<button class="button is-success" autoFocus onClick={() => reducer.startBackup()}> <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
<div class="icon"><i class="mdi mdi-arrow-up" /></div> <div class="icon"><i class="mdi mdi-arrow-up" /></div>
<span>Backup a secret</span> <span>Backup a secret</span>
</button> </button>
<button class="button is-info" onClick={() => reducer.startRecover()}> <button class="button is-info" onClick={() => reducer.startRecover()}>
<div class="icon"><i class="mdi mdi-arrow-down" /></div> <div class="icon"><i class="mdi mdi-arrow-down" /></div>
<span>Recover a secret</span> <span>Recover a secret</span>
</button> </button>
<button class="button"> <button class="button">
<div class="icon"><i class="mdi mdi-file" /></div> <div class="icon"><i class="mdi mdi-file" /></div>
<span>Restore a session</span> <span>Restore a session</span>
</button> </button>
</div>
</div>
<div class="column" />
</div> </div>
</section>
</div>
<div class="column" />
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -27,7 +27,7 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
<AnastasisClientFrame hideNav title="Add Security Question"> <AnastasisClientFrame hideNav title="Add Security Question">
<div> <div>
<p> <p>
For security question authentication, you need to provide a question For2 security question authentication, you need to provide a question
and its answer. When recovering your secret, you will be shown the 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 question and you will need to type the answer exactly as you typed it
here. here.
@ -47,6 +47,13 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
/> />
</div> </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"> {configured.length > 0 && <section class="section">
<div class="block"> <div class="block">
Your security questions: Your security questions:
@ -58,12 +65,6 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
</div> </div>
})} })}
</div></section>} </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={addQuestionAuth}>Add</button>
</span>
</div>
</div> </div>
</AnastasisClientFrame > </AnastasisClientFrame >
); );

View File

@ -13,6 +13,7 @@ import {
import { import {
useErrorBoundary useErrorBoundary
} from "preact/hooks"; } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
import { Menu } from "../../components/menu"; import { Menu } from "../../components/menu";
import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis"; import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
import { import {
@ -25,7 +26,6 @@ import { BackupFinishedScreen } from "./BackupFinishedScreen";
import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen"; import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen";
import { ChallengePayingScreen } from "./ChallengePayingScreen"; import { ChallengePayingScreen } from "./ChallengePayingScreen";
import { ContinentSelectionScreen } from "./ContinentSelectionScreen"; import { ContinentSelectionScreen } from "./ContinentSelectionScreen";
import { CountrySelectionScreen } from "./CountrySelectionScreen";
import { PoliciesPayingScreen } from "./PoliciesPayingScreen"; import { PoliciesPayingScreen } from "./PoliciesPayingScreen";
import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen"; import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen";
import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen"; import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen";
@ -95,12 +95,19 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
if (!reducer) { if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>; return <p>Fatal: Reducer must be in context.</p>;
} }
const next = (): void => { const next = async (): Promise<void> => {
if (props.onNext) { return new Promise((res, rej) => {
props.onNext(); try {
} else { if (props.onNext) {
reducer.transition("next", {}); props.onNext();
} } else {
reducer.transition("next", {});
}
res()
} catch {
rej()
}
})
}; };
const handleKeyPress = ( const handleKeyPress = (
e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>, e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
@ -111,20 +118,18 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
return ( return (
<Fragment> <Fragment>
<Menu title="Anastasis" /> <Menu title="Anastasis" />
<div> <div class="home" onKeyPress={(e) => handleKeyPress(e)}>
<div class="home" onKeyPress={(e) => handleKeyPress(e)}> <h1 class="title">{props.title}</h1>
<h1 class="title">{props.title}</h1> <section class="section is-main-section">
<ErrorBanner /> <ErrorBanner />
{props.children} {props.children}
{!props.hideNav ? ( {!props.hideNav ? (
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button> <button class="button" onClick={() => reducer.back()}>Back</button>
<span data-tooltip={props.hideNext}> <AsyncButton data-tooltip={props.hideNext} onClick={next} disabled={props.hideNext !== undefined}>Next</AsyncButton>
<button class="button is-info" onClick={next} disabled={props.hideNext !== undefined}>Next</button>
</span>
</div> </div>
) : null} ) : null}
</div> </section>
</div> </div>
</Fragment> </Fragment>
); );

View File

@ -195,7 +195,7 @@ div[data-tooltip]::before {
padding: 1em 1em; padding: 1em 1em;
min-height: 100%; min-height: 100%;
width: 100%; width: 100%;
max-width: 40em; // max-width: 40em;
} }
// .home div { // .home div {

View File

@ -86,10 +86,10 @@ const base = {
{ {
type: "question", type: "question",
usage_fee: "COL:0" usage_fee: "COL:0"
},{ }, {
type: "sms", type: "sms",
usage_fee: "COL:0" usage_fee: "COL:0"
},{ }, {
type: "email", type: "email",
usage_fee: "COL:0" usage_fee: "COL:0"
}, },
@ -98,6 +98,48 @@ const base = {
storage_limit_in_megabytes: 16, storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0" truth_upload_fee: "COL:0"
}, },
"https://kudos.demo.anastasis.lu/": {
http_status: 200,
annual_fee: "COL:0",
business_name: "ana",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
usage_fee: "COL:0"
}, {
type: "email",
usage_fee: "COL:0"
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
},
"https://anastasis.demo.taler.net/": {
http_status: 200,
annual_fee: "COL:0",
business_name: "ana",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
usage_fee: "COL:0"
}, {
type: "sms",
usage_fee: "COL:0"
}, {
type: "totp",
usage_fee: "COL:0"
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
},
"http://localhost:8087/": { "http://localhost:8087/": {
code: 8414, code: 8414,
hint: "request to provider failed" hint: "request to provider failed"
@ -118,55 +160,72 @@ const base = {
export const reducerStatesExample = { export const reducerStatesExample = {
initial: undefined, initial: undefined,
recoverySelectCountry: {...base, recoverySelectCountry: {
...base,
recovery_state: RecoveryStates.CountrySelecting recovery_state: RecoveryStates.CountrySelecting
} as ReducerState, } as ReducerState,
recoverySelectContinent: {...base, recoverySelectContinent: {
...base,
recovery_state: RecoveryStates.ContinentSelecting, recovery_state: RecoveryStates.ContinentSelecting,
} as ReducerState, } as ReducerState,
secretSelection: {...base, secretSelection: {
...base,
recovery_state: RecoveryStates.SecretSelecting, recovery_state: RecoveryStates.SecretSelecting,
} as ReducerState, } as ReducerState,
recoveryFinished: {...base, recoveryFinished: {
...base,
recovery_state: RecoveryStates.RecoveryFinished, recovery_state: RecoveryStates.RecoveryFinished,
} as ReducerState, } as ReducerState,
challengeSelecting: {...base, challengeSelecting: {
...base,
recovery_state: RecoveryStates.ChallengeSelecting, recovery_state: RecoveryStates.ChallengeSelecting,
} as ReducerState, } as ReducerState,
challengeSolving: {...base, challengeSolving: {
...base,
recovery_state: RecoveryStates.ChallengeSolving, recovery_state: RecoveryStates.ChallengeSolving,
} as ReducerState, } as ReducerState,
challengePaying: {...base, challengePaying: {
...base,
recovery_state: RecoveryStates.ChallengePaying, recovery_state: RecoveryStates.ChallengePaying,
} as ReducerState, } as ReducerState,
recoveryAttributeEditing: {...base, recoveryAttributeEditing: {
...base,
recovery_state: RecoveryStates.UserAttributesCollecting recovery_state: RecoveryStates.UserAttributesCollecting
} as ReducerState, } as ReducerState,
backupSelectCountry: {...base, backupSelectCountry: {
...base,
backup_state: BackupStates.CountrySelecting backup_state: BackupStates.CountrySelecting
} as ReducerState, } as ReducerState,
backupSelectContinent: {...base, backupSelectContinent: {
...base,
backup_state: BackupStates.ContinentSelecting, backup_state: BackupStates.ContinentSelecting,
} as ReducerState, } as ReducerState,
secretEdition: {...base, secretEdition: {
...base,
backup_state: BackupStates.SecretEditing, backup_state: BackupStates.SecretEditing,
} as ReducerState, } as ReducerState,
policyReview: {...base, policyReview: {
...base,
backup_state: BackupStates.PoliciesReviewing, backup_state: BackupStates.PoliciesReviewing,
} as ReducerState, } as ReducerState,
policyPay: {...base, policyPay: {
...base,
backup_state: BackupStates.PoliciesPaying, backup_state: BackupStates.PoliciesPaying,
} as ReducerState, } as ReducerState,
backupFinished: {...base, backupFinished: {
...base,
backup_state: BackupStates.BackupFinished, backup_state: BackupStates.BackupFinished,
} as ReducerState, } as ReducerState,
authEditing: {...base, authEditing: {
...base,
backup_state: BackupStates.AuthenticationsEditing backup_state: BackupStates.AuthenticationsEditing
} as ReducerState, } as ReducerState,
backupAttributeEditing: {...base, backupAttributeEditing: {
...base,
backup_state: BackupStates.UserAttributesCollecting backup_state: BackupStates.UserAttributesCollecting
} as ReducerState, } as ReducerState,
truthsPaying: {...base, truthsPaying: {
...base,
backup_state: BackupStates.TruthsPaying backup_state: BackupStates.TruthsPaying
} as ReducerState, } as ReducerState,