internal: change password preprocessing step (#2347)
This commit is contained in:
@@ -22,6 +22,8 @@ import {
|
||||
Environment,
|
||||
ApplicationOptionsDefaults,
|
||||
BackupServiceInterface,
|
||||
InternalFeatureService,
|
||||
InternalFeatureServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable } from 'mobx'
|
||||
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
||||
@@ -263,6 +265,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return undefined
|
||||
}
|
||||
|
||||
public getInternalFeatureService(): InternalFeatureServiceInterface {
|
||||
return InternalFeatureService.get()
|
||||
}
|
||||
|
||||
isNativeIOS() {
|
||||
return this.isNativeMobileWeb() && this.platform === Platform.Ios
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { CheckmarkCircle } from '../UIElements/CheckmarkCircle'
|
||||
|
||||
export const FinishStep = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-row items-start gap-3">
|
||||
<div className="pt-1">
|
||||
<CheckmarkCircle />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-base font-bold">Your password has been successfully changed.</div>
|
||||
<p>Ensure you are running the latest version of Standard Notes on all platforms for maximum compatibility.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useState } from 'react'
|
||||
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
||||
|
||||
export const PasswordStep = ({
|
||||
onCurrentPasswordChange,
|
||||
onNewPasswordChange,
|
||||
onNewPasswordConfirmationChange,
|
||||
}: {
|
||||
onCurrentPasswordChange: (value: string) => void
|
||||
onNewPasswordChange: (value: string) => void
|
||||
onNewPasswordConfirmationChange: (value: string) => void
|
||||
}) => {
|
||||
const [currentPassword, setCurrentPassword] = useState<string>('')
|
||||
const [newPassword, setNewPassword] = useState<string>('')
|
||||
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState<string>('')
|
||||
|
||||
const handleCurrentPasswordChange = (value: string) => {
|
||||
setCurrentPassword(value)
|
||||
onCurrentPasswordChange(value)
|
||||
}
|
||||
|
||||
const handleNewPasswordChange = (value: string) => {
|
||||
setNewPassword(value)
|
||||
onNewPasswordChange(value)
|
||||
}
|
||||
|
||||
const handleNewPasswordConfirmationChange = (value: string) => {
|
||||
setNewPasswordConfirmation(value)
|
||||
onNewPasswordConfirmationChange(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col pb-1.5">
|
||||
<form>
|
||||
<label htmlFor="password-wiz-current-password" className="mb-1 block">
|
||||
Current Password
|
||||
</label>
|
||||
|
||||
<DecoratedPasswordInput
|
||||
autofocus={true}
|
||||
id="password-wiz-current-password"
|
||||
value={currentPassword}
|
||||
onChange={handleCurrentPasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div className="min-h-2" />
|
||||
|
||||
<label htmlFor="password-wiz-new-password" className="mb-1 block">
|
||||
New Password
|
||||
</label>
|
||||
|
||||
<DecoratedPasswordInput
|
||||
id="password-wiz-new-password"
|
||||
value={newPassword}
|
||||
onChange={handleNewPasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div className="min-h-2" />
|
||||
|
||||
<label htmlFor="password-wiz-confirm-new-password" className="mb-1 block">
|
||||
Confirm New Password
|
||||
</label>
|
||||
|
||||
<DecoratedPasswordInput
|
||||
id="password-wiz-confirm-new-password"
|
||||
value={newPasswordConfirmation}
|
||||
onChange={handleNewPasswordConfirmationChange}
|
||||
type="password"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { createRef } from 'react'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
||||
import Modal from '../Modal/Modal'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import Spinner from '../Spinner/Spinner'
|
||||
import { PasswordStep } from './PasswordStep'
|
||||
import { FinishStep } from './FinishStep'
|
||||
import { PreprocessingStep } from './PreprocessingStep'
|
||||
import { featureTrunkVaultsEnabled } from '@/FeatureTrunk'
|
||||
|
||||
interface Props {
|
||||
application: WebApplication
|
||||
@@ -19,7 +21,6 @@ type State = {
|
||||
processing?: boolean
|
||||
showSpinner?: boolean
|
||||
step: Steps
|
||||
title: string
|
||||
}
|
||||
|
||||
const DEFAULT_CONTINUE_TITLE = 'Continue'
|
||||
@@ -27,8 +28,9 @@ const GENERATING_CONTINUE_TITLE = 'Generating Keys...'
|
||||
const FINISH_CONTINUE_TITLE = 'Finish'
|
||||
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
FinishStep = 2,
|
||||
PreprocessingStep = 'preprocessing-step',
|
||||
PasswordStep = 'password-step',
|
||||
FinishStep = 'finish-step',
|
||||
}
|
||||
|
||||
type FormData = {
|
||||
@@ -39,22 +41,32 @@ type FormData = {
|
||||
}
|
||||
|
||||
class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
private currentPasswordInput = createRef<HTMLInputElement>()
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props, props.application)
|
||||
this.registerWindowUnloadStopper()
|
||||
this.state = {
|
||||
|
||||
const baseState = {
|
||||
formData: {},
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||
step: Steps.PasswordStep,
|
||||
title: 'Change Password',
|
||||
}
|
||||
|
||||
if (featureTrunkVaultsEnabled()) {
|
||||
this.state = {
|
||||
...baseState,
|
||||
lockContinue: true,
|
||||
step: Steps.PreprocessingStep,
|
||||
}
|
||||
} else {
|
||||
this.state = {
|
||||
...baseState,
|
||||
lockContinue: false,
|
||||
step: Steps.PasswordStep,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
super.componentDidMount()
|
||||
this.currentPasswordInput.current?.focus()
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
@@ -83,6 +95,15 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
|
||||
if (this.state.step === Steps.FinishStep) {
|
||||
this.dismiss()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.state.step === Steps.PreprocessingStep) {
|
||||
this.setState({
|
||||
step: Steps.PasswordStep,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,7 +163,6 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
return false
|
||||
}
|
||||
|
||||
/** Validate current password */
|
||||
const success = await this.application.validateAccountPassword(this.state.formData.currentPassword as string)
|
||||
if (!success) {
|
||||
this.application.alertService
|
||||
@@ -192,7 +212,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
}
|
||||
|
||||
dismiss = () => {
|
||||
if (this.state.lockContinue) {
|
||||
if (this.state.processing) {
|
||||
this.application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
|
||||
} else {
|
||||
this.props.dismissModal()
|
||||
@@ -226,11 +246,32 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
}).catch(console.error)
|
||||
}
|
||||
|
||||
setContinueEnabled = (enabled: boolean) => {
|
||||
this.setState({
|
||||
lockContinue: !enabled,
|
||||
})
|
||||
}
|
||||
|
||||
nextStepFromPreprocessing = () => {
|
||||
if (this.state.lockContinue) {
|
||||
this.setState(
|
||||
{
|
||||
lockContinue: false,
|
||||
},
|
||||
() => {
|
||||
void this.nextStep()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
void this.nextStep()
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return (
|
||||
<div className="sn-component h-full w-full md:h-auto md:w-auto" id="password-wizard">
|
||||
<Modal
|
||||
title={this.state.title}
|
||||
title={'Change Password'}
|
||||
close={this.dismiss}
|
||||
actions={[
|
||||
{
|
||||
@@ -253,59 +294,23 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<div className="px-4.5 py-4">
|
||||
{this.state.step === Steps.PreprocessingStep && (
|
||||
<PreprocessingStep
|
||||
onContinue={this.nextStepFromPreprocessing}
|
||||
setContinueEnabled={this.setContinueEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.step === Steps.PasswordStep && (
|
||||
<div className="flex flex-col pb-1.5">
|
||||
<form>
|
||||
<label htmlFor="password-wiz-current-password" className="mb-1 block">
|
||||
Current Password
|
||||
</label>
|
||||
|
||||
<DecoratedPasswordInput
|
||||
ref={this.currentPasswordInput}
|
||||
id="password-wiz-current-password"
|
||||
value={this.state.formData.currentPassword}
|
||||
onChange={this.handleCurrentPasswordInputChange}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div className="min-h-2" />
|
||||
|
||||
<label htmlFor="password-wiz-new-password" className="mb-1 block">
|
||||
New Password
|
||||
</label>
|
||||
|
||||
<DecoratedPasswordInput
|
||||
id="password-wiz-new-password"
|
||||
value={this.state.formData.newPassword}
|
||||
onChange={this.handleNewPasswordInputChange}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div className="min-h-2" />
|
||||
|
||||
<label htmlFor="password-wiz-confirm-new-password" className="mb-1 block">
|
||||
Confirm New Password
|
||||
</label>
|
||||
|
||||
<DecoratedPasswordInput
|
||||
id="password-wiz-confirm-new-password"
|
||||
value={this.state.formData.newPasswordConfirmation}
|
||||
onChange={this.handleNewPasswordConfirmationInputChange}
|
||||
type="password"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{this.state.step === Steps.FinishStep && (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-1 font-bold text-info">Your password has been successfully changed.</div>
|
||||
<p className="sk-p">
|
||||
Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum
|
||||
compatibility.
|
||||
</p>
|
||||
</div>
|
||||
<PasswordStep
|
||||
onCurrentPasswordChange={this.handleCurrentPasswordInputChange}
|
||||
onNewPasswordChange={this.handleNewPasswordInputChange}
|
||||
onNewPasswordConfirmationChange={this.handleNewPasswordConfirmationInputChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.step === Steps.FinishStep && <FinishStep />}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import Spinner from '../Spinner/Spinner'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export const PreprocessingStep = ({
|
||||
onContinue,
|
||||
setContinueEnabled,
|
||||
}: {
|
||||
onContinue: () => void
|
||||
setContinueEnabled: (disabled: boolean) => void
|
||||
}) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [isProcessingSync, setIsProcessingSync] = useState<boolean>(true)
|
||||
const [isProcessingMessages, setIsProcessingMessages] = useState<boolean>(true)
|
||||
const [isProcessingInvites, setIsProcessingInvites] = useState<boolean>(true)
|
||||
const [needsUserConfirmation, setNeedsUserConfirmation] = useState<'yes' | 'no'>()
|
||||
|
||||
const continueIfPossible = useCallback(() => {
|
||||
if (isProcessingMessages || isProcessingInvites || isProcessingSync) {
|
||||
setContinueEnabled(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (needsUserConfirmation === 'yes') {
|
||||
setContinueEnabled(true)
|
||||
return
|
||||
}
|
||||
|
||||
onContinue()
|
||||
}, [
|
||||
isProcessingInvites,
|
||||
isProcessingMessages,
|
||||
isProcessingSync,
|
||||
needsUserConfirmation,
|
||||
onContinue,
|
||||
setContinueEnabled,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
continueIfPossible()
|
||||
}, [isProcessingInvites, isProcessingMessages, isProcessingSync, continueIfPossible])
|
||||
|
||||
useEffect(() => {
|
||||
const processPendingSync = async () => {
|
||||
await application.sync.sync()
|
||||
setIsProcessingSync(false)
|
||||
}
|
||||
|
||||
void processPendingSync()
|
||||
}, [application.sync])
|
||||
|
||||
useEffect(() => {
|
||||
const processPendingMessages = async () => {
|
||||
await application.asymmetric.downloadAndProcessInboundMessages()
|
||||
setIsProcessingMessages(false)
|
||||
}
|
||||
|
||||
void processPendingMessages()
|
||||
}, [application.asymmetric])
|
||||
|
||||
useEffect(() => {
|
||||
const processPendingInvites = async () => {
|
||||
await application.sharedVaults.downloadInboundInvites()
|
||||
const hasPendingInvites = application.sharedVaults.getCachedPendingInviteRecords().length > 0
|
||||
setNeedsUserConfirmation(hasPendingInvites ? 'yes' : 'no')
|
||||
setIsProcessingInvites(false)
|
||||
}
|
||||
|
||||
void processPendingInvites()
|
||||
}, [application.sharedVaults])
|
||||
|
||||
const isProcessing = isProcessingSync || isProcessingMessages || isProcessingInvites
|
||||
|
||||
if (isProcessing) {
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Spinner className="h-3 w-3" />
|
||||
<p className="">Checking for data conflicts...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (needsUserConfirmation === 'no') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p>
|
||||
You have pending vault invites. Changing your password will delete these invites. It is recommended you accept
|
||||
or decline these invites before changing your password. If you choose to continue, these invites will be
|
||||
deleted.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { PreferencesMenuItem } from './PreferencesMenuItem'
|
||||
|
||||
export const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
||||
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
||||
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
||||
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
||||
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
||||
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard', order: 8 },
|
||||
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility', order: 9 },
|
||||
{ id: 'get-free-month', label: 'Get a free month', icon: 'star', order: 10 },
|
||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
||||
]
|
||||
|
||||
export const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
||||
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
||||
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
||||
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
||||
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { PreferenceId } from '@standardnotes/ui-services'
|
||||
|
||||
export interface PreferencesMenuItem {
|
||||
readonly id: PreferenceId
|
||||
readonly icon: IconType
|
||||
readonly label: string
|
||||
readonly order: number
|
||||
readonly hasBubble?: boolean
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { action, makeAutoObservable, observable } from 'mobx'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { PackageProvider } from '../Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||
import { securityPrefsHasBubble } from '../Panes/Security/securityPrefsHasBubble'
|
||||
import { PreferenceId } from '@standardnotes/ui-services'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import { featureTrunkHomeServerEnabled, featureTrunkVaultsEnabled } from '@/FeatureTrunk'
|
||||
import { PreferencesMenuItem } from './PreferencesMenuItem'
|
||||
import { SelectableMenuItem } from './SelectableMenuItem'
|
||||
import { PREFERENCES_MENU_ITEMS, READY_PREFERENCES_MENU_ITEMS } from './MenuItems'
|
||||
|
||||
/**
|
||||
* Unlike PreferencesController, the PreferencesSessionController is ephemeral and bound to a single opening of the
|
||||
* Preferences menu. It is created and destroyed each time the menu is opened and closed.
|
||||
*/
|
||||
export class PreferencesSessionController {
|
||||
private _selectedPane: PreferenceId = 'account'
|
||||
private _menu: PreferencesMenuItem[]
|
||||
private _extensionLatestVersions: PackageProvider = new PackageProvider(new Map())
|
||||
|
||||
constructor(private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean) {
|
||||
const menuItems = this._enableUnfinishedFeatures
|
||||
? PREFERENCES_MENU_ITEMS.slice()
|
||||
: READY_PREFERENCES_MENU_ITEMS.slice()
|
||||
|
||||
if (featureTrunkVaultsEnabled()) {
|
||||
menuItems.push({ id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
||||
}
|
||||
|
||||
if (featureTrunkHomeServerEnabled() && isDesktopApplication()) {
|
||||
menuItems.push({ id: 'home-server', label: 'Home Server', icon: 'server', order: 5 })
|
||||
}
|
||||
|
||||
this._menu = menuItems.sort((a, b) => a.order - b.order)
|
||||
|
||||
this.loadLatestVersions()
|
||||
|
||||
makeAutoObservable<
|
||||
PreferencesSessionController,
|
||||
'_selectedPane' | '_twoFactorAuth' | '_extensionPanes' | '_extensionLatestVersions' | 'loadLatestVersions'
|
||||
>(this, {
|
||||
_twoFactorAuth: observable,
|
||||
_selectedPane: observable,
|
||||
_extensionPanes: observable.ref,
|
||||
_extensionLatestVersions: observable.ref,
|
||||
loadLatestVersions: action,
|
||||
})
|
||||
}
|
||||
|
||||
private loadLatestVersions(): void {
|
||||
PackageProvider.load()
|
||||
.then((versions) => {
|
||||
if (versions) {
|
||||
this._extensionLatestVersions = versions
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
get extensionsLatestVersions(): PackageProvider {
|
||||
return this._extensionLatestVersions
|
||||
}
|
||||
|
||||
get menuItems(): SelectableMenuItem[] {
|
||||
const menuItems = this._menu.map((preference) => {
|
||||
const item: SelectableMenuItem = {
|
||||
...preference,
|
||||
selected: preference.id === this._selectedPane,
|
||||
hasBubble: this.sectionHasBubble(preference.id),
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
return menuItems
|
||||
}
|
||||
|
||||
get selectedMenuItem(): PreferencesMenuItem | undefined {
|
||||
return this._menu.find((item) => item.id === this._selectedPane)
|
||||
}
|
||||
|
||||
get selectedPaneId(): PreferenceId {
|
||||
if (this.selectedMenuItem != undefined) {
|
||||
return this.selectedMenuItem.id
|
||||
}
|
||||
|
||||
return 'account'
|
||||
}
|
||||
|
||||
selectPane = (key: PreferenceId) => {
|
||||
this._selectedPane = key
|
||||
}
|
||||
|
||||
sectionHasBubble(id: PreferenceId): boolean {
|
||||
if (id === 'security') {
|
||||
return securityPrefsHasBubble(this.application)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PreferencesMenuItem } from './PreferencesMenuItem'
|
||||
|
||||
export interface SelectableMenuItem extends PreferencesMenuItem {
|
||||
selected: boolean
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||
import Backups from '@/Components/Preferences/Panes/Backups/Backups'
|
||||
import Appearance from './Panes/Appearance'
|
||||
import General from './Panes/General/General'
|
||||
@@ -13,7 +13,7 @@ import WhatsNew from './Panes/WhatsNew/WhatsNew'
|
||||
import HomeServer from './Panes/HomeServer/HomeServer'
|
||||
import Vaults from './Panes/Vaults/Vaults'
|
||||
|
||||
const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = ({
|
||||
const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesSessionController }> = ({
|
||||
menu,
|
||||
viewControllerManager,
|
||||
application,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Fragment, FunctionComponent, useState } from 'react'
|
||||
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
|
||||
import { Text, Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import {
|
||||
ButtonType,
|
||||
ClientDisplayableError,
|
||||
ContentType,
|
||||
DisplayStringForContentType,
|
||||
EncryptedItemInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
@@ -12,13 +12,18 @@ import Button from '@/Components/Button/Button'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import { ErrorCircle } from '@/Components/UIElements/ErrorCircle'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
type Props = { viewControllerManager: ViewControllerManager }
|
||||
const ErroredItems: FunctionComponent = () => {
|
||||
const application = useApplication()
|
||||
const [erroredItems, setErroredItems] = useState(application.items.invalidNonVaultedItems)
|
||||
|
||||
const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
|
||||
const app = viewControllerManager.application
|
||||
|
||||
const [erroredItems, setErroredItems] = useState(app.items.invalidNonVaultedItems)
|
||||
useEffect(() => {
|
||||
return application.streamItems(ContentType.Any, () => {
|
||||
setErroredItems(application.items.invalidNonVaultedItems)
|
||||
})
|
||||
}, [application])
|
||||
|
||||
const getContentTypeDisplay = (item: EncryptedItemInterface): string => {
|
||||
const display = DisplayStringForContentType(item.content_type)
|
||||
@@ -34,7 +39,7 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
||||
}
|
||||
|
||||
const deleteItems = async (items: EncryptedItemInterface[]): Promise<void> => {
|
||||
const confirmed = await app.alertService.confirm(
|
||||
const confirmed = await application.alertService.confirm(
|
||||
`Are you sure you want to permanently delete ${items.length} item(s)?`,
|
||||
undefined,
|
||||
'Delete',
|
||||
@@ -44,30 +49,35 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
||||
return
|
||||
}
|
||||
|
||||
void app.mutator.deleteItems(items).then(() => {
|
||||
void app.sync.sync()
|
||||
void application.mutator.deleteItems(items).then(() => {
|
||||
void application.sync.sync()
|
||||
})
|
||||
|
||||
setErroredItems(app.items.invalidItems)
|
||||
setErroredItems(application.items.invalidItems)
|
||||
}
|
||||
|
||||
const attemptDecryption = (item: EncryptedItemInterface): void => {
|
||||
const errorOrTrue = app.canAttemptDecryptionOfItem(item)
|
||||
const errorOrTrue = application.canAttemptDecryptionOfItem(item)
|
||||
|
||||
if (errorOrTrue instanceof ClientDisplayableError) {
|
||||
void app.alertService.showErrorAlert(errorOrTrue)
|
||||
void application.alertService.showErrorAlert(errorOrTrue)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
app.presentKeyRecoveryWizard()
|
||||
application.presentKeyRecoveryWizard()
|
||||
}
|
||||
|
||||
if (erroredItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>
|
||||
Error decrypting items <span className="ml-1 text-warning">⚠️</span>
|
||||
<Title className="flex flex-row items-center gap-2">
|
||||
<ErrorCircle />
|
||||
Error decrypting items
|
||||
</Title>
|
||||
<Text>{`${erroredItems.length} items are errored and could not be decrypted.`}</Text>
|
||||
<div className="flex">
|
||||
@@ -75,7 +85,7 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
||||
className="mt-3 mr-2 min-w-20"
|
||||
label="Export all"
|
||||
onClick={() => {
|
||||
void app.getArchiveService().downloadEncryptedItems(erroredItems)
|
||||
void application.getArchiveService().downloadEncryptedItems(erroredItems)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -95,10 +105,8 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
||||
<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>
|
||||
<Text>Item ID: {item.uuid}</Text>
|
||||
<Text>Last Modified: {item.updatedAtString}</Text>
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="mt-3 mr-2 min-w-20"
|
||||
@@ -111,7 +119,7 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
||||
className="mt-3 mr-2 min-w-20"
|
||||
label="Export"
|
||||
onClick={() => {
|
||||
void app.getArchiveService().downloadEncryptedItem(item)
|
||||
void application.getArchiveService().downloadEncryptedItem(item)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -30,9 +30,7 @@ const Security: FunctionComponent<SecurityProps> = (props) => {
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Encryption viewControllerManager={props.viewControllerManager} />
|
||||
{props.application.items.invalidNonVaultedItems.length > 0 && (
|
||||
<ErroredItems viewControllerManager={props.viewControllerManager} />
|
||||
)}
|
||||
{props.application.items.invalidNonVaultedItems.length > 0 && <ErroredItems />}
|
||||
<Protections application={props.application} />
|
||||
<TwoFactorAuthWrapper
|
||||
mfaProvider={props.mfaProvider}
|
||||
|
||||
@@ -25,7 +25,7 @@ const ContactItem = ({ contact }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay isOpen={isContactModalOpen} close={closeContactModal}>
|
||||
<EditContactModal editContactUuid={contact.uuid} onCloseDialog={closeContactModal} />
|
||||
<EditContactModal editContactUuid={contact.contactUuid} onCloseDialog={closeContactModal} />
|
||||
</ModalOverlay>
|
||||
|
||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
|
||||
@@ -5,33 +5,36 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs'
|
||||
import { useCallback, useState } from 'react'
|
||||
import EditContactModal from '../Contacts/EditContactModal'
|
||||
import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle'
|
||||
|
||||
type Props = {
|
||||
invite: PendingSharedVaultInviteRecord
|
||||
inviteRecord: PendingSharedVaultInviteRecord
|
||||
}
|
||||
|
||||
const InviteItem = ({ invite }: Props) => {
|
||||
const InviteItem = ({ inviteRecord }: Props) => {
|
||||
const application = useApplication()
|
||||
const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false)
|
||||
|
||||
const isTrusted = invite.trusted
|
||||
const inviteData = invite.message.data
|
||||
const isTrusted = inviteRecord.trusted
|
||||
const inviteData = inviteRecord.message.data
|
||||
|
||||
const addAsTrustedContact = useCallback(() => {
|
||||
setIsAddContactModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const acceptInvite = useCallback(async () => {
|
||||
await application.sharedVaults.acceptPendingSharedVaultInvite(invite)
|
||||
}, [application.sharedVaults, invite])
|
||||
await application.sharedVaults.acceptPendingSharedVaultInvite(inviteRecord)
|
||||
}, [application.sharedVaults, inviteRecord])
|
||||
|
||||
const closeAddContactModal = () => setIsAddContactModalOpen(false)
|
||||
const collaborationId = application.contacts.getCollaborationIDFromInvite(invite.invite)
|
||||
const collaborationId = application.contacts.getCollaborationIDFromInvite(inviteRecord.invite)
|
||||
|
||||
const trustedContact = application.contacts.findTrustedContactForInvite(inviteRecord.invite)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
||||
<EditContactModal fromInvite={invite} onCloseDialog={closeAddContactModal} />
|
||||
<EditContactModal fromInvite={inviteRecord} onCloseDialog={closeAddContactModal} />
|
||||
</ModalOverlay>
|
||||
|
||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
@@ -41,9 +44,16 @@ const InviteItem = ({ invite }: Props) => {
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
||||
Vault Description: {inviteData.metadata.description}
|
||||
</span>
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
||||
Sender CollaborationID: {collaborationId}
|
||||
</span>
|
||||
{trustedContact ? (
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className="overflow-hidden text-ellipsis text-sm">Trusted Sender: {trustedContact.name}</span>
|
||||
<CheckmarkCircle />
|
||||
</div>
|
||||
) : (
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
||||
Sender CollaborationID: {collaborationId}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
{isTrusted ? (
|
||||
|
||||
@@ -40,31 +40,36 @@ const Vaults = () => {
|
||||
setVaults(vaultService.getVaults())
|
||||
}, [vaultService])
|
||||
|
||||
const fetchInvites = useCallback(async () => {
|
||||
await sharedVaultService.downloadInboundInvites()
|
||||
const invites = sharedVaultService.getCachedPendingInviteRecords()
|
||||
setInvites(invites)
|
||||
const updateInvites = useCallback(async () => {
|
||||
setInvites(sharedVaultService.getCachedPendingInviteRecords())
|
||||
}, [sharedVaultService])
|
||||
|
||||
const updateContacts = useCallback(async () => {
|
||||
setContacts(contactService.getAllContacts())
|
||||
}, [contactService])
|
||||
|
||||
const updateAllData = useCallback(async () => {
|
||||
await Promise.all([updateVaults(), updateInvites(), updateContacts()])
|
||||
}, [updateContacts, updateInvites, updateVaults])
|
||||
|
||||
useEffect(() => {
|
||||
return application.sharedVaults.addEventObserver((event) => {
|
||||
if (event === SharedVaultServiceEvent.SharedVaultStatusChanged) {
|
||||
void fetchInvites()
|
||||
void updateAllData()
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [application.sharedVaults, updateAllData])
|
||||
|
||||
useEffect(() => {
|
||||
return application.streamItems([ContentType.VaultListing, ContentType.TrustedContact], () => {
|
||||
void updateVaults()
|
||||
void fetchInvites()
|
||||
void updateContacts()
|
||||
void updateAllData()
|
||||
})
|
||||
}, [application, updateVaults, fetchInvites, updateContacts])
|
||||
}, [application, updateAllData])
|
||||
|
||||
useEffect(() => {
|
||||
void sharedVaultService.downloadInboundInvites()
|
||||
void updateAllData()
|
||||
}, [updateAllData, sharedVaultService])
|
||||
|
||||
const createNewVault = useCallback(async () => {
|
||||
setIsVaultModalOpen(true)
|
||||
@@ -74,12 +79,6 @@ const Vaults = () => {
|
||||
setIsAddContactModalOpen(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void updateVaults()
|
||||
void fetchInvites()
|
||||
void updateContacts()
|
||||
}, [updateContacts, updateVaults, fetchInvites])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
||||
@@ -95,7 +94,7 @@ const Vaults = () => {
|
||||
<Title>Incoming Invites</Title>
|
||||
<div className="my-2 flex flex-col">
|
||||
{invites.map((invite) => {
|
||||
return <InviteItem invite={invite} key={invite.invite.uuid} />
|
||||
return <InviteItem inviteRecord={invite} key={invite.invite.uuid} />
|
||||
})}
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
|
||||
@@ -28,9 +28,8 @@ export const VaultModalInvites = ({
|
||||
<div className="mb-3 text-lg">Pending Invites</div>
|
||||
{invites.map((invite) => {
|
||||
const contact = application.contacts.findTrustedContactForInvite(invite)
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
<div key={invite.uuid} className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-2 py-1.5">
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-base font-bold">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||
import PreferencesMenuView from './PreferencesMenuView'
|
||||
import PaneSelector from './PaneSelector'
|
||||
import { PreferencesProps } from './PreferencesProps'
|
||||
|
||||
const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = (props) => (
|
||||
const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesSessionController }> = (props) => (
|
||||
<div className="flex min-h-0 flex-grow flex-col md:flex-row md:justify-between">
|
||||
<PreferencesMenuView menu={props.menu} />
|
||||
<div className="min-h-0 flex-grow overflow-auto bg-contrast">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { ErrorCircle } from '@/Components/UIElements/ErrorCircle'
|
||||
|
||||
interface Props {
|
||||
iconType: IconType
|
||||
@@ -23,7 +24,11 @@ const PreferencesMenuItem: FunctionComponent<Props> = ({ iconType, label, select
|
||||
<Icon className={`icon text-base ${selected ? 'text-info' : 'text-neutral'}`} type={iconType} />
|
||||
<div className="min-w-1" />
|
||||
{label}
|
||||
{hasBubble && <span className="ml-1 text-warning">⚠️</span>}
|
||||
{hasBubble && (
|
||||
<span className="ml-2">
|
||||
<ErrorCircle />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { action, makeAutoObservable, observable } from 'mobx'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||
import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble'
|
||||
import { PreferenceId } from '@standardnotes/ui-services'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import { featureTrunkHomeServerEnabled, featureTrunkVaultsEnabled } from '@/FeatureTrunk'
|
||||
|
||||
interface PreferencesMenuItem {
|
||||
readonly id: PreferenceId
|
||||
readonly icon: IconType
|
||||
readonly label: string
|
||||
readonly order: number
|
||||
readonly hasBubble?: boolean
|
||||
}
|
||||
|
||||
interface SelectableMenuItem extends PreferencesMenuItem {
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Items are in order of appearance
|
||||
*/
|
||||
const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
||||
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
||||
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
||||
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
||||
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
||||
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard', order: 8 },
|
||||
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility', order: 9 },
|
||||
{ id: 'get-free-month', label: 'Get a free month', icon: 'star', order: 10 },
|
||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
||||
]
|
||||
|
||||
const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
||||
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
||||
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
||||
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
||||
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
||||
]
|
||||
|
||||
const DESKTOP_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = []
|
||||
|
||||
export class PreferencesMenu {
|
||||
private _selectedPane: PreferenceId = 'account'
|
||||
private _menu: PreferencesMenuItem[]
|
||||
private _extensionLatestVersions: PackageProvider = new PackageProvider(new Map())
|
||||
|
||||
constructor(private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean) {
|
||||
if (featureTrunkVaultsEnabled()) {
|
||||
PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
||||
READY_PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
||||
}
|
||||
|
||||
if (featureTrunkHomeServerEnabled()) {
|
||||
DESKTOP_PREFERENCES_MENU_ITEMS.push({ id: 'home-server', label: 'Home Server', icon: 'server', order: 5 })
|
||||
}
|
||||
|
||||
let menuItems = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS
|
||||
|
||||
if (isDesktopApplication()) {
|
||||
menuItems = [...menuItems, ...DESKTOP_PREFERENCES_MENU_ITEMS]
|
||||
}
|
||||
|
||||
this._menu = menuItems.sort((a, b) => a.order - b.order)
|
||||
|
||||
this.loadLatestVersions()
|
||||
|
||||
makeAutoObservable<
|
||||
PreferencesMenu,
|
||||
'_selectedPane' | '_twoFactorAuth' | '_extensionPanes' | '_extensionLatestVersions' | 'loadLatestVersions'
|
||||
>(this, {
|
||||
_twoFactorAuth: observable,
|
||||
_selectedPane: observable,
|
||||
_extensionPanes: observable.ref,
|
||||
_extensionLatestVersions: observable.ref,
|
||||
loadLatestVersions: action,
|
||||
})
|
||||
}
|
||||
|
||||
private loadLatestVersions(): void {
|
||||
PackageProvider.load()
|
||||
.then((versions) => {
|
||||
if (versions) {
|
||||
this._extensionLatestVersions = versions
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
get extensionsLatestVersions(): PackageProvider {
|
||||
return this._extensionLatestVersions
|
||||
}
|
||||
|
||||
get menuItems(): SelectableMenuItem[] {
|
||||
const menuItems = this._menu.map((preference) => {
|
||||
const item: SelectableMenuItem = {
|
||||
...preference,
|
||||
selected: preference.id === this._selectedPane,
|
||||
hasBubble: this.sectionHasBubble(preference.id),
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
return menuItems
|
||||
}
|
||||
|
||||
get selectedMenuItem(): PreferencesMenuItem | undefined {
|
||||
return this._menu.find((item) => item.id === this._selectedPane)
|
||||
}
|
||||
|
||||
get selectedPaneId(): PreferenceId {
|
||||
if (this.selectedMenuItem != undefined) {
|
||||
return this.selectedMenuItem.id
|
||||
}
|
||||
|
||||
return 'account'
|
||||
}
|
||||
|
||||
selectPane = (key: PreferenceId) => {
|
||||
this._selectedPane = key
|
||||
}
|
||||
|
||||
sectionHasBubble(id: PreferenceId): boolean {
|
||||
if (id === 'security') {
|
||||
return securityPrefsHasBubble(this.application)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import { FunctionComponent, useMemo } from 'react'
|
||||
import Dropdown from '../Dropdown/Dropdown'
|
||||
import { DropdownItem } from '../Dropdown/DropdownItem'
|
||||
import PreferencesMenuItem from './PreferencesComponents/MenuItem'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||
import { PreferenceId } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
menu: PreferencesMenu
|
||||
menu: PreferencesSessionController
|
||||
}
|
||||
|
||||
const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||
import { FunctionComponent, useEffect, useMemo } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||
import PreferencesCanvas from './PreferencesCanvas'
|
||||
import { PreferencesProps } from './PreferencesProps'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
@@ -19,7 +19,7 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
|
||||
mfaProvider,
|
||||
}) => {
|
||||
const menu = useMemo(
|
||||
() => new PreferencesMenu(application, viewControllerManager.enableUnfinishedFeatures),
|
||||
() => new PreferencesSessionController(application, viewControllerManager.enableUnfinishedFeatures),
|
||||
[viewControllerManager.enableUnfinishedFeatures, application],
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
export const CheckmarkCircle = () => {
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
'peer flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-success text-success-contrast'
|
||||
}
|
||||
>
|
||||
<Icon type={'check'} size="small" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
export const ErrorCircle = () => {
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
'peer flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-danger text-danger-contrast'
|
||||
}
|
||||
>
|
||||
<Icon type={'warning'} size="small" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user