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,17 +527,8 @@ 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 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>
@@ -960,6 +952,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
</div>
)}
</div>
<ConfirmSignoutContainer application={application} appState={appState} />
<div className='sk-panel-footer'>
<div className='sk-panel-row'>
<div className='sk-p left neutral'>
@@ -982,11 +975,10 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
</div>
</div>
</div>
</div>
);
});
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" }}