refactor: repo (#1070)

This commit is contained in:
Mo
2022-06-07 07:18:41 -05:00
committed by GitHub
parent 4c65784421
commit f4ef63693c
1102 changed files with 5786 additions and 3366 deletions

View File

@@ -0,0 +1,36 @@
import { STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED } from '@/Constants/Strings'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import { Title, Text } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import EncryptionEnabled from './EncryptionEnabled'
type Props = { viewControllerManager: ViewControllerManager }
const Encryption: FunctionComponent<Props> = ({ viewControllerManager }) => {
const app = viewControllerManager.application
const hasUser = app.hasAccount()
const hasPasscode = app.hasPasscode()
const isEncryptionEnabled = app.isEncryptionAvailable()
const encryptionStatusString = hasUser
? STRING_E2E_ENABLED
: hasPasscode
? STRING_LOCAL_ENC_ENABLED
: STRING_ENC_NOT_ENABLED
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Encryption</Title>
<Text>{encryptionStatusString}</Text>
{isEncryptionEnabled && <EncryptionEnabled viewControllerManager={viewControllerManager} />}
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(Encryption)

View File

@@ -0,0 +1,39 @@
import Icon from '@/Components/Icon/Icon'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import EncryptionStatusItem from './EncryptionStatusItem'
import { formatCount } from './formatCount'
type Props = {
viewControllerManager: ViewControllerManager
}
const EncryptionEnabled: FunctionComponent<Props> = ({ viewControllerManager }) => {
const count = viewControllerManager.accountMenuController.structuredNotesAndTagsCount
const notes = formatCount(count.notes, 'notes')
const tags = formatCount(count.tags, 'tags')
const archived = formatCount(count.archived, 'archived notes')
const deleted = formatCount(count.deleted, 'trashed notes')
const noteIcon = <Icon type="rich-text" className="min-w-5 min-h-5" />
const tagIcon = <Icon type="hashtag" className="min-w-5 min-h-5" />
const archiveIcon = <Icon type="archive" className="min-w-5 min-h-5" />
const trashIcon = <Icon type="trash" className="min-w-5 min-h-5" />
return (
<>
<div className="flex flex-row items-start pb-1 pt-1.5">
<EncryptionStatusItem status={notes} icon={noteIcon} />
<div className="min-w-3" />
<EncryptionStatusItem status={tags} icon={tagIcon} />
</div>
<div className="flex flex-row items-start">
<EncryptionStatusItem status={archived} icon={archiveIcon} />
<div className="min-w-3" />
<EncryptionStatusItem status={deleted} icon={trashIcon} />
</div>
</>
)
}
export default observer(EncryptionEnabled)

View File

@@ -0,0 +1,20 @@
import Icon from '@/Components/Icon/Icon'
import { FunctionComponent, ReactNode } from 'react'
type Props = {
icon: ReactNode
status: string
checkmark?: boolean
}
const EncryptionStatusItem: FunctionComponent<Props> = ({ icon, status, checkmark = true }) => (
<div className="w-full rounded py-1.5 px-3 text-input my-1 min-h-8 flex flex-row items-center bg-contrast no-border focus-within:ring-info">
{icon}
<div className="min-w-3 min-h-1" />
<div className="flex-grow color-text text-sm">{status}</div>
<div className="min-w-3 min-h-1" />
{checkmark && <Icon className="success min-w-4 min-h-4" type="check-bold" />}
</div>
)
export default EncryptionStatusItem

View File

@@ -0,0 +1,140 @@
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { Fragment, FunctionComponent, useState } from 'react'
import { Text, Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import {
ButtonType,
ClientDisplayableError,
DisplayStringForContentType,
EncryptedItemInterface,
} from '@standardnotes/snjs'
import Button from '@/Components/Button/Button'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
type Props = { viewControllerManager: ViewControllerManager }
const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
const app = viewControllerManager.application
const [erroredItems, setErroredItems] = useState(app.items.invalidItems)
const getContentTypeDisplay = (item: EncryptedItemInterface): string => {
const display = DisplayStringForContentType(item.content_type)
if (display) {
return `${display[0].toUpperCase()}${display.slice(1)}`
} else {
return `Item of type ${item.content_type}`
}
}
const deleteItem = async (item: EncryptedItemInterface): Promise<void> => {
return deleteItems([item])
}
const deleteItems = async (items: EncryptedItemInterface[]): Promise<void> => {
const confirmed = await app.alertService.confirm(
`Are you sure you want to permanently delete ${items.length} item(s)?`,
undefined,
'Delete',
ButtonType.Danger,
)
if (!confirmed) {
return
}
void app.mutator.deleteItems(items)
setErroredItems(app.items.invalidItems)
}
const attemptDecryption = (item: EncryptedItemInterface): void => {
const errorOrTrue = app.canAttemptDecryptionOfItem(item)
if (errorOrTrue instanceof ClientDisplayableError) {
void app.alertService.showErrorAlert(errorOrTrue)
return
}
app.presentKeyRecoveryWizard()
}
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>
Error Decrypting Items <span className="ml-1 color-warning"></span>
</Title>
<Text>{`${erroredItems.length} items are errored and could not be decrypted.`}</Text>
<div className="flex">
<Button
className="min-w-20 mt-3 mr-2"
variant="normal"
label="Export all"
onClick={() => {
void app.getArchiveService().downloadEncryptedItems(erroredItems)
}}
/>
<Button
className="min-w-20 mt-3 mr-2"
variant="normal"
dangerStyle={true}
label="Delete all"
onClick={() => {
void deleteItems(erroredItems)
}}
/>
</div>
<HorizontalSeparator classes="mt-2.5 mb-3" />
{erroredItems.map((item, index) => {
return (
<Fragment key={item.uuid}>
<div className="flex items-center justify-between">
<div className="flex flex-col">
<Subtitle>{`${getContentTypeDisplay(item)} created on ${item.createdAtString}`}</Subtitle>
<Text>
<div>Item ID: {item.uuid}</div>
<div>Last Modified: {item.updatedAtString}</div>
</Text>
<div className="flex">
<Button
className="min-w-20 mt-3 mr-2"
variant="normal"
label="Attempt decryption"
onClick={() => {
attemptDecryption(item)
}}
/>
<Button
className="min-w-20 mt-3 mr-2"
variant="normal"
label="Export"
onClick={() => {
void app.getArchiveService().downloadEncryptedItem(item)
}}
/>
<Button
className="min-w-20 mt-3 mr-2"
variant="normal"
dangerStyle={true}
label="Delete"
onClick={() => {
void deleteItem(item)
}}
/>
</div>
</div>
</div>
{index < erroredItems.length - 1 && <HorizontalSeparator classes="mt-2.5 mb-3" />}
</Fragment>
)
})}
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(ErroredItems)

View File

@@ -0,0 +1,263 @@
import {
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
STRING_E2E_ENABLED,
STRING_ENC_NOT_ENABLED,
STRING_LOCAL_ENC_ENABLED,
STRING_NON_MATCHING_PASSCODES,
StringUtils,
Strings,
} from '@/Constants/Strings'
import { WebApplication } from '@/Application/Application'
import { preventRefreshing } from '@/Utils'
import { alertDialog } from '@/Services/AlertService'
import { ChangeEventHandler, FormEvent, useCallback, useEffect, useRef, useState } from 'react'
import { ApplicationEvent } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
import Button from '@/Components/Button/Button'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const PasscodeLock = ({ application, viewControllerManager }: Props) => {
const keyStorageInfo = StringUtils.keyStorageInfo(application)
const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions()
const { setIsEncryptionEnabled, setIsBackupEncrypted, setEncryptionStatusString } =
viewControllerManager.accountMenuController
const passcodeInputRef = useRef<HTMLInputElement>(null)
const [passcode, setPasscode] = useState<string>()
const [passcodeConfirmation, setPasscodeConfirmation] = useState<string>()
const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState<unknown>(null)
const [isPasscodeFocused, setIsPasscodeFocused] = useState(false)
const [showPasscodeForm, setShowPasscodeForm] = useState(false)
const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession())
const [hasPasscode, setHasPasscode] = useState(application.hasPasscode())
const handleAddPassCode = () => {
setShowPasscodeForm(true)
setIsPasscodeFocused(true)
}
const changePasscodePressed = () => {
handleAddPassCode()
}
const reloadAutoLockInterval = useCallback(async () => {
const interval = await application.getAutolockService().getAutoLockInterval()
setSelectedAutoLockInterval(interval)
}, [application])
const refreshEncryptionStatus = useCallback(() => {
const hasUser = application.hasAccount()
const hasPasscode = application.hasPasscode()
setHasPasscode(hasPasscode)
const encryptionEnabled = hasUser || hasPasscode
const encryptionStatusString = hasUser
? STRING_E2E_ENABLED
: hasPasscode
? STRING_LOCAL_ENC_ENABLED
: STRING_ENC_NOT_ENABLED
setEncryptionStatusString(encryptionStatusString)
setIsEncryptionEnabled(encryptionEnabled)
setIsBackupEncrypted(encryptionEnabled)
}, [application, setEncryptionStatusString, setIsBackupEncrypted, setIsEncryptionEnabled])
const selectAutoLockInterval = async (interval: number) => {
if (!(await application.authorizeAutolockIntervalChange())) {
return
}
await application.getAutolockService().setAutoLockInterval(interval)
reloadAutoLockInterval().catch(console.error)
}
const removePasscodePressed = async () => {
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => {
if (await application.removePasscode()) {
await application.getAutolockService().deleteAutolockPreference()
await reloadAutoLockInterval()
refreshEncryptionStatus()
}
})
}
const handlePasscodeChange: ChangeEventHandler<HTMLInputElement> = (event) => {
const { value } = event.target
setPasscode(value)
}
const handleConfirmPasscodeChange: ChangeEventHandler<HTMLInputElement> = (event) => {
const { value } = event.target
setPasscodeConfirmation(value)
}
const submitPasscodeForm = async (event: MouseEvent | FormEvent) => {
event.preventDefault()
if (!passcode || passcode.length === 0) {
await alertDialog({
text: Strings.enterPasscode,
})
}
if (passcode !== passcodeConfirmation) {
await alertDialog({
text: STRING_NON_MATCHING_PASSCODES,
})
setIsPasscodeFocused(true)
return
}
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => {
const successful = application.hasPasscode()
? await application.changePasscode(passcode as string)
: await application.addPasscode(passcode as string)
if (!successful) {
setIsPasscodeFocused(true)
}
})
setPasscode(undefined)
setPasscodeConfirmation(undefined)
setShowPasscodeForm(false)
refreshEncryptionStatus()
}
useEffect(() => {
refreshEncryptionStatus()
}, [refreshEncryptionStatus])
// `reloadAutoLockInterval` gets interval asynchronously, therefore we call `useEffect` to set initial
// value of `selectedAutoLockInterval`
useEffect(() => {
reloadAutoLockInterval().catch(console.error)
}, [reloadAutoLockInterval])
useEffect(() => {
if (isPasscodeFocused) {
passcodeInputRef.current?.focus()
setIsPasscodeFocused(false)
}
}, [isPasscodeFocused])
// Add the required event observers
useEffect(() => {
const removeKeyStatusChangedObserver = application.addEventObserver(async () => {
setCanAddPasscode(!application.isEphemeralSession())
setHasPasscode(application.hasPasscode())
setShowPasscodeForm(false)
}, ApplicationEvent.KeyStatusChanged)
return () => {
removeKeyStatusChangedObserver()
}
}, [application])
const cancelPasscodeForm = () => {
setShowPasscodeForm(false)
setPasscode(undefined)
setPasscodeConfirmation(undefined)
}
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Passcode Lock</Title>
{!hasPasscode && canAddPasscode && (
<>
<Text className="mb-3">Add a passcode to lock the application and encrypt on-device key storage.</Text>
{keyStorageInfo && <Text className="mb-3">{keyStorageInfo}</Text>}
{!showPasscodeForm && <Button label="Add passcode" onClick={handleAddPassCode} variant="primary" />}
</>
)}
{!hasPasscode && !canAddPasscode && (
<Text>
Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the
"Stay signed in" option checked.
</Text>
)}
{showPasscodeForm && (
<form className="sk-panel-form" onSubmit={submitPasscodeForm}>
<div className="sk-panel-row" />
<input
className="sk-input contrast"
type="password"
ref={passcodeInputRef}
value={passcode ? passcode : ''}
onChange={handlePasscodeChange}
placeholder="Passcode"
/>
<input
className="sk-input contrast"
type="password"
value={passcodeConfirmation ? passcodeConfirmation : ''}
onChange={handleConfirmPasscodeChange}
placeholder="Confirm Passcode"
/>
<div className="min-h-2" />
<Button variant="primary" onClick={submitPasscodeForm} label="Set Passcode" className="mr-3" />
<Button variant="normal" onClick={cancelPasscodeForm} label="Cancel" />
</form>
)}
{hasPasscode && !showPasscodeForm && (
<>
<Text>Passcode lock is enabled.</Text>
<div className="flex flex-row mt-3">
<Button variant="normal" label="Change Passcode" onClick={changePasscodePressed} className="mr-3" />
<Button dangerStyle={true} label="Remove Passcode" onClick={removePasscodePressed} />
</div>
</>
)}
</PreferencesSegment>
</PreferencesGroup>
{hasPasscode && (
<>
<div className="min-h-3" />
<PreferencesGroup>
<PreferencesSegment>
<Title>Autolock</Title>
<Text className="mb-3">The autolock timer begins when the window or tab loses focus.</Text>
<div className="flex flex-row items-center">
{passcodeAutoLockOptions.map((option) => {
return (
<a
key={option.value}
className={`sk-a info mr-3 ${option.value === selectedAutoLockInterval ? 'boxed' : ''}`}
onClick={() => selectAutoLockInterval(option.value)}
>
{option.label}
</a>
)
})}
</div>
</PreferencesSegment>
</PreferencesGroup>
</>
)}
</>
)
}
export default observer(PasscodeLock)

