diff --git a/app/assets/icons/ic-copy.svg b/app/assets/icons/ic-copy.svg new file mode 100644 index 000000000..9ad40e8f1 --- /dev/null +++ b/app/assets/icons/ic-copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-download.svg b/app/assets/icons/ic-download.svg new file mode 100644 index 000000000..de2c70fc2 --- /dev/null +++ b/app/assets/icons/ic-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index e7470bbe0..b493808ad 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -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 { /** diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx new file mode 100644 index 000000000..860b6b362 --- /dev/null +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -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 = ({ + 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 ( + + {left} + + + + {right} + + ); +}; diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index e9c9a8b1e..c6f9f632c 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -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 = ({ type, className }) => { +export const Icon: FunctionalComponent = ({ type, className = '' }) => { const IconComponent = ICONS[type]; return ; }; diff --git a/app/assets/javascripts/components/IconButton.tsx b/app/assets/javascripts/components/IconButton.tsx index 93d826c4e..8c0920c64 100644 --- a/app/assets/javascripts/components/IconButton.tsx +++ b/app/assets/javascripts/components/IconButton.tsx @@ -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 = ({ +export const IconButton: FunctionComponent = ({ onClick, - type, className, - iconType, + icon, }) => { const click = (e: MouseEvent) => { e.preventDefault(); onClick(); }; - const typeProps = ICON_BUTTON_TYPES[type]; return ( - + ); }; diff --git a/app/assets/javascripts/components/Input.tsx b/app/assets/javascripts/components/Input.tsx new file mode 100644 index 000000000..0955b632c --- /dev/null +++ b/app/assets/javascripts/components/Input.tsx @@ -0,0 +1,22 @@ +import { FunctionalComponent } from 'preact'; + +interface Props { + text?: string; + disabled?: boolean; + className?: string; +} + +export const Input: FunctionalComponent = ({ + 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 ( + + ); +}; diff --git a/app/assets/javascripts/components/PreferencesMenuItem.tsx b/app/assets/javascripts/components/PreferencesMenuItem.tsx deleted file mode 100644 index eb872053a..000000000 --- a/app/assets/javascripts/components/PreferencesMenuItem.tsx +++ /dev/null @@ -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 = - ({ iconType, label, selected, onClick }) => ( - { - e.preventDefault(); - onClick(); - }} - > - - {label} - - ); diff --git a/app/assets/javascripts/components/RoundIconButton.tsx b/app/assets/javascripts/components/RoundIconButton.tsx new file mode 100644 index 000000000..d143fd4e2 --- /dev/null +++ b/app/assets/javascripts/components/RoundIconButton.tsx @@ -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 = ({ + onClick, + type, + className, + icon: iconType, +}) => { + const click = (e: MouseEvent) => { + e.preventDefault(); + onClick(); + }; + const classes = type === 'primary' ? 'info ' : ''; + return ( + + + + ); +}; diff --git a/app/assets/javascripts/components/Switch.tsx b/app/assets/javascripts/components/Switch.tsx index c504df77c..0a35c2a74 100644 --- a/app/assets/javascripts/components/Switch.tsx +++ b/app/assets/javascripts/components/Switch.tsx @@ -12,7 +12,7 @@ export type SwitchProps = HTMLProps & { checked?: boolean; onChange: (checked: boolean) => void; className?: string; - children: ComponentChildren; + children?: ComponentChildren; }; export const Switch: FunctionalComponent = ( @@ -22,7 +22,9 @@ export const Switch: FunctionalComponent = ( const checked = props.checked ?? checkedState; const className = props.className ?? ''; return ( - + {props.children} void } }; -} - -const PreferencesViewWrapper: FunctionComponent = observer( - ({ appState }) => { - if (!appState.preferences.isOpen) return null; - return ( - appState.preferences.closePreferences()} /> - ); - } -); - -export const PreferencesDirective = toDirective( - PreferencesViewWrapper -); diff --git a/app/assets/javascripts/components/preferences/menu.tsx b/app/assets/javascripts/components/preferences/menu.tsx deleted file mode 100644 index 274ff36a2..000000000 --- a/app/assets/javascripts/components/preferences/menu.tsx +++ /dev/null @@ -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 = - observer(({ preferences }) => ( - - {preferences.items.map((pref) => ( - preferences.selectItem(pref.id)} - /> - ))} - - )); diff --git a/app/assets/javascripts/components/preferences/pane.tsx b/app/assets/javascripts/components/preferences/pane.tsx deleted file mode 100644 index 4f6fbd73d..000000000 --- a/app/assets/javascripts/components/preferences/pane.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FunctionalComponent } from 'preact'; - -const HorizontalLine: FunctionalComponent<{ index: number; length: number }> = - ({ index, length }) => - index < length - 1 ? ( - - ) : null; - -export const PreferencesSegment: FunctionalComponent = ({ children }) => ( - {children} -); - -export const PreferencesGroup: FunctionalComponent = ({ children }) => ( - - {!Array.isArray(children) - ? children - : children.map((c, i, arr) => ( - <> - {c} - - > - ))} - -); - -export const PreferencesPane: FunctionalComponent = ({ children }) => ( - - - {children} - - - -); diff --git a/app/assets/javascripts/components/preferences/preferences.ts b/app/assets/javascripts/components/preferences/preferences.ts deleted file mode 100644 index 3a7407d4a..000000000 --- a/app/assets/javascripts/components/preferences/preferences.ts +++ /dev/null @@ -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(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)!; - } -} diff --git a/app/assets/javascripts/components/preferences/view.tsx b/app/assets/javascripts/components/preferences/view.tsx deleted file mode 100644 index 9b2f8b854..000000000 --- a/app/assets/javascripts/components/preferences/view.tsx +++ /dev/null @@ -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 }) => ( - - - {/* Temporary selector until a full solution is implemented */} - {prefs.selectedItem.label === 'Help & feedback' ? ( - - ) : null} - -)); - -export const PreferencesView: FunctionComponent = - observer(({ close }) => { - const prefs = new Preferences(); - return ( - - - {/* div is added so flex justify-between can center the title */} - - Your preferences for Standard Notes - { - close(); - }} - type="normal" - iconType="close" - /> - - - - ); - }); diff --git a/app/assets/javascripts/preferences/PreferencesMenu.tsx b/app/assets/javascripts/preferences/PreferencesMenu.tsx new file mode 100644 index 000000000..8e74450e2 --- /dev/null +++ b/app/assets/javascripts/preferences/PreferencesMenu.tsx @@ -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 }) => ( + + {preferences.menuItems.map((pref) => ( + preferences.selectPane(pref.id)} + /> + ))} + + )); diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx new file mode 100644 index 000000000..423017cde --- /dev/null +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -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 ; + case 'listed': + return null; + case 'shortcuts': + return null; + case 'accessibility': + return null; + case 'get-free-month': + return null; + case 'help-feedback': + return ; + } +}); + +const PreferencesCanvas: FunctionComponent<{ + preferences: Preferences; +}> = observer(({ preferences: prefs }) => ( + + + + +)); + +const PreferencesView: FunctionComponent = observer( + ({ close }) => { + const prefs = new Preferences(); + return ( + + + {/* div is added so flex justify-between can center the title */} + + Your preferences for Standard Notes + { + close(); + }} + type="normal" + icon="close" + /> + + + + ); + } +); + +export interface PreferencesWrapperProps { + appState: { preferences: { isOpen: boolean; closePreferences: () => void } }; +} + +export const PreferencesViewWrapper: FunctionComponent = + observer(({ appState }) => { + if (!appState.preferences.isOpen) return null; + return ( + appState.preferences.closePreferences()} /> + ); + }); diff --git a/app/assets/javascripts/components/preferences/content.tsx b/app/assets/javascripts/preferences/components/Content.tsx similarity index 51% rename from app/assets/javascripts/components/preferences/content.tsx rename to app/assets/javascripts/preferences/components/Content.tsx index efdaa7109..cbc71ef4b 100644 --- a/app/assets/javascripts/components/preferences/content.tsx +++ b/app/assets/javascripts/preferences/components/Content.tsx @@ -1,18 +1,18 @@ -import { FunctionalComponent } from 'preact'; +import { FunctionComponent } from 'preact'; -export const Title: FunctionalComponent = ({ children }) => ( - {children} +export const Title: FunctionComponent = ({ children }) => ( + {children} ); -export const Subtitle: FunctionalComponent = ({ children }) => ( +export const Subtitle: FunctionComponent = ({ children }) => ( {children} ); -export const Text: FunctionalComponent = ({ children }) => ( +export const Text: FunctionComponent = ({ children }) => ( {children} ); -export const Button: FunctionalComponent<{ label: string; link: string }> = ({ +export const Button: FunctionComponent<{ label: string; link: string }> = ({ label, link, }) => ( diff --git a/app/assets/javascripts/preferences/components/MenuItem.tsx b/app/assets/javascripts/preferences/components/MenuItem.tsx new file mode 100644 index 000000000..d1f195f94 --- /dev/null +++ b/app/assets/javascripts/preferences/components/MenuItem.tsx @@ -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 = ({ + iconType, + label, + selected, + onClick, +}) => ( + { + e.preventDefault(); + onClick(); + }} + > + + {label} + +); diff --git a/app/assets/javascripts/preferences/components/Pane.tsx b/app/assets/javascripts/preferences/components/Pane.tsx new file mode 100644 index 000000000..ab2638f03 --- /dev/null +++ b/app/assets/javascripts/preferences/components/Pane.tsx @@ -0,0 +1,35 @@ +import { FunctionComponent } from 'preact'; + +const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({ + index, + length, +}) => + index < length - 1 ? ( + + ) : null; + +export const PreferencesSegment: FunctionComponent = ({ children }) => ( + {children} +); + +export const PreferencesGroup: FunctionComponent = ({ children }) => ( + + {!Array.isArray(children) + ? children + : children.map((c, i, arr) => ( + <> + {c} + + > + ))} + +); + +export const PreferencesPane: FunctionComponent = ({ children }) => ( + + + {children} + + + +); diff --git a/app/assets/javascripts/preferences/components/index.ts b/app/assets/javascripts/preferences/components/index.ts new file mode 100644 index 000000000..ab868c871 --- /dev/null +++ b/app/assets/javascripts/preferences/components/index.ts @@ -0,0 +1,3 @@ +export * from './Content'; +export * from './MenuItem'; +export * from './Pane'; diff --git a/app/assets/javascripts/preferences/index.ts b/app/assets/javascripts/preferences/index.ts new file mode 100644 index 000000000..8849e4b8c --- /dev/null +++ b/app/assets/javascripts/preferences/index.ts @@ -0,0 +1,9 @@ +import { toDirective } from '../components/utils'; +import { + PreferencesViewWrapper, + PreferencesWrapperProps, +} from './PreferencesView'; + +export const PreferencesDirective = toDirective( + PreferencesViewWrapper +); diff --git a/app/assets/javascripts/preferences/models/index.ts b/app/assets/javascripts/preferences/models/index.ts new file mode 100644 index 000000000..eee27f7f4 --- /dev/null +++ b/app/assets/javascripts/preferences/models/index.ts @@ -0,0 +1,2 @@ +export * from './preferences'; +export * from './two-factor-auth'; diff --git a/app/assets/javascripts/preferences/models/preferences.ts b/app/assets/javascripts/preferences/models/preferences.ts new file mode 100644 index 000000000..2402931cf --- /dev/null +++ b/app/assets/javascripts/preferences/models/preferences.ts @@ -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(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; + } +} diff --git a/app/assets/javascripts/preferences/models/two-factor-auth.ts b/app/assets/javascripts/preferences/models/two-factor-auth.ts new file mode 100644 index 000000000..c3b2503b9 --- /dev/null +++ b/app/assets/javascripts/preferences/models/two-factor-auth.ts @@ -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( + 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( + 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; + } +} diff --git a/app/assets/javascripts/components/preferences/help-feedback.tsx b/app/assets/javascripts/preferences/panes/HelpFeedback.tsx similarity index 91% rename from app/assets/javascripts/components/preferences/help-feedback.tsx rename to app/assets/javascripts/preferences/panes/HelpFeedback.tsx index 0f9ead7a9..231304cb2 100644 --- a/app/assets/javascripts/components/preferences/help-feedback.tsx +++ b/app/assets/javascripts/preferences/panes/HelpFeedback.tsx @@ -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 = () => ( Frequently asked questions + Who can read my private notes? Quite simply: no one but you. Not us, not your ISP, not a hacker, and diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx new file mode 100644 index 000000000..b026564a0 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -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 }) => ( + + + + ) +); diff --git a/app/assets/javascripts/preferences/panes/TwoFactorAuth.tsx b/app/assets/javascripts/preferences/panes/TwoFactorAuth.tsx new file mode 100644 index 000000000..f360dc680 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/TwoFactorAuth.tsx @@ -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 ( + + + + + Two-factor authentication + + An extra layer of security when logging in to your account. + + + tfAuth.toggle2FA()} + /> + + + + {tfAuth.twoFactorStatus === 'enabled' && + tfAuth.twoFactorData != null ? ( + + ) : ( + + )} + + + ); +}); + +const TwoFactorEnabled: FunctionComponent<{ tfAuth: TwoFactorAuth }> = observer( + ({ tfAuth }) => { + const state = tfAuth.twoFactorData!; + const download = ( + { + downloadSecretKey(state.secretKey); + }} + /> + ); + const copy = ( + { + navigator?.clipboard?.writeText(state.secretKey); + }} + /> + ); + const spinner = ; + return ( + + + Secret Key + + + + Authentication Code + + + + ); + } +); +const TwoFactorDisabled: FunctionComponent = () => ( + + Enabling two-factor authentication will sign you out of all other sessions.{' '} + + Learn more + + +); diff --git a/app/assets/javascripts/preferences/panes/index.ts b/app/assets/javascripts/preferences/panes/index.ts new file mode 100644 index 000000000..f176e8116 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/index.ts @@ -0,0 +1,2 @@ +export * from './HelpFeedback'; +export * from './Security'; diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index 7b24dc66d..dab4117e3 100644 --- a/app/assets/javascripts/strings.ts +++ b/app/assets/javascripts/strings.ts @@ -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."; }, -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index e1882c820..a11b36d5e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index bf79c098b..6aa9440a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"
{children}