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

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66724 3.66626C1.66724 2.56169 2.56267 1.66626 3.66724 1.66626H11.3339C12.4385 1.66626 13.3339 2.56169 13.3339 3.66626V13.3329H3.66724C2.56267 13.3329 1.66724 12.4375 1.66724 11.3329V3.66626ZM16.3339 6.66626C17.4385 6.66626 18.3339 7.56169 18.3339 8.66626V16.3329C18.3339 17.4375 17.4385 18.3329 16.3339 18.3329H8.66724C7.56267 18.3329 6.66724 17.4375 6.66724 16.3329V14.9996H15.0006V6.66626H16.3339ZM3.3339 3.33293V11.6663H11.6672V3.33293H3.3339Z" fill="#72767E"/>
</svg>

After

Width:  |  Height:  |  Size: 580 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 3.5V9.5H12.17L10 11.67L7.83 9.5H9V3.5H11ZM13 1.5H7V7.5H3L10 14.5L17 7.5H13V1.5ZM17 16.5H3V18.5H17V16.5Z" fill="#72767E"/>
</svg>

After

Width:  |  Height:  |  Size: 238 B

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.";
},
};
};

View File

@@ -55,7 +55,7 @@
"pug-loader": "^2.4.0",
"sass-loader": "^8.0.2",
"serve-static": "^1.14.1",
"sn-stylekit": "5.2.5",
"sn-stylekit": "5.2.6",
"ts-loader": "^8.0.17",
"typescript": "4.2.3",
"typescript-eslint": "0.0.1-alpha.0",

View File

@@ -7908,10 +7908,10 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
sn-stylekit@5.2.5:
version "5.2.5"
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.5.tgz#85a28da395fedbaae9f7a91c48648042cdcc8052"
integrity sha512-8J+8UtRvukyJOBp79RcD4IZrvJJbjYY6EdN4N125K0xW84nDjgURuPuCjwm4lnp6vcXODU6r5d3JMDJoXYq8wA==
sn-stylekit@5.2.6:
version "5.2.6"
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.6.tgz#3c63d4a6b20bb002b6add685e28abce4663475d2"
integrity sha512-jZI+D/evLwcJjNbxBueoczjEOjJZL61eGd4cluC0lpPYvkMwHGYj/gWi09KN1E16K++FQRNVvZ3/mMZjSfam2g==
dependencies:
"@reach/listbox" "^0.15.0"
"@reach/menu-button" "^0.15.1"