feat: two factor authentication segment in preferences with mocked state (#600)

This commit is contained in:
Gorjan Petrovski
2021-07-21 12:17:50 +02:00
committed by GitHub
parent bffd9ec54d
commit d9c5fd5129
32 changed files with 618 additions and 247 deletions

View File

@@ -65,7 +65,7 @@ import { NotesContextMenuDirective } from './components/NotesContextMenu';
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
import { IconDirective } from './components/Icon';
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
import { PreferencesDirective } from './components/preferences';
import { PreferencesDirective } from './preferences';
function reloadHiddenFirefoxTab(): boolean {
/**

View File

@@ -0,0 +1,42 @@
import { FunctionalComponent, ComponentChild } from 'preact';
interface Props {
className?: string;
disabled?: boolean;
left?: ComponentChild[];
right?: ComponentChild[];
text?: string;
}
/**
* Input that can be decorated on the left and right side
*/
export const DecoratedInput: FunctionalComponent<Props> = ({
className = '',
disabled = false,
left,
right,
text,
}) => {
const base =
'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center gap-4';
const stateClasses = disabled
? 'no-border bg-grey-5'
: 'border-solid border-1 border-gray-300';
const classes = `${base} ${stateClasses} ${className}`;
return (
<div className={classes}>
{left}
<div className="flex-grow">
<input
type="text"
className="w-full no-border color-black"
disabled={disabled}
value={text}
/>
</div>
{right}
</div>
);
};

View File

@@ -23,6 +23,8 @@ import SettingsIcon from '../../icons/ic-settings.svg';
import StarIcon from '../../icons/ic-star.svg';
import ThemesIcon from '../../icons/ic-themes.svg';
import UserIcon from '../../icons/ic-user.svg';
import CopyIcon from '../../icons/ic-copy.svg';
import DownloadIcon from '../../icons/ic-download.svg';
import { toDirective } from './utils';
import { FunctionalComponent } from 'preact';
@@ -52,6 +54,8 @@ const ICONS = {
star: StarIcon,
themes: ThemesIcon,
user: UserIcon,
copy: CopyIcon,
download: DownloadIcon,
};
export type IconType = keyof typeof ICONS;
@@ -61,7 +65,7 @@ type Props = {
className?: string;
};
export const Icon: FunctionalComponent<Props> = ({ type, className }) => {
export const Icon: FunctionalComponent<Props> = ({ type, className = '' }) => {
const IconComponent = ICONS[type];
return <IconComponent className={`sn-icon ${className}`} />;
};

View File

@@ -1,53 +1,38 @@
import { FunctionComponent } from 'preact';
import { Icon, IconType } from './Icon';
const ICON_BUTTON_TYPES: {
[type: string]: { className: string };
} = {
normal: {
className: '',
},
primary: {
className: 'info',
},
};
export type IconButtonType = keyof typeof ICON_BUTTON_TYPES;
interface IconButtonProps {
interface Props {
/**
* onClick - preventDefault is handled within the component
*/
onClick: () => void;
type: IconButtonType;
className?: string;
iconType: IconType;
icon: IconType;
}
/**
* CircleButton component with an icon for SPA
* IconButton component with an icon
* preventDefault is already handled within the component
*/
export const IconButton: FunctionComponent<IconButtonProps> = ({
export const IconButton: FunctionComponent<Props> = ({
onClick,
type,
className,
iconType,
icon,
}) => {
const click = (e: MouseEvent) => {
e.preventDefault();
onClick();
};
const typeProps = ICON_BUTTON_TYPES[type];
return (
<button
className={`sn-icon-button ${typeProps.className} ${className ?? ''}`}
className={`no-border bg-transparent hover:brightness-130 p-0 ${
className ?? ''
}`}
onClick={click}
>
<Icon type={iconType} />
<Icon type={icon} />
</button>
);
};

View File

@@ -0,0 +1,22 @@
import { FunctionalComponent } from 'preact';
interface Props {
text?: string;
disabled?: boolean;
className?: string;
}
export const Input: FunctionalComponent<Props> = ({
className = '',
disabled = false,
text,
}) => {
const base = `rounded py-1.5 px-3 text-input my-1 h-8`;
const stateClasses = disabled
? 'no-border bg-grey-5'
: 'border-solid border-1 border-gray-300';
const classes = `${base} ${stateClasses} ${className}`;
return (
<input type="text" className={classes} disabled={disabled} value={text} />
);
};

View File

@@ -1,23 +0,0 @@
import { Icon, IconType } from '@/components/Icon';
import { FunctionComponent } from 'preact';
interface PreferencesMenuItemProps {
iconType: IconType;
label: string;
selected: boolean;
onClick: () => void;
}
export const PreferencesMenuItem: FunctionComponent<PreferencesMenuItemProps> =
({ iconType, label, selected, onClick }) => (
<div
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
<Icon className="icon" type={iconType} />
{label}
</div>
);

View File

@@ -0,0 +1,42 @@
import { FunctionComponent } from 'preact';
import { Icon, IconType } from './Icon';
type ButtonType = 'normal' | 'primary';
interface Props {
/**
* onClick - preventDefault is handled within the component
*/
onClick: () => void;
type: ButtonType;
className?: string;
icon: IconType;
}
/**
* IconButton component with an icon
* preventDefault is already handled within the component
*/
export const RoundIconButton: FunctionComponent<Props> = ({
onClick,
type,
className,
icon: iconType,
}) => {
const click = (e: MouseEvent) => {
e.preventDefault();
onClick();
};
const classes = type === 'primary' ? 'info ' : '';
return (
<button
className={`sn-icon-button ${classes} ${className ?? ''}`}
onClick={click}
>
<Icon type={iconType} />
</button>
);
};

View File

@@ -12,7 +12,7 @@ export type SwitchProps = HTMLProps<HTMLInputElement> & {
checked?: boolean;
onChange: (checked: boolean) => void;
className?: string;
children: ComponentChildren;
children?: ComponentChildren;
};
export const Switch: FunctionalComponent<SwitchProps> = (
@@ -22,7 +22,9 @@ export const Switch: FunctionalComponent<SwitchProps> = (
const checked = props.checked ?? checkedState;
const className = props.className ?? '';
return (
<label className={`sn-component flex justify-between items-center cursor-pointer hover:bg-contrast px-3 ${className}`}>
<label
className={`sn-component flex justify-between items-center cursor-pointer hover:bg-contrast px-3 ${className}`}
>
{props.children}
<CustomCheckboxContainer
checked={checked}

View File

@@ -1,22 +0,0 @@
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { toDirective } from '../utils';
import { PreferencesView } from './view';
interface WrapperProps {
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
}
const PreferencesViewWrapper: FunctionComponent<WrapperProps> = observer(
({ appState }) => {
if (!appState.preferences.isOpen) return null;
return (
<PreferencesView close={() => appState.preferences.closePreferences()} />
);
}
);
export const PreferencesDirective = toDirective<WrapperProps>(
PreferencesViewWrapper
);

View File

@@ -1,23 +0,0 @@
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { PreferencesMenuItem } from '../PreferencesMenuItem';
import { Preferences } from './preferences';
interface PreferencesMenuProps {
preferences: Preferences;
}
export const PreferencesMenu: FunctionComponent<PreferencesMenuProps> =
observer(({ preferences }) => (
<div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6">
{preferences.items.map((pref) => (
<PreferencesMenuItem
key={pref.id}
iconType={pref.icon}
label={pref.label}
selected={pref.selected}
onClick={() => preferences.selectItem(pref.id)}
/>
))}
</div>
));

View File

@@ -1,33 +0,0 @@
import { FunctionalComponent } from 'preact';
const HorizontalLine: FunctionalComponent<{ index: number; length: number }> =
({ index, length }) =>
index < length - 1 ? (
<hr className="h-1px w-full bg-border no-border" />
) : null;
export const PreferencesSegment: FunctionalComponent = ({ children }) => (
<div>{children}</div>
);
export const PreferencesGroup: FunctionalComponent = ({ children }) => (
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
{!Array.isArray(children)
? children
: children.map((c, i, arr) => (
<>
{c}
<HorizontalLine index={i} length={arr.length} />
</>
))}
</div>
);
export const PreferencesPane: FunctionalComponent = ({ children }) => (
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
<div className="flex-grow flex flex-col py-6 items-center">
<div className="max-w-124 flex flex-col gap-3">{children}</div>
</div>
<div className="flex-basis-55 flex-shrink-max" />
</div>
);

View File

@@ -1,55 +0,0 @@
import { IconType } from '@/components/Icon';
import { action, computed, makeObservable, observable } from 'mobx';
interface PreferenceItem {
icon: IconType;
label: string;
}
interface PreferenceListItem extends PreferenceItem {
id: number;
}
const predefinedItems: PreferenceItem[] = [
{ label: 'General', icon: 'settings' },
{ label: 'Account', icon: 'user' },
{ label: 'Appearance', icon: 'themes' },
{ label: 'Security', icon: 'security' },
{ label: 'Listed', icon: 'listed' },
{ label: 'Shortcuts', icon: 'keyboard' },
{ label: 'Accessibility', icon: 'accessibility' },
{ label: 'Get a free month', icon: 'star' },
{ label: 'Help & feedback', icon: 'help' },
];
export class Preferences {
private readonly _items: PreferenceListItem[];
private _selectedId = 0;
constructor(items: PreferenceItem[] = predefinedItems) {
makeObservable<Preferences, '_selectedId'>(this, {
_selectedId: observable,
selectedItem: computed,
items: computed,
selectItem: action,
});
this._items = items.map((p, idx) => ({ ...p, id: idx }));
this._selectedId = this._items[0].id;
}
selectItem(id: number) {
this._selectedId = id;
}
get items(): (PreferenceListItem & { selected: boolean })[] {
return this._items.map((p) => ({
...p,
selected: p.id === this._selectedId,
}));
}
get selectedItem(): PreferenceListItem {
return this._items.find((item) => item.id === this._selectedId)!;
}
}

View File

@@ -1,45 +0,0 @@
import { IconButton } from '@/components/IconButton';
import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact';
import { Preferences } from './preferences';
import { PreferencesMenu } from './menu';
import { HelpAndFeedback } from './help-feedback';
import { observer } from 'mobx-react-lite';
interface PreferencesViewProps {
close: () => void;
}
export const PreferencesCanvas: FunctionComponent<{
preferences: Preferences;
}> = observer(({ preferences: prefs }) => (
<div className="flex flex-row flex-grow min-h-0 justify-between">
<PreferencesMenu preferences={prefs}></PreferencesMenu>
{/* Temporary selector until a full solution is implemented */}
{prefs.selectedItem.label === 'Help & feedback' ? (
<HelpAndFeedback />
) : null}
</div>
));
export const PreferencesView: FunctionComponent<PreferencesViewProps> =
observer(({ close }) => {
const prefs = new Preferences();
return (
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
<TitleBar className="items-center justify-between">
{/* div is added so flex justify-between can center the title */}
<div className="h-8 w-8" />
<Title className="text-lg">Your preferences for Standard Notes</Title>
<IconButton
onClick={() => {
close();
}}
type="normal"
iconType="close"
/>
</TitleBar>
<PreferencesCanvas preferences={prefs} />
</div>
);
});

View File

@@ -0,0 +1,19 @@
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { MenuItem } from './components';
import { Preferences } from './models/preferences';
export const PreferencesMenu: FunctionComponent<{ preferences: Preferences }> =
observer(({ preferences }) => (
<div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6">
{preferences.menuItems.map((pref) => (
<MenuItem
key={pref.id}
iconType={pref.icon}
label={pref.label}
selected={pref.selected}
onClick={() => preferences.selectPane(pref.id)}
/>
))}
</div>
));

View File

@@ -0,0 +1,81 @@
import { RoundIconButton } from '@/components/RoundIconButton';
import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact';
import { Preferences } from './models/preferences';
import { PreferencesMenu } from './PreferencesMenu';
import { HelpAndFeedback } from './panes/HelpFeedback';
import { observer } from 'mobx-react-lite';
import { Security } from './panes/Security';
interface PreferencesViewProps {
close: () => void;
}
const PaneSelector: FunctionComponent<{
prefs: Preferences;
}> = observer(({ prefs }) => {
switch (prefs.selectedPaneId) {
case 'general':
return null;
case 'account':
return null;
case 'appearance':
return null;
case 'security':
return <Security prefs={prefs} />;
case 'listed':
return null;
case 'shortcuts':
return null;
case 'accessibility':
return null;
case 'get-free-month':
return null;
case 'help-feedback':
return <HelpAndFeedback />;
}
});
const PreferencesCanvas: FunctionComponent<{
preferences: Preferences;
}> = observer(({ preferences: prefs }) => (
<div className="flex flex-row flex-grow min-h-0 justify-between">
<PreferencesMenu preferences={prefs}></PreferencesMenu>
<PaneSelector prefs={prefs} />
</div>
));
const PreferencesView: FunctionComponent<PreferencesViewProps> = observer(
({ close }) => {
const prefs = new Preferences();
return (
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
<TitleBar className="items-center justify-between">
{/* div is added so flex justify-between can center the title */}
<div className="h-8 w-8" />
<Title className="text-lg">Your preferences for Standard Notes</Title>
<RoundIconButton
onClick={() => {
close();
}}
type="normal"
icon="close"
/>
</TitleBar>
<PreferencesCanvas preferences={prefs} />
</div>
);
}
);
export interface PreferencesWrapperProps {
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
}
export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> =
observer(({ appState }) => {
if (!appState.preferences.isOpen) return null;
return (
<PreferencesView close={() => appState.preferences.closePreferences()} />
);
});

View File

@@ -1,18 +1,18 @@
import { FunctionalComponent } from 'preact';
import { FunctionComponent } from 'preact';
export const Title: FunctionalComponent = ({ children }) => (
<h2 className="text-base m-0 mb-3">{children}</h2>
export const Title: FunctionComponent = ({ children }) => (
<h2 className="text-base m-0 mb-1">{children}</h2>
);
export const Subtitle: FunctionalComponent = ({ children }) => (
export const Subtitle: FunctionComponent = ({ children }) => (
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
);
export const Text: FunctionalComponent = ({ children }) => (
export const Text: FunctionComponent = ({ children }) => (
<p className="text-xs">{children}</p>
);
export const Button: FunctionalComponent<{ label: string; link: string }> = ({
export const Button: FunctionComponent<{ label: string; link: string }> = ({
label,
link,
}) => (

View File

@@ -0,0 +1,27 @@
import { Icon, IconType } from '@/components/Icon';
import { FunctionComponent } from 'preact';
interface Props {
iconType: IconType;
label: string;
selected: boolean;
onClick: () => void;
}
export const MenuItem: FunctionComponent<Props> = ({
iconType,
label,
selected,
onClick,
}) => (
<div
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
<Icon className="icon" type={iconType} />
{label}
</div>
);

View File

@@ -0,0 +1,35 @@
import { FunctionComponent } from 'preact';
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
index,
length,
}) =>
index < length - 1 ? (
<hr className="h-1px w-full bg-border no-border" />
) : null;
export const PreferencesSegment: FunctionComponent = ({ children }) => (
<div className="flex flex-col">{children}</div>
);
export const PreferencesGroup: FunctionComponent = ({ children }) => (
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
{!Array.isArray(children)
? children
: children.map((c, i, arr) => (
<>
{c}
<HorizontalLine index={i} length={arr.length} />
</>
))}
</div>
);
export const PreferencesPane: FunctionComponent = ({ children }) => (
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
<div className="flex-grow flex flex-col py-6 items-center">
<div className="w-125 max-w-125 flex flex-col gap-3">{children}</div>
</div>
<div className="flex-basis-55 flex-shrink" />
</div>
);

View File

@@ -0,0 +1,3 @@
export * from './Content';
export * from './MenuItem';
export * from './Pane';

View File

@@ -0,0 +1,9 @@
import { toDirective } from '../components/utils';
import {
PreferencesViewWrapper,
PreferencesWrapperProps,
} from './PreferencesView';
export const PreferencesDirective = toDirective<PreferencesWrapperProps>(
PreferencesViewWrapper
);

View File

@@ -0,0 +1,2 @@
export * from './preferences';
export * from './two-factor-auth';

View File

@@ -0,0 +1,76 @@
import { IconType } from '@/components/Icon';
import { makeAutoObservable, observable } from 'mobx';
import { TwoFactorAuth } from './two-factor-auth';
const PREFERENCE_IDS = [
'general',
'account',
'appearance',
'security',
'listed',
'shortcuts',
'accessibility',
'get-free-month',
'help-feedback',
] as const;
export type PreferenceId = typeof PREFERENCE_IDS[number];
interface PreferenceMenuItem {
readonly id: PreferenceId;
readonly icon: IconType;
readonly label: string;
}
type PreferencesMenu = PreferenceMenuItem[];
/**
* Items are in order of appearance
*/
const PREFERENCES_MENU: PreferencesMenu = [
{ id: 'general', label: 'General', icon: 'settings' },
{ id: 'account', label: 'Account', icon: 'user' },
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
{ id: 'security', label: 'Security', icon: 'security' },
{ id: 'listed', label: 'Listed', icon: 'listed' },
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard' },
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility' },
{ id: 'get-free-month', label: 'Get a free month', icon: 'star' },
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help' },
];
export class Preferences {
private _selectedPane: PreferenceId = 'general';
private _twoFactorAuth: TwoFactorAuth;
constructor(private readonly _menu: PreferencesMenu = PREFERENCES_MENU) {
this._twoFactorAuth = new TwoFactorAuth();
makeAutoObservable<Preferences, '_selectedPane' | '_twoFactorAuth'>(this, {
_twoFactorAuth: observable,
_selectedPane: observable,
});
}
get menuItems(): (PreferenceMenuItem & {
selected: boolean;
})[] {
return this._menu.map((p) => ({
...p,
selected: p.id === this._selectedPane,
}));
}
get selectedPaneId(): PreferenceId {
return (
this._menu.find((item) => item.id === this._selectedPane)?.id ?? 'general'
);
}
selectPane(key: PreferenceId) {
this._selectedPane = key;
}
get twoFactorAuth() {
return this._twoFactorAuth;
}
}

View File

@@ -0,0 +1,81 @@
import { makeAutoObservable, observable } from 'mobx';
function getNewAuthCode() {
const MIN = 100000;
const MAX = 999999;
const code = Math.floor(Math.random() * (MAX - MIN) + MIN);
return code.toString();
}
class TwoFactorData {
private _secretKey: string;
private _authCode: string;
constructor(secretKey: string) {
this._secretKey = secretKey;
this._authCode = getNewAuthCode();
makeAutoObservable<TwoFactorData, '_secretKey' | '_authCode'>(
this,
{
_secretKey: observable,
_authCode: observable,
},
{ autoBind: true }
);
}
get secretKey() {
return this._secretKey;
}
get authCode() {
return this._authCode;
}
refreshAuthCode() {
this._authCode = getNewAuthCode();
}
}
type TwoFactorStatus = 'enabled' | 'disabled';
export class TwoFactorAuth {
private _twoFactorStatus: TwoFactorStatus = 'disabled';
private _twoFactorData: TwoFactorData | null = null;
constructor() {
makeAutoObservable<TwoFactorAuth, '_twoFactorStatus' | '_twoFactorData'>(
this,
{
_twoFactorStatus: observable,
_twoFactorData: observable,
},
{ autoBind: true }
);
}
private activate2FA() {
this._twoFactorData = new TwoFactorData('FHJJSAJKDASKW43KJS');
this._twoFactorStatus = 'enabled';
}
private deactivate2FA() {
this._twoFactorData = null;
this._twoFactorStatus = 'disabled';
}
toggle2FA() {
if (this._twoFactorStatus === 'enabled') this.deactivate2FA();
else this.activate2FA();
}
get twoFactorStatus() {
return this._twoFactorStatus;
}
get twoFactorData() {
if (this._twoFactorStatus !== 'enabled')
throw new Error(`Can't provide 2FA data if not enabled`);
return this._twoFactorData;
}
}

View File

@@ -1,12 +1,21 @@
import { FunctionalComponent } from 'preact';
import { PreferencesGroup, PreferencesPane, PreferencesSegment } from './pane';
import { Title, Subtitle, Text, Button } from './content';
import { FunctionComponent } from 'preact';
import {} from '../components';
import {
Title,
Subtitle,
Text,
Button,
PreferencesGroup,
PreferencesPane,
PreferencesSegment,
} from '../components';
export const HelpAndFeedback: FunctionalComponent = () => (
export const HelpAndFeedback: FunctionComponent = () => (
<PreferencesPane>
<PreferencesGroup>
<PreferencesSegment>
<Title>Frequently asked questions</Title>
<div className="h-2 w-full" />
<Subtitle>Who can read my private notes?</Subtitle>
<Text>
Quite simply: no one but you. Not us, not your ISP, not a hacker, and

View File

@@ -0,0 +1,13 @@
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { PreferencesPane } from '../components';
import { Preferences } from '../models';
import { TwoFactorAuthComponent } from './TwoFactorAuth';
export const Security: FunctionComponent<{ prefs: Preferences }> = observer(
({ prefs }) => (
<PreferencesPane>
<TwoFactorAuthComponent tfAuth={prefs.twoFactorAuth} />
</PreferencesPane>
)
);

View File

@@ -0,0 +1,110 @@
import { FunctionComponent } from 'preact';
import {
Title,
Text,
PreferencesGroup,
PreferencesSegment,
} from '../components';
import { Switch } from '../../components/Switch';
import { observer } from 'mobx-react-lite';
import { DecoratedInput } from '../../components/DecoratedInput';
import { IconButton } from '../../components/IconButton';
import { TwoFactorAuth } from '../models';
// Temporary implementation until integration
function downloadSecretKey(text: string) {
const link = document.createElement('a');
const blob = new Blob([text], {
type: 'text/plain;charset=utf-8',
});
link.href = window.URL.createObjectURL(blob);
link.setAttribute('download', 'secret_key.txt');
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(link.href);
}
export const TwoFactorAuthComponent: FunctionComponent<{
tfAuth: TwoFactorAuth;
}> = observer(({ tfAuth }) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<Title>Two-factor authentication</Title>
<Text>
An extra layer of security when logging in to your account.
</Text>
</div>
<Switch
checked={tfAuth.twoFactorStatus === 'enabled'}
onChange={() => tfAuth.toggle2FA()}
/>
</div>
</PreferencesSegment>
<PreferencesSegment>
{tfAuth.twoFactorStatus === 'enabled' &&
tfAuth.twoFactorData != null ? (
<TwoFactorEnabled tfAuth={tfAuth} />
) : (
<TwoFactorDisabled />
)}
</PreferencesSegment>
</PreferencesGroup>
);
});
const TwoFactorEnabled: FunctionComponent<{ tfAuth: TwoFactorAuth }> = observer(
({ tfAuth }) => {
const state = tfAuth.twoFactorData!;
const download = (
<IconButton
icon="download"
onClick={() => {
downloadSecretKey(state.secretKey);
}}
/>
);
const copy = (
<IconButton
icon="copy"
onClick={() => {
navigator?.clipboard?.writeText(state.secretKey);
}}
/>
);
const spinner = <div class="sk-spinner info w-8 h-3.5" />;
return (
<div className="flex flex-row gap-4">
<div className="flex-grow flex flex-col">
<Text>Secret Key</Text>
<DecoratedInput
disabled={true}
right={[copy, download]}
text={state.secretKey}
/>
</div>
<div className="w-30 flex flex-col">
<Text>Authentication Code</Text>
<DecoratedInput
disabled={true}
text={state.authCode}
right={[spinner]}
/>
</div>
</div>
);
}
);
const TwoFactorDisabled: FunctionComponent = () => (
<Text>
Enabling two-factor authentication will sign you out of all other sessions.{' '}
<a
target="_blank"
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
>
Learn more
</a>
</Text>
);

View File

@@ -0,0 +1,2 @@
export * from './HelpFeedback';
export * from './Security';

View File

@@ -56,7 +56,8 @@ export function StringEmptyTrash(count: number) {
/** @account */
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE =
'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?';
export const STRING_SIGN_OUT_CONFIRMATION = 'This will delete all local items and extensions.';
export const STRING_SIGN_OUT_CONFIRMATION =
'This will delete all local items and extensions.';
export const STRING_ERROR_DECRYPTING_IMPORT =
'There was an error decrypting your items. Make sure the password you entered is correct and try again.';
export const STRING_E2E_ENABLED =
@@ -111,7 +112,8 @@ export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade';
export const Strings = {
protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.',
protectingNoteWithoutProtectionSources:
'Access to this note will not be restricted until you set up a passcode or account.',
openAccountMenu: 'Open Account Menu',
trashNotesTitle: 'Move to Trash',
trashNotesText: 'Are you sure you want to move these notes to the trash?',
@@ -157,4 +159,4 @@ export const StringUtils = {
? "This note has editing disabled. If you'd like to delete it, enable editing, and try again."
: "One or more of these notes have editing disabled. If you'd like to delete them, make sure editing is enabled on all of them, and try again.";
},
};
};