View File

@@ -0,0 +1,141 @@
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch'
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/Application'
import { MuteSignInEmailsOption, LogSessionUserAgentOption, SettingName } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Constants/Strings'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
}
const Privacy: FunctionComponent<Props> = ({ application }: Props) => {
const [signInEmailsMutedValue, setSignInEmailsMutedValue] = useState<MuteSignInEmailsOption>(
MuteSignInEmailsOption.NotMuted,
)
const [sessionUaLoggingValue, setSessionUaLoggingValue] = useState<LogSessionUserAgentOption>(
LogSessionUserAgentOption.Enabled,
)
const [isLoading, setIsLoading] = useState(true)
const updateSetting = async (settingName: SettingName, payload: string): Promise<boolean> => {
try {
await application.settings.updateSetting(settingName, payload, false)
return true
} catch (e) {
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
return false
}
}
const loadSettings = useCallback(async () => {
if (!application.getUser()) {
return
}
setIsLoading(true)
try {
const userSettings = await application.settings.listSettings()
setSignInEmailsMutedValue(
userSettings.getSettingValue<MuteSignInEmailsOption>(
SettingName.MuteSignInEmails,
MuteSignInEmailsOption.NotMuted,
),
)
setSessionUaLoggingValue(
userSettings.getSettingValue<LogSessionUserAgentOption>(
SettingName.LogSessionUserAgent,
LogSessionUserAgentOption.Enabled,
),
)
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}, [application])
useEffect(() => {
loadSettings().catch(console.error)
}, [loadSettings])
const toggleMuteSignInEmails = async () => {
const previousValue = signInEmailsMutedValue
const newValue =
previousValue === MuteSignInEmailsOption.Muted ? MuteSignInEmailsOption.NotMuted : MuteSignInEmailsOption.Muted
setSignInEmailsMutedValue(newValue)
const updateResult = await updateSetting(SettingName.MuteSignInEmails, newValue)
if (!updateResult) {
setSignInEmailsMutedValue(previousValue)
}
}
const toggleSessionLogging = async () => {
const previousValue = sessionUaLoggingValue
const newValue =
previousValue === LogSessionUserAgentOption.Enabled
? LogSessionUserAgentOption.Disabled
: LogSessionUserAgentOption.Enabled
setSessionUaLoggingValue(newValue)
const updateResult = await updateSetting(SettingName.LogSessionUserAgent, newValue)
if (!updateResult) {
setSessionUaLoggingValue(previousValue)
}
}
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Privacy</Title>
<div>
<div className="flex items-center justify-between">
<div className="flex flex-col">
<Subtitle>Disable sign-in notification emails</Subtitle>
<Text>
Disables email notifications when a new sign-in occurs on your account. (Email notifications are
available to paid subscribers).
</Text>
</div>
{isLoading ? (
<div className={'sk-spinner info small flex-shrink-0 ml-2'} />
) : (
<Switch
onChange={toggleMuteSignInEmails}
checked={signInEmailsMutedValue === MuteSignInEmailsOption.Muted}
/>
)}
</div>
<HorizontalSeparator classes="my-4" />
<div className="flex items-center justify-between">
<div className="flex flex-col">
<Subtitle>Session user agent logging</Subtitle>
<Text>
User agent logging allows you to identify the devices or browsers signed into your account. For
increased privacy, you can disable this feature, which will remove all saved user agent values from our
server, and disable future logging of this value.
</Text>
</div>
{isLoading ? (
<div className={'sk-spinner info small flex-shrink-0 ml-2'} />
) : (
<Switch
onChange={toggleSessionLogging}
checked={sessionUaLoggingValue === LogSessionUserAgentOption.Enabled}
/>
)}
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(Privacy)

View File

@@ -0,0 +1,93 @@
import { WebApplication } from '@/Application/Application'
import { FunctionComponent, useCallback, useState, useEffect } from 'react'
import { ApplicationEvent } from '@standardnotes/snjs'
import { isSameDay } from '@/Utils'
import Button from '@/Components/Button/Button'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import { Title, Text } from '../../PreferencesComponents/Content'
type Props = {
application: WebApplication
}
const Protections: FunctionComponent<Props> = ({ application }) => {
const enableProtections = () => {
application.clearProtectionSession().catch(console.error)
}
const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources())
const getProtectionsDisabledUntil = useCallback((): string | null => {
const protectionExpiry = application.getProtectionSessionExpiryDate()
const now = new Date()
if (protectionExpiry > now) {
let f: Intl.DateTimeFormat
if (isSameDay(protectionExpiry, now)) {
f = new Intl.DateTimeFormat(undefined, {
hour: 'numeric',
minute: 'numeric',
})
} else {
f = new Intl.DateTimeFormat(undefined, {
weekday: 'long',
day: 'numeric',
month: 'short',
hour: 'numeric',
minute: 'numeric',
})
}
return f.format(protectionExpiry)
}
return null
}, [application])
const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil())
useEffect(() => {
const removeUnprotectedSessionBeginObserver = application.addEventObserver(async () => {
setProtectionsDisabledUntil(getProtectionsDisabledUntil())
}, ApplicationEvent.UnprotectedSessionBegan)
const removeUnprotectedSessionEndObserver = application.addEventObserver(async () => {
setProtectionsDisabledUntil(getProtectionsDisabledUntil())
}, ApplicationEvent.UnprotectedSessionExpired)
const removeKeyStatusChangedObserver = application.addEventObserver(async () => {
setHasProtections(application.hasProtectionSources())
}, ApplicationEvent.KeyStatusChanged)
return () => {
removeUnprotectedSessionBeginObserver()
removeUnprotectedSessionEndObserver()
removeKeyStatusChangedObserver()
}
}, [application, getProtectionsDisabledUntil])
if (!hasProtections) {
return null
}
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Protections</Title>
{protectionsDisabledUntil ? (
<Text className="info">Unprotected access expires at {protectionsDisabledUntil}.</Text>
) : (
<Text className="info">Protections are enabled.</Text>
)}
<Text className="mt-2">
Actions like viewing or searching protected notes, exporting decrypted backups, or revoking an active session
require additional authentication such as entering your account password or application passcode.
</Text>
{protectionsDisabledUntil && (
<Button className="mt-3" variant="primary" label="End Unprotected Access" onClick={enableProtections} />
)}
</PreferencesSegment>
</PreferencesGroup>
)
}
export default Protections

