refactor: migrate account-menu to react - implement functionality

- remove all Angular code related to `account-menu`
- rename React component to AccountMenu so that many parts of old code remain unchanged
- code cleanup
This commit is contained in:
VardanHakobyan
2021-06-07 20:30:49 +04:00
parent 6db97436b8
commit 7f11e25e63
14 changed files with 430 additions and 1427 deletions

View File

@@ -34,7 +34,6 @@ import {
} from './directives/functional';
import {
AccountMenu,
ActionsMenu,
ComponentModal,
ComponentView,
@@ -59,7 +58,7 @@ import { SessionsModalDirective } from './components/SessionsModal';
import { NoAccountWarningDirective } from './components/NoAccountWarning';
import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning';
import { SearchOptionsDirective } from './components/SearchOptions';
import { AccountMenuReact } from './components/AccountMenuReact';
import { AccountMenuDirective } from './components/AccountMenu';
import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal';
import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes';
import { NotesContextMenuDirective } from './components/NotesContextMenu';
@@ -137,7 +136,6 @@ const startApplication: StartApplication = async function startApplication(
// Directives - Views
angular
.module('app')
.directive('accountMenu', () => new AccountMenu())
.directive('accountSwitcher', () => new AccountSwitcher())
.directive('actionsMenu', () => new ActionsMenu())
.directive('challengeModal', () => new ChallengeModal())
@@ -153,7 +151,7 @@ const startApplication: StartApplication = async function startApplication(
.directive('historyMenu', () => new HistoryMenu())
.directive('syncResolutionMenu', () => new SyncResolutionMenu())
.directive('sessionsModal', SessionsModalDirective)
.directive('accountMenuReact', AccountMenuReact)
.directive('accountMenu', AccountMenuDirective)
.directive('noAccountWarning', NoAccountWarningDirective)
.directive('protectedNotePanel', NoProtectionsdNoteWarningDirective)
.directive('searchOptions', SearchOptionsDirective)

View File

@@ -32,6 +32,7 @@ import TargetedKeyboardEvent = JSXInternal.TargetedKeyboardEvent;
import TargetedMouseEvent = JSXInternal.TargetedMouseEvent;
import { alertDialog, confirmDialog } from '@Services/alertService';
import { RefObject } from 'react';
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
type Props = {
appState: AppState;
@@ -367,7 +368,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
};
const signOut = () => {
appState.accountMenuReact.setSigningOut(true);
appState.accountMenu.setSigningOut(true);
};
const changePasscodePressed = () => {
@@ -526,118 +527,109 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
}, [refreshEncryptionStatus]);
return (
<div style={{
top: '-70px',
right: '-450px',
width: '100px',
height: '100px',
background: 'green',
zIndex: 100,
position: 'absolute'
}}>
<div className='sn-component'>
<div id='account-panel-react' className='sk-panel'>
<div className='sk-panel-header'>
<div className='sk-panel-header-title'>Account</div>
<a className='sk-a info close-button' onClick={closeAccountMenu}>Close</a>
</div>
<div className='sk-panel-content'>
{!user && !showLogin && !showRegister && (
<div className='sk-panel-section sk-panel-hero'>
<div className='sk-panel-row'>
<div className='sk-h1'>Sign in or register to enable sync and end-to-end encryption.</div>
</div>
<div className='flex my-1'>
<button
className='sn-button info flex-grow text-base py-3 mr-1.5'
onClick={handleSignInClick}
>
Sign In
</button>
<button
className='sn-button info flex-grow text-base py-3 ml-1.5'
onClick={handleRegisterClick}
>
Register
</button>
</div>
<div className='sk-panel-row sk-p'>
Standard Notes is free on every platform, and comes
standard with sync and encryption.
</div>
<div className='sn-component'>
<div id='account-panel' className='sk-panel'>
<div className='sk-panel-header'>
<div className='sk-panel-header-title'>Account</div>
<a className='sk-a info close-button' onClick={closeAccountMenu}>Close</a>
</div>
<div className='sk-panel-content'>
{!user && !showLogin && !showRegister && (
<div className='sk-panel-section sk-panel-hero'>
<div className='sk-panel-row'>
<div className='sk-h1'>Sign in or register to enable sync and end-to-end encryption.</div>
</div>
)}
{(showLogin || showRegister) && (
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>
{showLogin ? 'Sign In' : 'Register'}
<div className='flex my-1'>
<button
className='sn-button info flex-grow text-base py-3 mr-1.5'
onClick={handleSignInClick}
>
Sign In
</button>
<button
className='sn-button info flex-grow text-base py-3 ml-1.5'
onClick={handleRegisterClick}
>
Register
</button>
</div>
<div className='sk-panel-row sk-p'>
Standard Notes is free on every platform, and comes
standard with sync and encryption.
</div>
</div>
)}
{(showLogin || showRegister) && (
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>
{showLogin ? 'Sign In' : 'Register'}
</div>
<form className='sk-panel-form' onSubmit={handleAuthFormSubmit} noValidate>
<div className='sk-panel-section'>
<input className='sk-input contrast'
name='email'
type='email'
value={email}
onChange={handleEmailChange}
placeholder='Email'
required
spellCheck={false}
ref={emailInputRef}
/>
<input className='sk-input contrast'
name='password'
type='password'
value={password}
onChange={handlePasswordChange}
placeholder='Password'
required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
ref={passwordInputRef}
/>
{showRegister &&
<input className='sk-input contrast'
name='password_conf'
type='password'
placeholder='Confirm Password' required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
value={passwordConfirmation}
onChange={handlePasswordConfirmationChange}
/>}
<div className='sk-panel-row' />
<a className='sk-panel-row sk-bold' onClick={() => {
setShowAdvanced(showAdvanced => !showAdvanced);
}}>
Advanced Options
</a>
</div>
<form className='sk-panel-form' onSubmit={handleAuthFormSubmit} noValidate>
<div className='sk-panel-section'>
<input className='sk-input contrast'
name='email'
type='email'
value={email}
onChange={handleEmailChange}
placeholder='Email'
required
spellCheck={false}
ref={emailInputRef}
/>
<input className='sk-input contrast'
name='password'
type='password'
value={password}
onChange={handlePasswordChange}
placeholder='Password'
required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
ref={passwordInputRef}
/>
{showRegister &&
<input className='sk-input contrast'
name='password_conf'
type='password'
placeholder='Confirm Password' required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
value={passwordConfirmation}
onChange={handlePasswordConfirmationChange}
/>}
<div className='sk-panel-row' />
<a className='sk-panel-row sk-bold' onClick={() => {
setShowAdvanced(showAdvanced => !showAdvanced);
}}>
Advanced Options
</a>
</div>
{showAdvanced && (
<div className='sk-notification unpadded contrast advanced-options sk-panel-row'>
<div className='sk-panel-column stretch'>
<div className='sk-notification-title sk-panel-row padded-row'>
Advanced Options
</div>
<div className='bordered-row padded-row'>
<label className='sk-label'>Sync Server Domain</label>
<input className='sk-input sk-base'
name='server'
placeholder='Server URL'
onChange={handleHostInputChange}
value={url}
required
/>
</div>
{showLogin && (
<label className='sk-label padded-row sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'>
<input
className='sk-input'
type='checkbox'
onChange={() => setIsStrictSignIn(prevState => !prevState)}
/>
<p className='sk-p'>Use strict sign in</p>
<span>
{showAdvanced && (
<div className='sk-notification unpadded contrast advanced-options sk-panel-row'>
<div className='sk-panel-column stretch'>
<div className='sk-notification-title sk-panel-row padded-row'>
Advanced Options
</div>
<div className='bordered-row padded-row'>
<label className='sk-label'>Sync Server Domain</label>
<input className='sk-input sk-base'
name='server'
placeholder='Server URL'
onChange={handleHostInputChange}
value={url}
required
/>
</div>
{showLogin && (
<label className='sk-label padded-row sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'>
<input
className='sk-input'
type='checkbox'
onChange={() => setIsStrictSignIn(prevState => !prevState)}
/>
<p className='sk-p'>Use strict sign in</p>
<span>
<a className='info'
href='https://standardnotes.org/help/security' rel='noopener'
target='_blank'
@@ -645,340 +637,340 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
(Learn more)
</a>
</span>
</div>
</label>
)}
</div>
</div>
)}
{!isAuthenticating && (
<div className='sk-panel-section form-submit'>
<button className='sn-button info text-base py-3 text-center' type='submit'
disabled={isAuthenticating}>
{showLogin ? 'Sign In' : 'Register'}
</button>
</div>
)}
{showRegister && (
<div className='sk-notification neutral'>
<div className='sk-notification-title'>No Password Reset.</div>
<div className='sk-notification-text'>
Because your notes are encrypted using your password,
Standard Notes does not have a password reset option.
You cannot forget your password.
</div>
</div>
)}
{status && (
<div className='sk-panel-section no-bottom-pad'>
<div className='sk-horizontal-group'>
<div className='sk-spinner small neutral' />
<div className='sk-label'>{status}</div>
</div>
</div>
)}
{!isAuthenticating && (
<div className='sk-panel-section no-bottom-pad'>
<label className='sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'>
<input
type='checkbox'
checked={!isEphemeral}
onChange={() => setIsEphemeral(prevState => !prevState)}
/>
<p className='sk-p'>Stay signed in</p>
</div>
</label>
{notesAndTagsCount > 0 && (
<label className='sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'>
<input
type='checkbox'
checked={shouldMergeLocal}
onChange={handleMergeLocalData}
/>
<p className='sk-p'>Merge local data ({notesAndTagsCount}) notes and tags</p>
</div>
</label>
)}
</div>
)}
</form>
</div>
)}
{!showLogin && !showRegister && (
<div>
{user && (
<div className='sk-panel-section'>
{syncError && (
<div className='sk-notification danger'>
<div className='sk-notification-title'>Sync Unreachable</div>
<div className='sk-notification-text'>
Hmm...we can't seem to sync your account.
The reason: {syncError}
</div>
<a
className='sk-a info-contrast sk-bold sk-panel-row'
href='https://standardnotes.org/help'
rel='noopener'
target='_blank'
>
Need help?
</a>
</div>
)}
<div className='sk-panel-row'>
<div className='sk-panel-column'>
<div className='sk-h1 sk-bold wrap'>
{user.email}
</div>
<div className='sk-subtitle neutral'>
{server}
</div>
</div>
</div>
<div className='sk-panel-row' />
<a className='sk-a info sk-panel-row condensed' onClick={openPasswordWizard}>
Change Password
</a>
<a className='sk-a info sk-panel-row condensed' onClick={openSessionsModal}>
Manage Sessions
</a>
</div>
)}
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>
Encryption
</div>
{isEncryptionEnabled && (
<div className='sk-panel-section-subtitle info'>
{getEncryptionStatusForNotes()}
</div>
)}
<p className='sk-p'>
{encryptionStatusString}
</p>
</div>
{hasProtections && (
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Protections</div>
{protectionsDisabledUntil && (
<div className='sk-panel-section-subtitle info'>
Protections are disabled until {protectionsDisabledUntil}
</div>
)}
{!protectionsDisabledUntil && (
<div className='sk-panel-section-subtitle info'>
Protections are enabled
</div>
)}
<p className='sk-p'>
Actions like viewing protected notes, exporting decrypted backups,
or revoking an active session, require additional authentication
like entering your account password or application passcode.
</p>
{protectionsDisabledUntil && (
<div className='sk-panel-row'>
<button className='sn-button small info' onClick={enableProtections}>
Enable protections
</button>
</div>
)}
</div>
)}
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Passcode Lock</div>
{!hasPasscode && (
<div>
{canAddPasscode && (
<>
{!showPasscodeForm && (
<div className='sk-panel-row'>
<button className='sn-button small info' onClick={handleAddPassCode}>
Add Passcode
</button>
</div>
)}
<p className='sk-p'>
Add a passcode to lock the application and
encrypt on-device key storage.
</p>
{keyStorageInfo && (
<p>{keyStorageInfo}</p>
)}
</>
)}
{!canAddPasscode && (
<p className='sk-p'>
Adding a passcode is not supported in temporary sessions. Please sign
out, then sign back in with the "Stay signed in" option checked.
</p>
)}
</div>
)}
{showPasscodeForm && (
<form className='sk-panel-form' onSubmit={submitPasscodeForm}>
<div className='sk-panel-row' />
<input
className='sk-input contrast'
type='password'
ref={passcodeInputRef}
value={passcode}
onChange={handlePasscodeChange}
placeholder='Passcode'
/>
<input
className='sk-input contrast'
type='password'
value={passcodeConfirmation}
onChange={handleConfirmPasscodeChange}
placeholder='Confirm Passcode'
/>
<button className='sn-button small info mt-2' onClick={submitPasscodeForm}>
Set Passcode
</button>
<button className='sn-button small outlined ml-2' onClick={() => setShowPasscodeForm(false)}>
Cancel
</button>
</form>
)}
{hasPasscode && !showPasscodeForm && (
<>
<div className='sk-panel-section-subtitle info'>Passcode lock is enabled</div>
<div className='sk-notification contrast'>
<div className='sk-notification-title'>Options</div>
<div className='sk-notification-text'>
<div className='sk-panel-row'>
<div className='sk-horizontal-group'>
<div className='sk-h4 sk-bold'>Autolock</div>
{passcodeAutoLockOptions.map(option => {
return (
<a
className={`sk-a info ${option.value === selectedAutoLockInterval ? 'boxed' : ''}`}
onClick={() => selectAutoLockInterval(option.value)}>
{option.label}
</a>
);
})}
</div>
</div>
<div className='sk-p'>The autolock timer begins when the window or tab loses focus.</div>
<div className='sk-panel-row' />
<a className='sk-a info sk-panel-row condensed' onClick={changePasscodePressed}>
Change Passcode
</a>
<a className='sk-a danger sk-panel-row condensed' onClick={removePasscodePressed}>
Remove Passcode
</a>
</div>
</div>
</>
)}
</div>
{!isImportDataLoading && (
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Data Backups</div>
<div className='sk-p'>Download a backup of all your data.</div>
{isEncryptionEnabled && (
<form className='sk-panel-form sk-panel-row'>
<div className='sk-input-group'>
<label className='sk-horizontal-group tight'>
<input
type='radio'
onChange={() => setIsBackupEncrypted(true)}
checked={isBackupEncrypted}
/>
<p className='sk-p'>Encrypted</p>
</label>
<label className='sk-horizontal-group tight'>
<input
type='radio'
onChange={() => setIsBackupEncrypted(false)}
checked={!isBackupEncrypted}
/>
<p className='sk-p'>Decrypted</p>
</label>
</div>
</form>
)}
<div className='sk-panel-row' />
<div className='flex'>
<button className='sn-button small info' onClick={downloadDataArchive}>Download Backup</button>
<label className='sn-button small flex items-center info ml-2'>
<input
type='file'
onChange={importFileSelected}
style={{ display: 'none' }}
/>
Import Backup
</label>
</div>
{isDesktopApplication() && (
<p className='mt-5'>
Backups are automatically created on desktop and can be managed
via the "Backups" top-level menu.
</p>
)}
<div className='sk-panel-row' />
{isImportDataLoading && (
<div className='sk-spinner small info' />
)}
</div>
)}
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Error Reporting</div>
<div className='sk-panel-section-subtitle info'>
Automatic error reporting is {isErrorReportingEnabled ? 'enabled' : 'disabled'}
</div>
<p className='sk-p'>
Help us improve Standard Notes by automatically submitting
anonymized error reports.
</p>
{errorReportingIdValue && (
<>
<p className='sk-p selectable'>
Your random identifier is
strong {errorReportingIdValue}
</p>
<p className='sk-p'>
Disabling error reporting will remove that identifier from your
local storage, and a new identifier will be created should you
decide to enable error reporting again in the future.
</p>
</>
)}
<div className='sk-panel-row'>
<button className='sn-button small info' onClick={toggleErrorReportingEnabled}>
{isErrorReportingEnabled ? 'Disable' : 'Enable'} Error Reporting
{!isAuthenticating && (
<div className='sk-panel-section form-submit'>
<button className='sn-button info text-base py-3 text-center' type='submit'
disabled={isAuthenticating}>
{showLogin ? 'Sign In' : 'Register'}
</button>
</div>
<div className='sk-panel-row'>
<a className='sk-a' onClick={openErrorReportingDialog}>What data is being sent?</a>
)}
{showRegister && (
<div className='sk-notification neutral'>
<div className='sk-notification-title'>No Password Reset.</div>
<div className='sk-notification-text'>
Because your notes are encrypted using your password,
Standard Notes does not have a password reset option.
You cannot forget your password.
</div>
</div>
)}
{status && (
<div className='sk-panel-section no-bottom-pad'>
<div className='sk-horizontal-group'>
<div className='sk-spinner small neutral' />
<div className='sk-label'>{status}</div>
</div>
</div>
)}
{!isAuthenticating && (
<div className='sk-panel-section no-bottom-pad'>
<label className='sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'>
<input
type='checkbox'
checked={!isEphemeral}
onChange={() => setIsEphemeral(prevState => !prevState)}
/>
<p className='sk-p'>Stay signed in</p>
</div>
</label>
{notesAndTagsCount > 0 && (
<label className='sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'>
<input
type='checkbox'
checked={shouldMergeLocal}
onChange={handleMergeLocalData}
/>
<p className='sk-p'>Merge local data ({notesAndTagsCount}) notes and tags</p>
</div>
</label>
)}
</div>
)}
</form>
</div>
)}
{!showLogin && !showRegister && (
<div>
{user && (
<div className='sk-panel-section'>
{syncError && (
<div className='sk-notification danger'>
<div className='sk-notification-title'>Sync Unreachable</div>
<div className='sk-notification-text'>
Hmm...we can't seem to sync your account.
The reason: {syncError}
</div>
<a
className='sk-a info-contrast sk-bold sk-panel-row'
href='https://standardnotes.org/help'
rel='noopener'
target='_blank'
>
Need help?
</a>
</div>
)}
<div className='sk-panel-row'>
<div className='sk-panel-column'>
<div className='sk-h1 sk-bold wrap'>
{user.email}
</div>
<div className='sk-subtitle neutral'>
{server}
</div>
</div>
</div>
<div className='sk-panel-row' />
<a className='sk-a info sk-panel-row condensed' onClick={openPasswordWizard}>
Change Password
</a>
<a className='sk-a info sk-panel-row condensed' onClick={openSessionsModal}>
Manage Sessions
</a>
</div>
)}
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>
Encryption
</div>
{isEncryptionEnabled && (
<div className='sk-panel-section-subtitle info'>
{getEncryptionStatusForNotes()}
</div>
)}
<p className='sk-p'>
{encryptionStatusString}
</p>
</div>
)}
</div>
<div className='sk-panel-footer'>
<div className='sk-panel-row'>
<div className='sk-p left neutral'>
<span>{appVersion}</span>
{showBetaWarning && (
<span>
<a className='sk-a' onClick={disableBetaWarning}>Hide beta warning</a>
</span>
{hasProtections && (
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Protections</div>
{protectionsDisabledUntil && (
<div className='sk-panel-section-subtitle info'>
Protections are disabled until {protectionsDisabledUntil}
</div>
)}
{!protectionsDisabledUntil && (
<div className='sk-panel-section-subtitle info'>
Protections are enabled
</div>
)}
<p className='sk-p'>
Actions like viewing protected notes, exporting decrypted backups,
or revoking an active session, require additional authentication
like entering your account password or application passcode.
</p>
{protectionsDisabledUntil && (
<div className='sk-panel-row'>
<button className='sn-button small info' onClick={enableProtections}>
Enable protections
</button>
</div>
)}
</div>
)}
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Passcode Lock</div>
{!hasPasscode && (
<div>
{canAddPasscode && (
<>
{!showPasscodeForm && (
<div className='sk-panel-row'>
<button className='sn-button small info' onClick={handleAddPassCode}>
Add Passcode
</button>
</div>
)}
<p className='sk-p'>
Add a passcode to lock the application and
encrypt on-device key storage.
</p>
{keyStorageInfo && (
<p>{keyStorageInfo}</p>
)}
</>
)}
{!canAddPasscode && (
<p className='sk-p'>
Adding a passcode is not supported in temporary sessions. Please sign
out, then sign back in with the "Stay signed in" option checked.
</p>
)}
</div>
)}
{showPasscodeForm && (
<form className='sk-panel-form' onSubmit={submitPasscodeForm}>
<div className='sk-panel-row' />
<input
className='sk-input contrast'
type='password'
ref={passcodeInputRef}
value={passcode}
onChange={handlePasscodeChange}
placeholder='Passcode'
/>
<input
className='sk-input contrast'
type='password'
value={passcodeConfirmation}
onChange={handleConfirmPasscodeChange}
placeholder='Confirm Passcode'
/>
<button className='sn-button small info mt-2' onClick={submitPasscodeForm}>
Set Passcode
</button>
<button className='sn-button small outlined ml-2' onClick={() => setShowPasscodeForm(false)}>
Cancel
</button>
</form>
)}
{hasPasscode && !showPasscodeForm && (
<>
<div className='sk-panel-section-subtitle info'>Passcode lock is enabled</div>
<div className='sk-notification contrast'>
<div className='sk-notification-title'>Options</div>
<div className='sk-notification-text'>
<div className='sk-panel-row'>
<div className='sk-horizontal-group'>
<div className='sk-h4 sk-bold'>Autolock</div>
{passcodeAutoLockOptions.map(option => {
return (
<a
className={`sk-a info ${option.value === selectedAutoLockInterval ? 'boxed' : ''}`}
onClick={() => selectAutoLockInterval(option.value)}>
{option.label}
</a>
);
})}
</div>
</div>
<div className='sk-p'>The autolock timer begins when the window or tab loses focus.</div>
<div className='sk-panel-row' />
<a className='sk-a info sk-panel-row condensed' onClick={changePasscodePressed}>
Change Passcode
</a>
<a className='sk-a danger sk-panel-row condensed' onClick={removePasscodePressed}>
Remove Passcode
</a>
</div>
</div>
</>
)}
</div>
{(showLogin || showRegister) && (
<a className='sk-a right' onClick={hidePasswordForm}>Cancel</a>
{!isImportDataLoading && (
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Data Backups</div>
<div className='sk-p'>Download a backup of all your data.</div>
{isEncryptionEnabled && (
<form className='sk-panel-form sk-panel-row'>
<div className='sk-input-group'>
<label className='sk-horizontal-group tight'>
<input
type='radio'
onChange={() => setIsBackupEncrypted(true)}
checked={isBackupEncrypted}
/>
<p className='sk-p'>Encrypted</p>
</label>
<label className='sk-horizontal-group tight'>
<input
type='radio'
onChange={() => setIsBackupEncrypted(false)}
checked={!isBackupEncrypted}
/>
<p className='sk-p'>Decrypted</p>
</label>
</div>
</form>
)}
<div className='sk-panel-row' />
<div className='flex'>
<button className='sn-button small info' onClick={downloadDataArchive}>Download Backup</button>
<label className='sn-button small flex items-center info ml-2'>
<input
type='file'
onChange={importFileSelected}
style={{ display: 'none' }}
/>
Import Backup
</label>
</div>
{isDesktopApplication() && (
<p className='mt-5'>
Backups are automatically created on desktop and can be managed
via the "Backups" top-level menu.
</p>
)}
<div className='sk-panel-row' />
{isImportDataLoading && (
<div className='sk-spinner small info' />
)}
</div>
)}
{!showLogin && !showRegister && (
<a className='sk-a right danger capitalize' onClick={signOut}>
{user ? 'Sign out' : 'Clear session data'}
</a>
<div className='sk-panel-section'>
<div className='sk-panel-section-title'>Error Reporting</div>
<div className='sk-panel-section-subtitle info'>
Automatic error reporting is {isErrorReportingEnabled ? 'enabled' : 'disabled'}
</div>
<p className='sk-p'>
Help us improve Standard Notes by automatically submitting
anonymized error reports.
</p>
{errorReportingIdValue && (
<>
<p className='sk-p selectable'>
Your random identifier is
strong {errorReportingIdValue}
</p>
<p className='sk-p'>
Disabling error reporting will remove that identifier from your
local storage, and a new identifier will be created should you
decide to enable error reporting again in the future.
</p>
</>
)}
<div className='sk-panel-row'>
<button className='sn-button small info' onClick={toggleErrorReportingEnabled}>
{isErrorReportingEnabled ? 'Disable' : 'Enable'} Error Reporting
</button>
</div>
<div className='sk-panel-row'>
<a className='sk-a' onClick={openErrorReportingDialog}>What data is being sent?</a>
</div>
</div>
</div>
)}
</div>
<ConfirmSignoutContainer application={application} appState={appState} />
<div className='sk-panel-footer'>
<div className='sk-panel-row'>
<div className='sk-p left neutral'>
<span>{appVersion}</span>
{showBetaWarning && (
<span>
<a className='sk-a' onClick={disableBetaWarning}>Hide beta warning</a>
</span>
)}
</div>
{(showLogin || showRegister) && (
<a className='sk-a right' onClick={hidePasswordForm}>Cancel</a>
)}
{!showLogin && !showRegister && (
<a className='sk-a right danger capitalize' onClick={signOut}>
{user ? 'Sign out' : 'Clear session data'}
</a>
)}
</div>
</div>
</div>
@@ -986,7 +978,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
);
});
export const AccountMenuReact = toDirective<Props>(
export const AccountMenuDirective = toDirective<Props>(
AccountMenu,
{ closeAccountMenu: '&' }
);

View File

@@ -15,13 +15,10 @@ type Props = {
appState: AppState;
};
const ConfirmSignoutContainer = observer((props: Props) => {
export const ConfirmSignoutContainer = observer((props: Props) => {
if (!props.appState.accountMenu.signingOut) {
return null;
}
if (!props.appState.accountMenuReact.signingOut) {
return null;
}
return <ConfirmSignoutModal {...props} />;
});
@@ -33,14 +30,13 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
const cancelRef = useRef<HTMLButtonElement>();
function close() {
appState.accountMenu.setSigningOut(false);
appState.accountMenuReact.setSigningOut(false);
}
const [localBackupsCount, setLocalBackupsCount] = useState(0);
useEffect(() => {
application.bridge.localBackupsCount().then(setLocalBackupsCount);
}, [appState.accountMenu.signingOut, appState.accountMenuReact.signingOut, application.bridge]);
}, [appState.accountMenu.signingOut, application.bridge]);
return (
<AlertDialog onDismiss={close} leastDestructiveRef={cancelRef}>

View File

@@ -21,7 +21,6 @@ const NoAccountWarning = observer(({ appState }: Props) => {
onClick={(event) => {
event.stopPropagation();
appState.accountMenu.setShow(true);
appState.accountMenuReact.setShow(true);
}}
>
Open Account menu

View File

@@ -16,7 +16,6 @@ function NoProtectionsNoteWarning({ appState, onViewNote }: Props) {
className="sn-button small info"
onClick={() => {
appState.accountMenu.setShow(true);
appState.accountMenuReact.setShow(true);
}}
>
Open account menu

View File

@@ -1,608 +0,0 @@
import { WebDirective } from './../../types';
import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils';
import template from '%/directives/account-menu.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_E2E_ENABLED,
STRING_LOCAL_ENC_ENABLED,
STRING_ENC_NOT_ENABLED,
STRING_IMPORT_SUCCESS,
STRING_NON_MATCHING_PASSCODES,
STRING_NON_MATCHING_PASSWORDS,
STRING_INVALID_IMPORT_FILE,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
StringImportError,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
STRING_UNSUPPORTED_BACKUP_FILE_VERSION,
StringUtils,
} from '@/strings';
import { PasswordWizardType } from '@/types';
import {
ApplicationEvent,
BackupFile,
ContentType,
} from '@standardnotes/snjs';
import { confirmDialog, alertDialog } from '@/services/alertService';
import { storage, StorageKey } from '@/services/localStorage';
import {
disableErrorReporting,
enableErrorReporting,
errorReportingId,
} from '@/services/errorReporting';
const ELEMENT_NAME_AUTH_EMAIL = 'email';
const ELEMENT_NAME_AUTH_PASSWORD = 'password';
const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf';
type FormData = {
email: string;
user_password: string;
password_conf: string;
confirmPassword: boolean;
showLogin: boolean;
showRegister: boolean;
showPasscodeForm: boolean;
strictSignin?: boolean;
ephemeral: boolean;
mergeLocal?: boolean;
url: string;
authenticating: boolean;
status: string;
passcode: string;
confirmPasscode: string;
changingPasscode: boolean;
};
type AccountMenuState = {
formData: Partial<FormData>;
appVersion: string;
passcodeAutoLockOptions: any;
user: any;
mutable: any;
importData: any;
encryptionStatusString?: string;
server?: string;
encryptionEnabled?: boolean;
selectedAutoLockInterval?: unknown;
showBetaWarning: boolean;
errorReportingEnabled: boolean;
syncInProgress: boolean;
syncError?: string;
showSessions: boolean;
errorReportingId: string | null;
keyStorageInfo: string | null;
protectionsDisabledUntil: string | null;
};
class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
public appVersion: string;
/** @template */
private closeFunction?: () => void;
private removeProtectionLengthObserver?: () => void;
public passcodeInput!: JQLite;
/* @ngInject */
constructor($timeout: ng.ITimeoutService, appVersion: string) {
super($timeout);
this.appVersion = appVersion;
}
/** @override */
getInitialState() {
return {
appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion),
passcodeAutoLockOptions: this.application
.getAutolockService()
.getAutoLockIntervalOptions(),
user: this.application.getUser(),
formData: {
mergeLocal: true,
ephemeral: false,
},
mutable: {},
showBetaWarning: false,
errorReportingEnabled:
storage.get(StorageKey.DisableErrorReporting) === false,
showSessions: false,
errorReportingId: errorReportingId(),
keyStorageInfo: StringUtils.keyStorageInfo(this.application),
importData: null,
syncInProgress: false,
protectionsDisabledUntil: this.getProtectionsDisabledUntil(),
};
}
getState() {
return this.state as AccountMenuState;
}
async onAppKeyChange() {
super.onAppKeyChange();
this.setState(this.refreshedCredentialState());
}
async onAppLaunch() {
super.onAppLaunch();
this.setState(this.refreshedCredentialState());
this.loadHost();
this.reloadAutoLockInterval();
this.refreshEncryptionStatus();
}
refreshedCredentialState() {
return {
user: this.application.getUser(),
canAddPasscode: !this.application.isEphemeralSession(),
hasPasscode: this.application.hasPasscode(),
showPasscodeForm: false,
};
}
async $onInit() {
super.$onInit();
this.setState({
showSessions: await this.application.userCanManageSessions(),
});
const sync = this.appState.sync;
this.autorun(() => {
this.setState({
syncInProgress: sync.inProgress,
syncError: sync.errorMessage,
});
});
this.autorun(() => {
this.setState({
showBetaWarning: this.appState.showBetaWarning,
});
});
this.removeProtectionLengthObserver = this.application.addEventObserver(
async () => {
this.setState({
protectionsDisabledUntil: this.getProtectionsDisabledUntil(),
});
},
ApplicationEvent.ProtectionSessionExpiryDateChanged
);
}
deinit() {
this.removeProtectionLengthObserver?.();
super.deinit();
}
close() {
this.$timeout(() => {
this.closeFunction?.();
});
}
hasProtections() {
return this.application.hasProtectionSources();
}
private getProtectionsDisabledUntil(): string | null {
const protectionExpiry = this.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;
}
async loadHost() {
const host = await this.application.getHost();
this.setState({
server: host,
formData: {
...this.getState().formData,
url: host,
},
});
}
enableProtections() {
this.application.clearProtectionSession();
}
onHostInputChange() {
const url = this.getState().formData.url!;
this.application!.setHost(url);
}
refreshEncryptionStatus() {
const hasUser = this.application!.hasAccount();
const hasPasscode = this.application!.hasPasscode();
const encryptionEnabled = hasUser || hasPasscode;
this.setState({
encryptionStatusString: hasUser
? STRING_E2E_ENABLED
: hasPasscode
? STRING_LOCAL_ENC_ENABLED
: STRING_ENC_NOT_ENABLED,
encryptionEnabled,
mutable: {
...this.getState().mutable,
backupEncrypted: encryptionEnabled,
},
});
}
submitMfaForm() {
this.login();
}
blurAuthFields() {
const names = [
ELEMENT_NAME_AUTH_EMAIL,
ELEMENT_NAME_AUTH_PASSWORD,
ELEMENT_NAME_AUTH_PASSWORD_CONF,
];
for (const name of names) {
const element = document.getElementsByName(name)[0];
if (element) {
element.blur();
}
}
}
submitAuthForm() {
if (
!this.getState().formData.email ||
!this.getState().formData.user_password
) {
return;
}
this.blurAuthFields();
if (this.getState().formData.showLogin) {
this.login();
} else {
this.register();
}
}
async setFormDataState(formData: Partial<FormData>) {
return this.setState({
formData: {
...this.getState().formData,
...formData,
},
});
}
async login() {
await this.setFormDataState({
status: STRING_GENERATING_LOGIN_KEYS,
authenticating: true,
});
const formData = this.getState().formData;
const response = await this.application!.signIn(
formData.email!,
formData.user_password!,
formData.strictSignin,
formData.ephemeral,
formData.mergeLocal
);
const error = response.error;
if (!error) {
await this.setFormDataState({
authenticating: false,
user_password: undefined,
});
this.close();
return;
}
await this.setFormDataState({
showLogin: true,
status: undefined,
user_password: undefined,
});
if (error.message) {
this.application!.alertService!.alert(error.message);
}
await this.setFormDataState({
authenticating: false,
});
}
async register() {
const confirmation = this.getState().formData.password_conf;
if (confirmation !== this.getState().formData.user_password) {
this.application!.alertService!.alert(STRING_NON_MATCHING_PASSWORDS);
return;
}
await this.setFormDataState({
confirmPassword: false,
status: STRING_GENERATING_REGISTER_KEYS,
authenticating: true,
});
const response = await this.application!.register(
this.getState().formData.email!,
this.getState().formData.user_password!,
this.getState().formData.ephemeral,
this.getState().formData.mergeLocal
);
const error = response.error;
if (error) {
await this.setFormDataState({
status: undefined,
});
await this.setFormDataState({
authenticating: false,
});
this.application!.alertService!.alert(error.message);
} else {
await this.setFormDataState({ authenticating: false });
this.close();
}
}
async mergeLocalChanged() {
if (!this.getState().formData.mergeLocal) {
this.setFormDataState({
mergeLocal: !(await confirmDialog({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
confirmButtonStyle: 'danger',
})),
});
}
}
openPasswordWizard() {
this.close();
this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword);
}
openSessionsModal() {
this.close();
this.appState.openSessionsModal();
}
signOut() {
this.appState.accountMenu.setSigningOut(true);
}
showRegister() {
this.setFormDataState({
showRegister: true,
});
}
async readFile(file: File): Promise<any> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target!.result as string);
resolve(data);
} catch (e) {
this.application!.alertService!.alert(STRING_INVALID_IMPORT_FILE);
}
};
reader.readAsText(file);
});
}
/**
* @template
*/
async importFileSelected(files: File[]) {
const file = files[0];
const data = await this.readFile(file);
if (!data) {
return;
}
if (data.version || data.auth_params || data.keyParams) {
const version =
data.version || data.keyParams?.version || data.auth_params?.version;
if (
this.application.protocolService.supportedVersions().includes(version)
) {
await this.performImport(data);
} else {
await this.setState({ importData: null });
void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
}
} else {
await this.performImport(data);
}
}
async performImport(data: BackupFile) {
await this.setState({
importData: {
...this.getState().importData,
loading: true,
},
});
const result = await this.application.importData(data);
this.setState({
importData: null,
});
if (!result) {
return;
} else if ('error' in result) {
void alertDialog({
text: result.error,
});
} else if (result.errorCount) {
void alertDialog({
text: StringImportError(result.errorCount),
});
} else {
void alertDialog({
text: STRING_IMPORT_SUCCESS,
});
}
}
async downloadDataArchive() {
this.application
.getArchiveService()
.downloadBackup(this.getState().mutable.backupEncrypted);
}
notesAndTagsCount() {
return this.application.getItems([ContentType.Note, ContentType.Tag])
.length;
}
encryptionStatusForNotes() {
const length = this.notesAndTagsCount();
return length + '/' + length + ' notes and tags encrypted';
}
async reloadAutoLockInterval() {
const interval = await this.application!.getAutolockService().getAutoLockInterval();
this.setState({
selectedAutoLockInterval: interval,
});
}
async selectAutoLockInterval(interval: number) {
if (!(await this.application.authorizeAutolockIntervalChange())) {
return;
}
await this.application!.getAutolockService().setAutoLockInterval(interval);
this.reloadAutoLockInterval();
}
hidePasswordForm() {
this.setFormDataState({
showLogin: false,
showRegister: false,
user_password: undefined,
password_conf: undefined,
});
}
hasPasscode() {
return this.application!.hasPasscode();
}
addPasscodeClicked() {
this.setFormDataState({
showPasscodeForm: true,
});
}
async submitPasscodeForm() {
const passcode = this.getState().formData.passcode!;
if (passcode !== this.getState().formData.confirmPasscode!) {
await alertDialog({
text: STRING_NON_MATCHING_PASSCODES,
});
this.passcodeInput[0].focus();
return;
}
await preventRefreshing(
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
async () => {
const successful = this.application.hasPasscode()
? await this.application.changePasscode(passcode)
: await this.application.addPasscode(passcode);
if (!successful) {
this.passcodeInput[0].focus();
}
}
);
this.setFormDataState({
passcode: undefined,
confirmPasscode: undefined,
showPasscodeForm: false,
});
this.refreshEncryptionStatus();
}
async changePasscodePressed() {
this.getState().formData.changingPasscode = true;
this.addPasscodeClicked();
}
async removePasscodePressed() {
await preventRefreshing(
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
async () => {
if (await this.application!.removePasscode()) {
await this.application
.getAutolockService()
.deleteAutolockPreference();
await this.reloadAutoLockInterval();
this.refreshEncryptionStatus();
}
}
);
}
openErrorReportingDialog() {
alertDialog({
title: 'Data sent during automatic error reporting',
text: `
We use <a target="_blank" rel="noreferrer" href="https://www.bugsnag.com/">Bugsnag</a>
to automatically report errors that occur while the app is running. See
<a target="_blank" rel="noreferrer" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
this article, paragraph 'Browser' under 'Sending diagnostic data',
</a>
to see what data is included in error reports.
<br><br>
Error reports never include IP addresses and are fully
anonymized. We use error reports to be alerted when something in our
code is causing unexpected errors and crashes in your application
experience.
`,
});
}
toggleErrorReportingEnabled() {
if (this.state.errorReportingEnabled) {
disableErrorReporting();
} else {
enableErrorReporting();
}
if (!this.state.syncInProgress) {
window.location.reload();
}
}
isDesktopApplication() {
return isDesktopApplication();
}
}
export class AccountMenu extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = AccountMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
closeFunction: '&',
application: '=',
};
}
}

View File

@@ -1,4 +1,3 @@
export { AccountMenu } from './accountMenu';
export { ActionsMenu } from './actionsMenu';
export { ComponentModal } from './componentModal';
export { ComponentView } from './componentView';

View File

@@ -1,33 +0,0 @@
import { action, makeObservable, observable } from "mobx";
import { WebApplication } from '@/ui_models/application';
export class AccountMenuStateReact {
show = false;
signingOut = false;
constructor(
// private application: WebApplication,
// appEventListeners: (() => void)[]
) {
makeObservable(this, {
show: observable,
signingOut: observable,
setShow: action,
toggleShow: action,
setSigningOut: action,
});
}
setShow = (show: boolean): void => {
this.show = show;
}
setSigningOut = (signingOut: boolean): void => {
this.signingOut = signingOut;
}
toggleShow = (): void => {
this.show = !this.show;
}
}

View File

@@ -14,14 +14,13 @@ import { Editor } from '@/ui_models/editor';
import { action, makeObservable, observable } from 'mobx';
import { Bridge } from '@/services/bridge';
import { storage, StorageKey } from '@/services/localStorage';
import { AccountMenuState } from './account_menu_state';
import { ActionsMenuState } from './actions_menu_state';
import { NoAccountWarningState } from './no_account_warning_state';
import { SyncState } from './sync_state';
import { SearchOptionsState } from './search_options_state';
import { NotesState } from './notes_state';
import { TagsState } from './tags_state';
import { AccountMenuStateReact } from '@/ui_models/app_state/account_menu_react_state';
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
export enum AppStateEvent {
TagChanged,
@@ -61,8 +60,7 @@ export class AppState {
onVisibilityChange: any;
selectedTag?: SNTag;
showBetaWarning: boolean;
readonly accountMenu = new AccountMenuState();
readonly accountMenuReact: AccountMenuStateReact;
readonly accountMenu: AccountMenuState;
readonly actionsMenu = new ActionsMenuState();
readonly noAccountWarning: NoAccountWarningState;
readonly sync = new SyncState();
@@ -98,7 +96,7 @@ export class AppState {
application,
this.appEventObserverRemovers
);
this.accountMenuReact = new AccountMenuStateReact();
this.accountMenu = new AccountMenuState();
this.searchOptions = new SearchOptionsState(
application,
this.appEventObserverRemovers

View File

@@ -15,7 +15,7 @@ class EditorGroupViewCtrl extends PureViewCtrl<unknown, {
super($timeout);
this.state = {
showMultipleSelectedNotes: false
}
};
}
$onInit() {

View File

@@ -13,12 +13,6 @@
.sk-app-bar-item-column
.sk-label.title(ng-class='{red: ctrl.hasError}') Account
account-menu(
close-function='ctrl.closeAccountMenu()',
ng-click='$event.stopPropagation()',
ng-if='ctrl.showAccountMenu',
application='ctrl.application'
)
account-menu-react(
ng-click='$event.stopPropagation()',
app-state='ctrl.appState'
application='ctrl.application'

View File

@@ -62,7 +62,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
public user?: any
private offline = true
public showAccountMenu = false
public showAccountMenuReact = false
private didCheckForOffline = false
private queueExtReload = false
private reloadInProgress = false
@@ -116,7 +115,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
this.autorun(() => {
const showBetaWarning = this.appState.showBetaWarning;
this.showAccountMenu = this.appState.accountMenu.show;
this.showAccountMenuReact = this.appState.accountMenuReact.show;
this.setState({
showBetaWarning: showBetaWarning,
showDataUpgrade: !showBetaWarning
@@ -256,7 +254,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
this.didCheckForOffline = true;
if (this.offline && this.application.getNoteCount() === 0) {
this.appState.accountMenu.setShow(true);
this.appState.accountMenuReact.setShow(true);
}
}
this.syncUpdated();
@@ -439,7 +436,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
accountMenuPressed() {
this.appState.accountMenu.toggleShow();
this.appState.accountMenuReact.toggleShow();
this.closeAllRooms();
}
@@ -449,7 +445,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
closeAccountMenu() {
this.appState.accountMenu.setShow(false);
this.appState.accountMenuReact.setShow(false);
}
lockApp() {
@@ -567,7 +562,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
return;
}
this.appState.accountMenu.setShow(false);
this.appState.accountMenuReact.setShow(false);
}
}

View File

@@ -56,7 +56,6 @@
}
#account-panel,
#account-panel-react,
#sync-resolution-menu {
width: 400px;
}

View File

@@ -1,324 +0,0 @@
.sn-component
#account-panel.sk-panel
.sk-panel-header
.sk-panel-header-title Account
a.sk-a.info.close-button(ng-click='self.close()') Close
.sk-panel-content
.sk-panel-section.sk-panel-hero(
ng-if=`
!self.state.user &&
!self.state.formData.showLogin &&
!self.state.formData.showRegister`
)
.sk-panel-row
.sk-h1 Sign in or register to enable sync and end-to-end encryption.
.flex.my-1
button(
class="sn-button info flex-grow text-base py-3 mr-1.5"
ng-click='self.state.formData.showLogin = true'
) Sign In
button(
class="sn-button info flex-grow text-base py-3 ml-1.5"
ng-click='self.showRegister()'
) Register
.sk-panel-row.sk-p
| Standard Notes is free on every platform, and comes
| standard with sync and encryption.
.sk-panel-section(ng-if=`
self.state.formData.showLogin ||
self.state.formData.showRegister`
)
.sk-panel-section-title
| {{self.state.formData.showLogin ? "Sign In" : "Register"}}
form.sk-panel-form(ng-submit='self.submitAuthForm()' novalidate)
.sk-panel-section
input.sk-input.contrast(
name='email',
ng-model='self.state.formData.email',
ng-model-options='{allowInvalid: true}',
placeholder='Email',
required='',
should-focus='true',
sn-autofocus='true',
spellcheck='false',
type='email'
)
input.sk-input.contrast(
name='password',
ng-model='self.state.formData.user_password',
placeholder='Password',
required='',
sn-enter='self.submitAuthForm()',
type='password'
)
input.sk-input.contrast(
name='password_conf',
ng-if='self.state.formData.showRegister',
ng-model='self.state.formData.password_conf',
placeholder='Confirm Password',
required='',
sn-enter='self.submitAuthForm()',
type='password'
)
.sk-panel-row
a.sk-panel-row.sk-bold(
ng-click=`
self.state.formData.showAdvanced = !self.state.formData.showAdvanced
`
)
| Advanced Options
.sk-notification.unpadded.contrast.advanced-options.sk-panel-row(
ng-if='self.state.formData.showAdvanced'
)
.sk-panel-column.stretch
.sk-notification-title.sk-panel-row.padded-row Advanced Options
.bordered-row.padded-row
label.sk-label Sync Server Domain
input.sk-input.sk-base(
name='server',
ng-model='self.state.formData.url',
ng-change='self.onHostInputChange()'
placeholder='Server URL',
required='',
type='text'
)
label.sk-label.padded-row.sk-panel-row.justify-left(
ng-if='self.state.formData.showLogin'
)
.sk-horizontal-group.tight
input.sk-input(
ng-model='self.state.formData.strictSignin',
type='checkbox'
)
p.sk-p Use strict sign in
span
a.info(
href='https://standardnotes.org/help/security',
rel='noopener',
target='_blank'
) (Learn more)
.sk-panel-section.form-submit(ng-if='!self.state.formData.authenticating')
button.sn-button.info.text-base.py-3.text-center(
type="submit"
ng-disabled='self.state.formData.authenticating'
) {{self.state.formData.showLogin ? "Sign In" : "Register"}}
.sk-notification.neutral(ng-if='self.state.formData.showRegister')
.sk-notification-title No Password Reset.
.sk-notification-text
| Because your notes are encrypted using your password,
| Standard Notes does not have a password reset option.
| You cannot forget your password.
.sk-panel-section.no-bottom-pad(ng-if='self.state.formData.status')
.sk-horizontal-group
.sk-spinner.small.neutral
.sk-label {{self.state.formData.status}}
.sk-panel-section.no-bottom-pad(ng-if='!self.state.formData.authenticating')
label.sk-panel-row.justify-left
.sk-horizontal-group.tight
input(
ng-false-value='true',
ng-model='self.state.formData.ephemeral',
ng-true-value='false',
type='checkbox'
)
p.sk-p Stay signed in
label.sk-panel-row.justify-left(ng-if='self.notesAndTagsCount() > 0')
.sk-horizontal-group.tight
input(
ng-bind='true',
ng-change='self.mergeLocalChanged()',
ng-model='self.state.formData.mergeLocal',
type='checkbox'
)
p.sk-p Merge local data ({{self.notesAndTagsCount()}} notes and tags)
div(
ng-if=`
!self.state.formData.showLogin &&
!self.state.formData.showRegister`
)
.sk-panel-section(ng-if='self.state.user')
.sk-notification.danger(ng-if='self.state.syncError')
.sk-notification-title Sync Unreachable
.sk-notification-text
| Hmm...we can't seem to sync your account.
| The reason: {{self.state.syncError}}
a.sk-a.info-contrast.sk-bold.sk-panel-row(
href='https://standardnotes.org/help',
rel='noopener',
target='_blank'
) Need help?
.sk-panel-row
.sk-panel-column
.sk-h1.sk-bold.wrap {{self.state.user.email}}
.sk-subtitle.neutral {{self.state.server}}
.sk-panel-row
a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openPasswordWizard()"
) Change Password
a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openSessionsModal()"
) Manage Sessions
.sk-panel-section
.sk-panel-section-title Encryption
.sk-panel-section-subtitle.info(ng-if='self.state.encryptionEnabled')
| {{self.encryptionStatusForNotes()}}
p.sk-p
| {{self.state.encryptionStatusString}}
.sk-panel-section(ng-if="self.hasProtections()")
.sk-panel-section-title Protections
.sk-panel-section-subtitle.info(ng-if="self.state.protectionsDisabledUntil")
| Protections are disabled until {{self.state.protectionsDisabledUntil}}
.sk-panel-section-subtitle.info(ng-if="!self.state.protectionsDisabledUntil")
| Protections are enabled
p.sk-p
| Actions like viewing protected notes, exporting decrypted backups,
| or revoking an active session, require additional authentication
| like entering your account password or application passcode.
.sk-panel-row(ng-if="self.state.protectionsDisabledUntil")
button.sn-button.small.info(ng-click="self.enableProtections()")
| Enable protections
.sk-panel-section
.sk-panel-section-title Passcode Lock
div(ng-if='!self.state.hasPasscode')
div(ng-if='self.state.canAddPasscode')
.sk-panel-row(ng-if='!self.state.formData.showPasscodeForm')
button.sn-button.small.info(
ng-click='self.addPasscodeClicked(); $event.stopPropagation();'
) Add Passcode
p.sk-p
| Add a passcode to lock the application and
| encrypt on-device key storage.
p(ng-if='self.state.keyStorageInfo')
| {{self.state.keyStorageInfo}}
div(ng-if='!self.state.canAddPasscode')
p.sk-p
| Adding a passcode is not supported in temporary sessions. Please sign
| out, then sign back in with the "Stay signed in" option checked.
form.sk-panel-form(
ng-if='self.state.formData.showPasscodeForm',
ng-submit='self.submitPasscodeForm()'
)
.sk-panel-row
input.sk-input.contrast(
ng-ref='self.passcodeInput'
ng-model='self.state.formData.passcode'
placeholder='Passcode'
should-focus='true'
sn-autofocus='true'
type='password'
)
input.sk-input.contrast(
ng-model='self.state.formData.confirmPasscode',
placeholder='Confirm Passcode',
type='password'
)
button.sn-button.small.info.mt-2(type='submit') Set Passcode
button.sn-button.small.outlined.ml-2(
ng-click='self.state.formData.showPasscodeForm = false'
) Cancel
div(ng-if='self.state.hasPasscode && !self.state.formData.showPasscodeForm')
.sk-panel-section-subtitle.info Passcode lock is enabled
.sk-notification.contrast
.sk-notification-title Options
.sk-notification-text
.sk-panel-row
.sk-horizontal-group
.sk-h4.sk-bold Autolock
a.sk-a.info(
ng-class=`{
'boxed' : option.value == self.state.selectedAutoLockInterval
}`,
ng-click='self.selectAutoLockInterval(option.value)',
ng-repeat='option in self.state.passcodeAutoLockOptions'
)
| {{option.label}}
.sk-p The autolock timer begins when the window or tab loses focus.
.sk-panel-row
a.sk-a.info.sk-panel-row.condensed(
ng-click='self.changePasscodePressed()'
) Change Passcode
a.sk-a.danger.sk-panel-row.condensed(
ng-click='self.removePasscodePressed()'
) Remove Passcode
.sk-panel-section(ng-if='!self.state.importData.loading')
.sk-panel-section-title Data Backups
.sk-p
| Download a backup of all your data.
form.sk-panel-form.sk-panel-row(ng-if='self.state.encryptionEnabled')
.sk-input-group
label.sk-horizontal-group.tight
input(
ng-change='self.state.mutable.backupEncrypted = true',
ng-model='self.state.mutable.backupEncrypted',
ng-value='true',
type='radio'
)
p.sk-p Encrypted
label.sk-horizontal-group.tight
input(
ng-change='self.state.mutable.backupEncrypted = false',
ng-model='self.state.mutable.backupEncrypted',
ng-value='false',
type='radio'
)
p.sk-p Decrypted
.sk-panel-row
.flex
button.sn-button.small.info(ng-click='self.downloadDataArchive()')
| Download Backup
label.sn-button.small.flex.items-center.info.ml-2
input(
file-change='->',
handler='self.importFileSelected(files)',
style='display: none;',
type='file'
)
| Import Backup
p.mt-5(ng-if='self.isDesktopApplication()')
| Backups are automatically created on desktop and can be managed
| via the "Backups" top-level menu.
.sk-panel-row
.sk-spinner.small.info(ng-if='self.state.importData.loading')
.sk-panel-section
.sk-panel-section-title Error Reporting
.sk-panel-section-subtitle.info
| Automatic error reporting is {{ self.state.errorReportingEnabled ? 'enabled' : 'disabled' }}
p.sk-p
| Help us improve Standard Notes by automatically submitting
| anonymized error reports.
p.sk-p.selectable(ng-if="self.state.errorReportingId")
| Your random identifier is
strong {{ self.state.errorReportingId }}
p.sk-p(ng-if="self.state.errorReportingId")
| Disabling error reporting will remove that identifier from your
| local storage, and a new identifier will be created should you
| decide to enable error reporting again in the future.
.sk-panel-row
button(ng-click="self.toggleErrorReportingEnabled()").sn-button.small.info
| {{ self.state.errorReportingEnabled ? 'Disable' : 'Enable'}} Error Reporting
.sk-panel-row
a(ng-click="self.openErrorReportingDialog()").sk-a What data is being sent?
confirm-signout(
app-state='self.appState'
application='self.application'
)
.sk-panel-footer
.sk-panel-row
.sk-p.left.neutral
span {{self.state.appVersion}}
span(ng-if="self.state.showBetaWarning")
span (
a.sk-a(ng-click="self.appState.disableBetaWarning()") Hide beta warning
span )
a.sk-a.right(
ng-click='self.hidePasswordForm()',
ng-if='self.state.formData.showLogin || self.state.formData.showRegister'
)
| Cancel
a.sk-a.right.danger.capitalize(
ng-click='self.signOut()',
ng-if=`
!self.state.formData.showLogin &&
!self.state.formData.showRegister`
)
| {{ self.state.user ? "Sign out" : "Clear session data" }}