View File

@@ -0,0 +1,31 @@
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { FunctionComponent } from 'react'
import TwoFactorAuthWrapper from '../TwoFactorAuth/TwoFactorAuthWrapper'
import { MfaProps } from '../TwoFactorAuth/MfaProps'
import Encryption from './Encryption'
import PasscodeLock from './PasscodeLock'
import Privacy from './Privacy'
import Protections from './Protections'
import ErroredItems from './ErroredItems'
import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane'
interface SecurityProps extends MfaProps {
viewControllerManager: ViewControllerManager
application: WebApplication
}
const Security: FunctionComponent<SecurityProps> = (props) => (
<PreferencesPane>
<Encryption viewControllerManager={props.viewControllerManager} />
{props.application.items.invalidItems.length > 0 && (
<ErroredItems viewControllerManager={props.viewControllerManager} />
)}
<Protections application={props.application} />
<TwoFactorAuthWrapper mfaProvider={props.mfaProvider} userProvider={props.userProvider} />
<PasscodeLock viewControllerManager={props.viewControllerManager} application={props.application} />
{props.application.getUser() && <Privacy application={props.application} />}
</PreferencesPane>
)
export default Security

View File

@@ -0,0 +1 @@
export const formatCount = (count: number, itemType: string) => `${count} / ${count} ${itemType}`

View File

@@ -0,0 +1,5 @@
import { WebApplication } from '@/Application/Application'
export const securityPrefsHasBubble = (application: WebApplication): boolean => {
return application.items.invalidItems.length > 0
}