feat: batch manager protection + react challenge modal + eslint fix
This commit is contained in:
@@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
"extends": ["eslint:recommended", "prettier"],
|
"root": true,
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint", "react"],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": "./app/assets/javascripts/tsconfig.json"
|
"project": "./app/assets/javascripts/tsconfig.json"
|
||||||
},
|
},
|
||||||
|
|||||||
5
.github/workflows/dev.yml
vendored
5
.github/workflows/dev.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
tsc:
|
tsc:
|
||||||
|
|
||||||
name: Check types
|
name: Check types & lint
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -22,6 +22,9 @@ jobs:
|
|||||||
- name: Typescript
|
- name: Typescript
|
||||||
run: yarn tsc
|
run: yarn tsc
|
||||||
|
|
||||||
|
- name: ESLint
|
||||||
|
run: yarn lint --quiet
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
9
.github/workflows/pr.yml
vendored
9
.github/workflows/pr.yml
vendored
@@ -7,12 +7,21 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
tsc:
|
tsc:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --pure-lockfile
|
run: yarn install --pure-lockfile
|
||||||
|
|
||||||
- name: Typescript
|
- name: Typescript
|
||||||
run: yarn tsc
|
run: yarn tsc
|
||||||
|
|
||||||
|
- name: ESLint
|
||||||
|
run: yarn lint --quiet
|
||||||
|
|||||||
5
.github/workflows/prod.yml
vendored
5
.github/workflows/prod.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
tsc:
|
tsc:
|
||||||
|
|
||||||
name: Check types
|
name: Check types & lint
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -23,6 +23,9 @@ jobs:
|
|||||||
- name: Typescript
|
- name: Typescript
|
||||||
run: yarn tsc
|
run: yarn tsc
|
||||||
|
|
||||||
|
- name: ESLint
|
||||||
|
run: yarn lint --quiet
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@standardnotes/snjs';
|
} from '@standardnotes/snjs';
|
||||||
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
|
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
|
||||||
import { render, FunctionComponent } from 'preact';
|
import { render, FunctionComponent } from 'preact';
|
||||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
|
||||||
import { Dialog } from '@reach/dialog';
|
import { Dialog } from '@reach/dialog';
|
||||||
import { Alert } from '@reach/alert';
|
import { Alert } from '@reach/alert';
|
||||||
import {
|
import {
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from '@reach/alert-dialog';
|
} from '@reach/alert-dialog';
|
||||||
|
|
||||||
function useAutorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions) {
|
function useAutorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions) {
|
||||||
useEffect(() => autorun(view, opts), []);
|
useEffect(() => autorun(view, opts), [view, opts]);
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session = RemoteSession & {
|
type Session = RemoteSession & {
|
||||||
@@ -57,16 +57,16 @@ function useSessions(
|
|||||||
}
|
}
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
})();
|
})();
|
||||||
}, [lastRefreshDate]);
|
}, [application, lastRefreshDate]);
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
setLastRefreshDate(Date.now());
|
setLastRefreshDate(Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revokeSession(uuid: UuidString) {
|
async function revokeSession(uuid: UuidString) {
|
||||||
const responsePromise = application.revokeSession(uuid);
|
const sessionsBeforeRevoke = sessions;
|
||||||
|
|
||||||
let sessionsBeforeRevoke = sessions;
|
const responsePromise = application.revokeSession(uuid);
|
||||||
|
|
||||||
const sessionsDuringRevoke = sessions.slice();
|
const sessionsDuringRevoke = sessions.slice();
|
||||||
const toRemoveIndex = sessions.findIndex(
|
const toRemoveIndex = sessions.findIndex(
|
||||||
@@ -114,19 +114,23 @@ const SessionsModal: FunctionComponent<{
|
|||||||
const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
|
const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
|
||||||
const cancelRevokeRef = useRef<HTMLButtonElement>();
|
const cancelRevokeRef = useRef<HTMLButtonElement>();
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
const formatter = useMemo(
|
||||||
year: 'numeric',
|
() =>
|
||||||
month: 'long',
|
new Intl.DateTimeFormat(undefined, {
|
||||||
day: 'numeric',
|
year: 'numeric',
|
||||||
weekday: 'long',
|
month: 'long',
|
||||||
hour: 'numeric',
|
day: 'numeric',
|
||||||
minute: 'numeric',
|
weekday: 'long',
|
||||||
});
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog onDismiss={close}>
|
<Dialog onDismiss={close} className="sessions-modal">
|
||||||
<div className="sk-modal-content sessions-modal">
|
<div className="sk-modal-content">
|
||||||
<div class="sn-component">
|
<div class="sn-component">
|
||||||
<div class="sk-panel">
|
<div class="sk-panel">
|
||||||
<div class="sk-panel-header">
|
<div class="sk-panel-header">
|
||||||
@@ -250,7 +254,7 @@ const Sessions: FunctionComponent<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class SessionsModalCtrl extends PureViewCtrl<{}, {}> {
|
class SessionsModalCtrl extends PureViewCtrl<unknown, unknown> {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(private $element: JQLite, $timeout: ng.ITimeoutService) {
|
constructor(private $element: JQLite, $timeout: ng.ITimeoutService) {
|
||||||
super($timeout);
|
super($timeout);
|
||||||
@@ -264,6 +268,7 @@ class SessionsModalCtrl extends PureViewCtrl<{}, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function SessionsModalDirective() {
|
export function SessionsModalDirective() {
|
||||||
return {
|
return {
|
||||||
controller: SessionsModalCtrl,
|
controller: SessionsModalCtrl,
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ export class BrowserBridge implements Bridge {
|
|||||||
|
|
||||||
/** No-ops */
|
/** No-ops */
|
||||||
|
|
||||||
syncComponents() {}
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
onMajorDataChange() {}
|
syncComponents(): void {}
|
||||||
onInitialDataLoad() {}
|
onMajorDataChange(): void {}
|
||||||
onSearch() {}
|
onInitialDataLoad(): void {}
|
||||||
downloadBackup() {}
|
onSearch(): void {}
|
||||||
|
downloadBackup(): void {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export enum KeyboardKey {
|
|||||||
Backspace = "Backspace",
|
Backspace = "Backspace",
|
||||||
Up = "ArrowUp",
|
Up = "ArrowUp",
|
||||||
Down = "ArrowDown",
|
Down = "ArrowDown",
|
||||||
};
|
}
|
||||||
|
|
||||||
export enum KeyboardModifier {
|
export enum KeyboardModifier {
|
||||||
Shift = "Shift",
|
Shift = "Shift",
|
||||||
@@ -12,12 +12,12 @@ export enum KeyboardModifier {
|
|||||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||||
Meta = "Meta",
|
Meta = "Meta",
|
||||||
Alt = "Alt",
|
Alt = "Alt",
|
||||||
};
|
}
|
||||||
|
|
||||||
enum KeyboardKeyEvent {
|
enum KeyboardKeyEvent {
|
||||||
Down = "KeyEventDown",
|
Down = "KeyEventDown",
|
||||||
Up = "KeyEventUp"
|
Up = "KeyEventUp"
|
||||||
};
|
}
|
||||||
|
|
||||||
type KeyboardObserver = {
|
type KeyboardObserver = {
|
||||||
key?: KeyboardKey | string
|
key?: KeyboardKey | string
|
||||||
@@ -39,10 +39,10 @@ export class KeyboardManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.handleKeyDown = (event: KeyboardEvent) => {
|
this.handleKeyDown = (event: KeyboardEvent) => {
|
||||||
this.notifyObserver(event, KeyboardKeyEvent.Down);
|
this.notifyObserver(event, KeyboardKeyEvent.Down);
|
||||||
}
|
};
|
||||||
this.handleKeyUp = (event: KeyboardEvent) => {
|
this.handleKeyUp = (event: KeyboardEvent) => {
|
||||||
this.notifyObserver(event, KeyboardKeyEvent.Up);
|
this.notifyObserver(event, KeyboardKeyEvent.Up);
|
||||||
}
|
};
|
||||||
window.addEventListener('keydown', this.handleKeyDown);
|
window.addEventListener('keydown', this.handleKeyDown);
|
||||||
window.addEventListener('keyup', this.handleKeyUp);
|
window.addEventListener('keyup', this.handleKeyUp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
ComponentMutator,
|
ComponentMutator,
|
||||||
Copy,
|
Copy,
|
||||||
dictToArray
|
dictToArray
|
||||||
} from '@standardnotes/snjs';
|
, PayloadContent , ComponentPermission } from '@standardnotes/snjs';
|
||||||
import { PayloadContent } from '@standardnotes/snjs';
|
|
||||||
import { ComponentPermission } from '@standardnotes/snjs';
|
|
||||||
|
|
||||||
/** A class for handling installation of system extensions */
|
/** A class for handling installation of system extensions */
|
||||||
export class NativeExtManager extends ApplicationService {
|
export class NativeExtManager extends ApplicationService {
|
||||||
@@ -82,7 +82,7 @@ export class NativeExtManager extends ApplicationService {
|
|||||||
// Handle addition of SN|ExtensionRepo permission
|
// Handle addition of SN|ExtensionRepo permission
|
||||||
const permissions = Copy(extensionsManager!.permissions) as ComponentPermission[];
|
const permissions = Copy(extensionsManager!.permissions) as ComponentPermission[];
|
||||||
const permission = permissions.find((p) => {
|
const permission = permissions.find((p) => {
|
||||||
return p.name === ComponentAction.StreamItems
|
return p.name === ComponentAction.StreamItems;
|
||||||
});
|
});
|
||||||
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
|
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
|
||||||
permission.content_types!.push(ContentType.ExtensionRepo);
|
permission.content_types!.push(ContentType.ExtensionRepo);
|
||||||
@@ -160,7 +160,7 @@ export class NativeExtManager extends ApplicationService {
|
|||||||
// Handle addition of SN|ExtensionRepo permission
|
// Handle addition of SN|ExtensionRepo permission
|
||||||
const permissions = Copy(batchManager!.permissions) as ComponentPermission[];
|
const permissions = Copy(batchManager!.permissions) as ComponentPermission[];
|
||||||
const permission = permissions.find((p) => {
|
const permission = permissions.find((p) => {
|
||||||
return p.name === ComponentAction.StreamItems
|
return p.name === ComponentAction.StreamItems;
|
||||||
});
|
});
|
||||||
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
|
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
|
||||||
permission.content_types!.push(ContentType.ExtensionRepo);
|
permission.content_types!.push(ContentType.ExtensionRepo);
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ export class ThemeManager extends ApplicationService {
|
|||||||
this.deactivateAllThemes();
|
this.deactivateAllThemes();
|
||||||
} else if (event === ApplicationEvent.StorageReady) {
|
} else if (event === ApplicationEvent.StorageReady) {
|
||||||
await this.activateCachedThemes();
|
await this.activateCachedThemes();
|
||||||
if (!this.webApplication.getDesktopService().isDesktop) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +73,7 @@ export class ThemeManager extends ApplicationService {
|
|||||||
this.deactivateTheme(theme.uuid);
|
this.deactivateTheme(theme.uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearAppThemeState() {
|
private clearAppThemeState() {
|
||||||
|
|||||||
2
app/assets/javascripts/typings/pug.d.ts
vendored
2
app/assets/javascripts/typings/pug.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
declare module "*.pug" {
|
declare module "*.pug" {
|
||||||
import { compileTemplate } from 'pug'
|
import { compileTemplate } from 'pug';
|
||||||
const content: compileTemplate;
|
const content: compileTemplate;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
UuidString,
|
UuidString,
|
||||||
SyncOpStatus,
|
SyncOpStatus,
|
||||||
PrefKey,
|
PrefKey,
|
||||||
|
Challenge,
|
||||||
} from '@standardnotes/snjs';
|
} from '@standardnotes/snjs';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { Editor } from '@/ui_models/editor';
|
import { Editor } from '@/ui_models/editor';
|
||||||
@@ -289,10 +290,7 @@ export class AppState {
|
|||||||
} else if (
|
} else if (
|
||||||
note.archived &&
|
note.archived &&
|
||||||
!this.selectedTag?.isArchiveTag &&
|
!this.selectedTag?.isArchiveTag &&
|
||||||
!this.application.getPreference(
|
!this.application.getPreference(PrefKey.NotesShowArchived, false)
|
||||||
PrefKey.NotesShowArchived,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
this.closeEditor(editor);
|
this.closeEditor(editor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { PasswordWizardType, PasswordWizardScope } from '@/types';
|
|||||||
import {
|
import {
|
||||||
SNApplication,
|
SNApplication,
|
||||||
platformFromString,
|
platformFromString,
|
||||||
Challenge,
|
|
||||||
SNComponent,
|
SNComponent,
|
||||||
PermissionDialog,
|
PermissionDialog,
|
||||||
DeinitSource,
|
DeinitSource,
|
||||||
@@ -42,9 +41,9 @@ type WebServices = {
|
|||||||
|
|
||||||
export class WebApplication extends SNApplication {
|
export class WebApplication extends SNApplication {
|
||||||
|
|
||||||
private scope?: ng.IScope
|
private scope?: angular.IScope
|
||||||
private webServices!: WebServices
|
private webServices!: WebServices
|
||||||
private currentAuthenticationElement?: JQLite
|
private currentAuthenticationElement?: angular.IRootElementService
|
||||||
public editorGroup: EditorGroup
|
public editorGroup: EditorGroup
|
||||||
public componentGroup: ComponentGroup
|
public componentGroup: ComponentGroup
|
||||||
|
|
||||||
@@ -52,8 +51,8 @@ export class WebApplication extends SNApplication {
|
|||||||
constructor(
|
constructor(
|
||||||
deviceInterface: WebDeviceInterface,
|
deviceInterface: WebDeviceInterface,
|
||||||
identifier: string,
|
identifier: string,
|
||||||
private $compile: ng.ICompileService,
|
private $compile: angular.ICompileService,
|
||||||
scope: ng.IScope,
|
scope: angular.IScope,
|
||||||
defaultSyncServerHost: string,
|
defaultSyncServerHost: string,
|
||||||
private bridge: Bridge,
|
private bridge: Bridge,
|
||||||
) {
|
) {
|
||||||
@@ -98,10 +97,10 @@ export class WebApplication extends SNApplication {
|
|||||||
* to complete before destroying the global application instance and all its services */
|
* to complete before destroying the global application instance and all its services */
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
super.deinit(source);
|
super.deinit(source);
|
||||||
}, 0)
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStart() {
|
onStart(): void {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
this.componentManager!.openModalComponent = this.openModalComponent;
|
this.componentManager!.openModalComponent = this.openModalComponent;
|
||||||
this.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog;
|
this.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog;
|
||||||
@@ -158,18 +157,6 @@ export class WebApplication extends SNApplication {
|
|||||||
this.applicationElement.append(el);
|
this.applicationElement.append(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
promptForChallenge(challenge: Challenge) {
|
|
||||||
const scope: any = this.scope!.$new(true);
|
|
||||||
scope.challenge = challenge;
|
|
||||||
scope.application = this;
|
|
||||||
const el = this.$compile!(
|
|
||||||
"<challenge-modal " +
|
|
||||||
"class='sk-modal' application='application' challenge='challenge'>" +
|
|
||||||
"</challenge-modal>"
|
|
||||||
)(scope);
|
|
||||||
this.applicationElement.append(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationInProgress() {
|
authenticationInProgress() {
|
||||||
return this.currentAuthenticationElement != null;
|
return this.currentAuthenticationElement != null;
|
||||||
}
|
}
|
||||||
@@ -214,7 +201,12 @@ export class WebApplication extends SNApplication {
|
|||||||
this.applicationElement.append(el);
|
this.applicationElement.append(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
openModalComponent(component: SNComponent) {
|
async openModalComponent(component: SNComponent): Promise<void> {
|
||||||
|
if (component.package_info?.identifier === "org.standardnotes.batch-manager") {
|
||||||
|
if (!await this.authorizeBatchManagerAccess()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const scope = this.scope!.$new(true) as Partial<ComponentModalScope>;
|
const scope = this.scope!.$new(true) as Partial<ComponentModalScope>;
|
||||||
scope.componentUuid = component.uuid;
|
scope.componentUuid = component.uuid;
|
||||||
scope.application = this;
|
scope.application = this;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { SNComponent, ComponentArea, removeFromArray, addIfUnique } from '@standardnotes/snjs';
|
import { SNComponent, ComponentArea, removeFromArray, addIfUnique , UuidString } from '@standardnotes/snjs';
|
||||||
import { WebApplication } from './application';
|
import { WebApplication } from './application';
|
||||||
import { UuidString } from '@standardnotes/snjs';
|
|
||||||
|
|
||||||
/** Areas that only allow a single component to be active */
|
/** Areas that only allow a single component to be active */
|
||||||
const SingleComponentAreas = [
|
const SingleComponentAreas = [
|
||||||
ComponentArea.Editor,
|
ComponentArea.Editor,
|
||||||
ComponentArea.NoteTags,
|
ComponentArea.NoteTags,
|
||||||
ComponentArea.TagsList
|
ComponentArea.TagsList
|
||||||
]
|
];
|
||||||
|
|
||||||
export class ComponentGroup {
|
export class ComponentGroup {
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export class ComponentGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get componentManager() {
|
get componentManager() {
|
||||||
return this.application?.componentManager!;
|
return this.application.componentManager!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public deinit() {
|
public deinit() {
|
||||||
@@ -91,7 +91,7 @@ export class ComponentGroup {
|
|||||||
callback();
|
callback();
|
||||||
return () => {
|
return () => {
|
||||||
removeFromArray(this.changeObservers, callback);
|
removeFromArray(this.changeObservers, callback);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyObservers() {
|
private notifyObservers() {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export class EditorGroup {
|
|||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
removeFromArray(this.changeObservers, callback);
|
removeFromArray(this.changeObservers, callback);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyObservers() {
|
private notifyObservers() {
|
||||||
|
|||||||
@@ -77,10 +77,15 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
|||||||
*/
|
*/
|
||||||
this.state = Object.freeze(Object.assign({}, this.state, state));
|
this.state = Object.freeze(Object.assign({}, this.state, state));
|
||||||
resolve();
|
resolve();
|
||||||
|
this.afterStateChange();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
afterStateChange() {
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns a promise that resolves after the UI has been updated. */
|
/** @returns a promise that resolves after the UI has been updated. */
|
||||||
flushUI() {
|
flushUI() {
|
||||||
return this.$timeout();
|
return this.$timeout();
|
||||||
|
|||||||
@@ -27,3 +27,10 @@
|
|||||||
sessions-modal(
|
sessions-modal(
|
||||||
application='self.application'
|
application='self.application'
|
||||||
)
|
)
|
||||||
|
challenge-modal(
|
||||||
|
ng-repeat="challenge in self.state.challenges track by challenge.id"
|
||||||
|
class="sk-modal"
|
||||||
|
application="self.application"
|
||||||
|
challenge="challenge"
|
||||||
|
on-dismiss="self.removeChallenge(challenge)"
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { WebDirective } from '@/types';
|
|||||||
import { getPlatformString } from '@/utils';
|
import { getPlatformString } from '@/utils';
|
||||||
import template from './application-view.pug';
|
import template from './application-view.pug';
|
||||||
import { AppStateEvent } from '@/ui_models/app_state';
|
import { AppStateEvent } from '@/ui_models/app_state';
|
||||||
import { ApplicationEvent } from '@standardnotes/snjs';
|
import { ApplicationEvent, Challenge } from '@standardnotes/snjs';
|
||||||
import {
|
import {
|
||||||
PANEL_NAME_NOTES,
|
PANEL_NAME_NOTES,
|
||||||
PANEL_NAME_TAGS
|
PANEL_NAME_TAGS
|
||||||
@@ -14,7 +14,12 @@ import {
|
|||||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||||
import { alertDialog } from '@/services/alertService';
|
import { alertDialog } from '@/services/alertService';
|
||||||
|
|
||||||
class ApplicationViewCtrl extends PureViewCtrl {
|
class ApplicationViewCtrl extends PureViewCtrl<unknown, {
|
||||||
|
ready?: boolean,
|
||||||
|
needsUnlock?: boolean,
|
||||||
|
appClass: string,
|
||||||
|
challenges: Challenge[]
|
||||||
|
}> {
|
||||||
private $location?: ng.ILocationService
|
private $location?: ng.ILocationService
|
||||||
private $rootScope?: ng.IRootScopeService
|
private $rootScope?: ng.IRootScopeService
|
||||||
public platformString: string
|
public platformString: string
|
||||||
@@ -31,7 +36,7 @@ class ApplicationViewCtrl extends PureViewCtrl {
|
|||||||
this.$location = $location;
|
this.$location = $location;
|
||||||
this.$rootScope = $rootScope;
|
this.$rootScope = $rootScope;
|
||||||
this.platformString = getPlatformString();
|
this.platformString = getPlatformString();
|
||||||
this.state = { appClass: '' };
|
this.state = { appClass: '', challenges: [] };
|
||||||
this.onDragDrop = this.onDragDrop.bind(this);
|
this.onDragDrop = this.onDragDrop.bind(this);
|
||||||
this.onDragOver = this.onDragOver.bind(this);
|
this.onDragOver = this.onDragOver.bind(this);
|
||||||
this.addDragDropHandlers();
|
this.addDragDropHandlers();
|
||||||
@@ -40,11 +45,11 @@ class ApplicationViewCtrl extends PureViewCtrl {
|
|||||||
deinit() {
|
deinit() {
|
||||||
this.$location = undefined;
|
this.$location = undefined;
|
||||||
this.$rootScope = undefined;
|
this.$rootScope = undefined;
|
||||||
(this.application as any) = undefined;
|
(this.application as unknown) = undefined;
|
||||||
window.removeEventListener('dragover', this.onDragOver, true);
|
window.removeEventListener('dragover', this.onDragOver, true);
|
||||||
window.removeEventListener('drop', this.onDragDrop, true);
|
window.removeEventListener('drop', this.onDragDrop, true);
|
||||||
(this.onDragDrop as any) = undefined;
|
(this.onDragDrop as unknown) = undefined;
|
||||||
(this.onDragOver as any) = undefined;
|
(this.onDragOver as unknown) = undefined;
|
||||||
super.deinit();
|
super.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,12 +64,20 @@ class ApplicationViewCtrl extends PureViewCtrl {
|
|||||||
);
|
);
|
||||||
await this.application!.prepareForLaunch({
|
await this.application!.prepareForLaunch({
|
||||||
receiveChallenge: async (challenge) => {
|
receiveChallenge: async (challenge) => {
|
||||||
this.application!.promptForChallenge(challenge);
|
this.setState({
|
||||||
|
challenges: this.state.challenges.concat(challenge)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await this.application!.launch();
|
await this.application!.launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeChallenge(challenge: Challenge) {
|
||||||
|
this.setState({
|
||||||
|
challenges: this.state.challenges.filter(c => c.id !== challenge.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async onAppStart() {
|
async onAppStart() {
|
||||||
super.onAppStart();
|
super.onAppStart();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
.sk-modal-background(ng-click="ctrl.cancel()")
|
|
||||||
.challenge-modal.sk-modal-content(ng-if='ctrl.templateReady')
|
|
||||||
.sn-component
|
|
||||||
.sk-panel
|
|
||||||
.sk-panel-header
|
|
||||||
.sk-panel-header-title {{ctrl.challenge.modalTitle}}
|
|
||||||
.sk-panel-content
|
|
||||||
.sk-panel-section
|
|
||||||
.sk-p.sk-panel-row.centered.prompt
|
|
||||||
strong {{ctrl.challenge.heading}}
|
|
||||||
.sk-p.sk-panel-row.centered.subprompt(ng-if='ctrl.challenge.subheading')
|
|
||||||
| {{ctrl.challenge.subheading}}
|
|
||||||
.sk-panel-section
|
|
||||||
div(ng-repeat="prompt in ctrl.state.prompts track by prompt.id")
|
|
||||||
.sk-panel-row(
|
|
||||||
ng-if="prompt.validation != ctrl.protectionsSessionValidation"
|
|
||||||
)
|
|
||||||
input.sk-input.contrast(
|
|
||||||
ng-model="ctrl.state.values[prompt.id].value"
|
|
||||||
should-focus="$index == 0"
|
|
||||||
sn-autofocus="true"
|
|
||||||
sn-enter="ctrl.submit()" ,
|
|
||||||
ng-change="ctrl.onTextValueChange(prompt)"
|
|
||||||
ng-attr-type="{{prompt.secureTextEntry ? 'password' : 'text'}}",
|
|
||||||
ng-attr-placeholder="{{prompt.title}}"
|
|
||||||
)
|
|
||||||
.sk-horizontal-group(
|
|
||||||
ng-if="prompt.validation == ctrl.protectionsSessionValidation"
|
|
||||||
)
|
|
||||||
.sk-p.sk-bold Remember For
|
|
||||||
a.sk-a.info(
|
|
||||||
ng-repeat="option in ctrl.protectionsSessionDurations"
|
|
||||||
ng-class="{'boxed' : option.valueInSeconds == ctrl.state.values[prompt.id].value}"
|
|
||||||
ng-click="ctrl.onValueChange(prompt, option.valueInSeconds);"
|
|
||||||
)
|
|
||||||
| {{option.label}}
|
|
||||||
.sk-panel-row.centered
|
|
||||||
label.sk-label.danger(
|
|
||||||
ng-if="ctrl.state.values[prompt.id].invalid"
|
|
||||||
) Invalid authentication. Please try again.
|
|
||||||
.sk-panel-footer.extra-padding
|
|
||||||
.sk-button.info.big.block.bold(
|
|
||||||
ng-click="ctrl.submit()",
|
|
||||||
ng-class="{'info' : !ctrl.state.processing, 'neutral': ctrl.state.processing}"
|
|
||||||
ng-disabled="ctrl.state.processing"
|
|
||||||
)
|
|
||||||
.sk-label {{ctrl.state.processing ? 'Generating Keys...' : 'Submit'}}
|
|
||||||
.sk-panel-row(ng-if="ctrl.challenge.cancelable")
|
|
||||||
a.sk-panel-row.sk-a.info.centered(
|
|
||||||
ng-if="ctrl.challenge.cancelable"
|
|
||||||
ng-click="ctrl.cancel()"
|
|
||||||
) Cancel
|
|
||||||
|
|
||||||
.sk-panel-footer(ng-if="ctrl.state.showForgotPasscodeLink")
|
|
||||||
a.sk-panel-row.sk-a.info.centered(
|
|
||||||
ng-if="!ctrl.state.forgotPasscode"
|
|
||||||
ng-click="ctrl.onForgotPasscodeClick()"
|
|
||||||
) Forgot your passcode?
|
|
||||||
p.sk-panel-row.sk-p(ng-if="ctrl.state.forgotPasscode").
|
|
||||||
{{
|
|
||||||
ctrl.state.hasAccount
|
|
||||||
? "If you forgot your application passcode, your only option is to clear
|
|
||||||
your local data from this device and sign back in to your account."
|
|
||||||
: "If you forgot your application passcode, your only option is
|
|
||||||
to delete your data."
|
|
||||||
}}
|
|
||||||
a.sk-panel-row.sk-a.danger.centered(
|
|
||||||
ng-if="ctrl.state.forgotPasscode"
|
|
||||||
ng-click="ctrl.destroyLocalData()"
|
|
||||||
) Delete Local Data
|
|
||||||
.sk-panel-row
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import template from './challenge-modal.pug';
|
|
||||||
import {
|
|
||||||
ChallengeValue,
|
|
||||||
removeFromArray,
|
|
||||||
Challenge,
|
|
||||||
ChallengeReason,
|
|
||||||
ChallengePrompt,
|
|
||||||
ChallengeValidation,
|
|
||||||
ProtectionSessionDurations,
|
|
||||||
} from '@standardnotes/snjs';
|
|
||||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
|
||||||
import { WebDirective } from '@/types';
|
|
||||||
import { confirmDialog } from '@/services/alertService';
|
|
||||||
import { STRING_SIGN_OUT_CONFIRMATION } from '@/strings';
|
|
||||||
|
|
||||||
type InputValue = {
|
|
||||||
prompt: ChallengePrompt;
|
|
||||||
value: string | number | boolean;
|
|
||||||
invalid: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Values = Record<number, InputValue>;
|
|
||||||
|
|
||||||
type ChallengeModalState = {
|
|
||||||
prompts: ChallengePrompt[];
|
|
||||||
values: Partial<Values>;
|
|
||||||
processing: boolean;
|
|
||||||
forgotPasscode: boolean;
|
|
||||||
showForgotPasscodeLink: boolean;
|
|
||||||
processingPrompts: ChallengePrompt[];
|
|
||||||
hasAccount: boolean;
|
|
||||||
protectedNoteAccessDuration: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
|
|
||||||
application!: WebApplication;
|
|
||||||
challenge!: Challenge;
|
|
||||||
|
|
||||||
/** @template */
|
|
||||||
protectionsSessionDurations = ProtectionSessionDurations;
|
|
||||||
protectionsSessionValidation =
|
|
||||||
ChallengeValidation.ProtectionSessionDuration;
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(private $element: JQLite, $timeout: ng.ITimeoutService) {
|
|
||||||
super($timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
getState() {
|
|
||||||
return this.state as ChallengeModalState;
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
super.$onInit();
|
|
||||||
const values = {} as Values;
|
|
||||||
const prompts = this.challenge.prompts;
|
|
||||||
for (const prompt of prompts) {
|
|
||||||
values[prompt.id] = {
|
|
||||||
prompt,
|
|
||||||
value: prompt.initialValue ?? '',
|
|
||||||
invalid: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const showForgotPasscodeLink = [
|
|
||||||
ChallengeReason.ApplicationUnlock,
|
|
||||||
ChallengeReason.Migration,
|
|
||||||
].includes(this.challenge.reason);
|
|
||||||
this.setState({
|
|
||||||
prompts,
|
|
||||||
values,
|
|
||||||
processing: false,
|
|
||||||
forgotPasscode: false,
|
|
||||||
showForgotPasscodeLink,
|
|
||||||
hasAccount: this.application.hasAccount(),
|
|
||||||
processingPrompts: [],
|
|
||||||
protectedNoteAccessDuration: ProtectionSessionDurations[0].valueInSeconds,
|
|
||||||
});
|
|
||||||
this.application.addChallengeObserver(this.challenge, {
|
|
||||||
onValidValue: (value) => {
|
|
||||||
this.getState().values[value.prompt.id]!.invalid = false;
|
|
||||||
removeFromArray(this.state.processingPrompts, value.prompt);
|
|
||||||
this.reloadProcessingStatus();
|
|
||||||
},
|
|
||||||
onInvalidValue: (value) => {
|
|
||||||
this.getState().values[value.prompt.id]!.invalid = true;
|
|
||||||
/** If custom validation, treat all values together and not individually */
|
|
||||||
if (!value.prompt.validates) {
|
|
||||||
this.setState({ processingPrompts: [], processing: false });
|
|
||||||
} else {
|
|
||||||
removeFromArray(this.state.processingPrompts, value.prompt);
|
|
||||||
this.reloadProcessingStatus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
this.dismiss();
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
this.dismiss();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit() {
|
|
||||||
(this.application as any) = undefined;
|
|
||||||
(this.challenge as any) = undefined;
|
|
||||||
super.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadProcessingStatus() {
|
|
||||||
return this.setState({
|
|
||||||
processing: this.state.processingPrompts.length > 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroyLocalData() {
|
|
||||||
if (
|
|
||||||
await confirmDialog({
|
|
||||||
text: STRING_SIGN_OUT_CONFIRMATION,
|
|
||||||
confirmButtonStyle: 'danger',
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
await this.application.signOut();
|
|
||||||
this.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @template */
|
|
||||||
cancel() {
|
|
||||||
if (this.challenge.cancelable) {
|
|
||||||
this.application!.cancelChallenge(this.challenge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onForgotPasscodeClick() {
|
|
||||||
this.setState({
|
|
||||||
forgotPasscode: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextValueChange(prompt: ChallengePrompt) {
|
|
||||||
const values = this.getState().values;
|
|
||||||
values[prompt.id]!.invalid = false;
|
|
||||||
this.setState({ values });
|
|
||||||
}
|
|
||||||
|
|
||||||
onValueChange(prompt: ChallengePrompt, value: number) {
|
|
||||||
const values = this.state.values;
|
|
||||||
values[prompt.id]!.invalid = false;
|
|
||||||
values[prompt.id]!.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
validate() {
|
|
||||||
let failed = 0;
|
|
||||||
for (const prompt of this.state.prompts) {
|
|
||||||
const value = this.state.values[prompt.id]!;
|
|
||||||
if (typeof value.value === 'string' && value.value.length === 0) {
|
|
||||||
this.state.values[prompt.id]!.invalid = true;
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return failed === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
if (!this.validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.setState({ processing: true });
|
|
||||||
const values: ChallengeValue[] = [];
|
|
||||||
for (const inputValue of Object.values(this.getState().values)) {
|
|
||||||
const rawValue = inputValue!.value;
|
|
||||||
const value = new ChallengeValue(inputValue!.prompt, rawValue);
|
|
||||||
values.push(value);
|
|
||||||
}
|
|
||||||
const processingPrompts = values.map((v) => v.prompt);
|
|
||||||
await this.setState({
|
|
||||||
processingPrompts: processingPrompts,
|
|
||||||
processing: processingPrompts.length > 0,
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* Unfortunately neccessary to wait 50ms so that the above setState call completely
|
|
||||||
* updates the UI to change processing state, before we enter into UI blocking operation
|
|
||||||
* (crypto key generation)
|
|
||||||
*/
|
|
||||||
this.$timeout(() => {
|
|
||||||
if (values.length > 0) {
|
|
||||||
this.application.submitValuesForChallenge(this.challenge, values);
|
|
||||||
} else {
|
|
||||||
this.setState({ processing: false });
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss() {
|
|
||||||
const elem = this.$element;
|
|
||||||
const scope = elem.scope();
|
|
||||||
scope.$destroy();
|
|
||||||
elem.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChallengeModal extends WebDirective {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.controller = ChallengeModalCtrl;
|
|
||||||
this.controllerAs = 'ctrl';
|
|
||||||
this.bindToController = true;
|
|
||||||
this.scope = {
|
|
||||||
challenge: '=',
|
|
||||||
application: '=',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
404
app/assets/javascripts/views/challenge_modal/challenge_modal.tsx
Normal file
404
app/assets/javascripts/views/challenge_modal/challenge_modal.tsx
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { Dialog } from '@reach/dialog';
|
||||||
|
import {
|
||||||
|
ChallengeValue,
|
||||||
|
removeFromArray,
|
||||||
|
Challenge,
|
||||||
|
ChallengeReason,
|
||||||
|
ChallengePrompt,
|
||||||
|
ChallengeValidation,
|
||||||
|
ProtectionSessionDurations,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
|
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||||
|
import { WebDirective } from '@/types';
|
||||||
|
import { confirmDialog } from '@/services/alertService';
|
||||||
|
import { STRING_SIGN_OUT_CONFIRMATION } from '@/strings';
|
||||||
|
import { Ref, render } from 'preact';
|
||||||
|
import { useRef } from 'preact/hooks';
|
||||||
|
import ng from 'angular';
|
||||||
|
|
||||||
|
type InputValue = {
|
||||||
|
prompt: ChallengePrompt;
|
||||||
|
value: string | number | boolean;
|
||||||
|
invalid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Values = Record<number, InputValue>;
|
||||||
|
|
||||||
|
type ChallengeModalState = {
|
||||||
|
prompts: ChallengePrompt[];
|
||||||
|
values: Partial<Values>;
|
||||||
|
processing: boolean;
|
||||||
|
forgotPasscode: boolean;
|
||||||
|
showForgotPasscodeLink: boolean;
|
||||||
|
processingPrompts: ChallengePrompt[];
|
||||||
|
hasAccount: boolean;
|
||||||
|
protectedNoteAccessDuration: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
|
||||||
|
application!: WebApplication;
|
||||||
|
challenge!: Challenge;
|
||||||
|
onDismiss!: () => void;
|
||||||
|
|
||||||
|
/** @template */
|
||||||
|
protectionsSessionDurations = ProtectionSessionDurations;
|
||||||
|
protectionsSessionValidation = ChallengeValidation.ProtectionSessionDuration;
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
private $element: ng.IRootElementService,
|
||||||
|
$timeout: ng.ITimeoutService
|
||||||
|
) {
|
||||||
|
super($timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() {
|
||||||
|
return this.state as ChallengeModalState;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
super.$onInit();
|
||||||
|
const values = {} as Values;
|
||||||
|
const prompts = this.challenge.prompts;
|
||||||
|
for (const prompt of prompts) {
|
||||||
|
values[prompt.id] = {
|
||||||
|
prompt,
|
||||||
|
value: prompt.initialValue ?? '',
|
||||||
|
invalid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const showForgotPasscodeLink = [
|
||||||
|
ChallengeReason.ApplicationUnlock,
|
||||||
|
ChallengeReason.Migration,
|
||||||
|
].includes(this.challenge.reason);
|
||||||
|
this.setState({
|
||||||
|
prompts,
|
||||||
|
values,
|
||||||
|
processing: false,
|
||||||
|
forgotPasscode: false,
|
||||||
|
showForgotPasscodeLink,
|
||||||
|
hasAccount: this.application.hasAccount(),
|
||||||
|
processingPrompts: [],
|
||||||
|
protectedNoteAccessDuration: ProtectionSessionDurations[0].valueInSeconds,
|
||||||
|
});
|
||||||
|
this.application.addChallengeObserver(this.challenge, {
|
||||||
|
onValidValue: (value) => {
|
||||||
|
this.state.values[value.prompt.id]!.invalid = false;
|
||||||
|
removeFromArray(this.state.processingPrompts, value.prompt);
|
||||||
|
this.reloadProcessingStatus();
|
||||||
|
/** Trigger UI update */
|
||||||
|
this.afterStateChange();
|
||||||
|
},
|
||||||
|
onInvalidValue: (value) => {
|
||||||
|
this.state.values[value.prompt.id]!.invalid = true;
|
||||||
|
/** If custom validation, treat all values together and not individually */
|
||||||
|
if (!value.prompt.validates) {
|
||||||
|
this.setState({ processingPrompts: [], processing: false });
|
||||||
|
} else {
|
||||||
|
removeFromArray(this.state.processingPrompts, value.prompt);
|
||||||
|
this.reloadProcessingStatus();
|
||||||
|
}
|
||||||
|
/** Trigger UI update */
|
||||||
|
this.afterStateChange();
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
this.dismiss();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
this.dismiss();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit() {
|
||||||
|
(this.application as any) = undefined;
|
||||||
|
(this.challenge as any) = undefined;
|
||||||
|
super.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadProcessingStatus() {
|
||||||
|
return this.setState({
|
||||||
|
processing: this.state.processingPrompts.length > 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroyLocalData() {
|
||||||
|
if (
|
||||||
|
await confirmDialog({
|
||||||
|
text: STRING_SIGN_OUT_CONFIRMATION,
|
||||||
|
confirmButtonStyle: 'danger',
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
await this.application.signOut();
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @template */
|
||||||
|
cancel() {
|
||||||
|
if (this.challenge.cancelable) {
|
||||||
|
this.application!.cancelChallenge(this.challenge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onForgotPasscodeClick() {
|
||||||
|
this.setState({
|
||||||
|
forgotPasscode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextValueChange(prompt: ChallengePrompt) {
|
||||||
|
const values = this.getState().values;
|
||||||
|
values[prompt.id]!.invalid = false;
|
||||||
|
this.setState({ values });
|
||||||
|
}
|
||||||
|
|
||||||
|
onNumberValueChange(prompt: ChallengePrompt, value: number) {
|
||||||
|
const values = this.state.values;
|
||||||
|
values[prompt.id]!.invalid = false;
|
||||||
|
values[prompt.id]!.value = value;
|
||||||
|
this.setState({ values });
|
||||||
|
}
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
let failed = 0;
|
||||||
|
for (const prompt of this.state.prompts) {
|
||||||
|
const value = this.state.values[prompt.id]!;
|
||||||
|
if (typeof value.value === 'string' && value.value.length === 0) {
|
||||||
|
this.state.values[prompt.id]!.invalid = true;
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failed === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (!this.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.setState({ processing: true });
|
||||||
|
const values: ChallengeValue[] = [];
|
||||||
|
for (const inputValue of Object.values(this.getState().values)) {
|
||||||
|
const rawValue = inputValue!.value;
|
||||||
|
const value = new ChallengeValue(inputValue!.prompt, rawValue);
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
const processingPrompts = values.map((v) => v.prompt);
|
||||||
|
await this.setState({
|
||||||
|
processingPrompts: processingPrompts,
|
||||||
|
processing: processingPrompts.length > 0,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Unfortunately neccessary to wait 50ms so that the above setState call completely
|
||||||
|
* updates the UI to change processing state, before we enter into UI blocking operation
|
||||||
|
* (crypto key generation)
|
||||||
|
*/
|
||||||
|
this.$timeout(() => {
|
||||||
|
if (values.length > 0) {
|
||||||
|
this.application.submitValuesForChallenge(this.challenge, values);
|
||||||
|
} else {
|
||||||
|
this.setState({ processing: false });
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
afterStateChange() {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.onDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
$onDestroy() {
|
||||||
|
render(<></>, this.$element[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
|
if (!this.state.prompts) return;
|
||||||
|
render(<ChallengeModalView ctrl={this} />, this.$element[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChallengeModal extends WebDirective {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.restrict = 'E';
|
||||||
|
// this.template = template;
|
||||||
|
this.controller = ChallengeModalCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
challenge: '=',
|
||||||
|
application: '=',
|
||||||
|
onDismiss: '&',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChallengeModalView({ ctrl }: { ctrl: ChallengeModalCtrl }) {
|
||||||
|
const initialFocusRef = useRef<HTMLInputElement>();
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
initialFocusRef={initialFocusRef}
|
||||||
|
onDismiss={() => {
|
||||||
|
if (ctrl.challenge.cancelable) {
|
||||||
|
ctrl.dismiss();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="challenge-modal sk-modal-content">
|
||||||
|
<div className="sn-component">
|
||||||
|
<div className="sk-panel">
|
||||||
|
<div className="sk-panel-header">
|
||||||
|
<div className="sk-panel-header-title">
|
||||||
|
{ctrl.challenge.modalTitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sk-panel-content">
|
||||||
|
<div className="sk-panel-section">
|
||||||
|
<div className="sk-p sk-panel-row centered prompt">
|
||||||
|
<strong>{ctrl.challenge.heading}</strong>
|
||||||
|
</div>
|
||||||
|
{ctrl.challenge.subheading && (
|
||||||
|
<div className="sk-p sk-panel-row centered subprompt">
|
||||||
|
{ctrl.challenge.subheading}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sk-panel-section">
|
||||||
|
{ChallengePrompts({ ctrl, initialFocusRef })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sk-panel-footer extra-padding">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'sk-button big block bold ' +
|
||||||
|
(ctrl.state.processing ? 'neutral' : 'info')
|
||||||
|
}
|
||||||
|
disabled={ctrl.state.processing}
|
||||||
|
onClick={() => ctrl.submit()}
|
||||||
|
>
|
||||||
|
<div className="sk-label">
|
||||||
|
{ctrl.state.processing ? 'Generating Keys…' : 'Submit'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ctrl.challenge.cancelable && (
|
||||||
|
<>
|
||||||
|
<div className="sk-panel-row"></div>
|
||||||
|
<a
|
||||||
|
className="sk-panel-row sk-a info centered"
|
||||||
|
onClick={() => ctrl.cancel()}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{ctrl.state.showForgotPasscodeLink && (
|
||||||
|
<div className="sk-panel-footer">
|
||||||
|
{ctrl.state.forgotPasscode ? (
|
||||||
|
<>
|
||||||
|
<p className="sk-panel-row sk-p">
|
||||||
|
{ctrl.state.hasAccount
|
||||||
|
? 'If you forgot your application passcode, your ' +
|
||||||
|
'only option is to clear your local data from this ' +
|
||||||
|
'device and sign back in to your account.'
|
||||||
|
: 'If you forgot your application passcode, your ' +
|
||||||
|
'only option is to delete your data.'}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
className="sk-panel-row sk-a danger centered"
|
||||||
|
onClick={() => {
|
||||||
|
ctrl.destroyLocalData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete Local Data
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
className="sk-panel-row sk-a info centered"
|
||||||
|
onClick={() => ctrl.onForgotPasscodeClick()}
|
||||||
|
>
|
||||||
|
Forgot your passcode?
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<div className="sk-panel-row"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChallengePrompts({
|
||||||
|
ctrl,
|
||||||
|
initialFocusRef,
|
||||||
|
}: {
|
||||||
|
ctrl: ChallengeModalCtrl;
|
||||||
|
initialFocusRef: Ref<HTMLInputElement>;
|
||||||
|
}) {
|
||||||
|
return ctrl.state.prompts.map((prompt, index) => (
|
||||||
|
<>
|
||||||
|
{/** ProtectionSessionDuration can't just be an input field */}
|
||||||
|
{prompt.validation === ChallengeValidation.ProtectionSessionDuration ? (
|
||||||
|
<div key={prompt.id} className="sk-panel-row">
|
||||||
|
<div className="sk-horizontal-group">
|
||||||
|
<div className="sk-p sk-bold">Remember For</div>
|
||||||
|
{ProtectionSessionDurations.map((option) => (
|
||||||
|
<a
|
||||||
|
className={
|
||||||
|
'sk-a info ' +
|
||||||
|
(option.valueInSeconds === ctrl.state.values[prompt.id]!.value
|
||||||
|
? 'boxed'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
ctrl.onNumberValueChange(prompt, option.valueInSeconds);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key={prompt.id} className="sk-panel-row">
|
||||||
|
<input
|
||||||
|
className="sk-input contrast"
|
||||||
|
value={ctrl.state.values[prompt.id]!.value as string | number}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
ctrl.state.values[prompt.id]!.value = value;
|
||||||
|
ctrl.onTextValueChange(prompt);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
ctrl.submit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={index === 0 ? initialFocusRef : undefined}
|
||||||
|
placeholder={prompt.title}
|
||||||
|
type={prompt.secureTextEntry ? 'password' : 'text'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ctrl.state.values[prompt.id]!.invalid && (
|
||||||
|
<div className="sk-panel-row centered">
|
||||||
|
<label className="sk-label danger">
|
||||||
|
Invalid authentication. Please try again.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -6,4 +6,4 @@ export { EditorView } from './editor/editor_view';
|
|||||||
export { FooterView } from './footer/footer_view';
|
export { FooterView } from './footer/footer_view';
|
||||||
export { NotesView } from './notes/notes_view';
|
export { NotesView } from './notes/notes_view';
|
||||||
export { TagsView } from './tags/tags_view';
|
export { TagsView } from './tags/tags_view';
|
||||||
export { ChallengeModal } from './challenge_modal/challenge_modal'
|
export { ChallengeModal } from './challenge_modal/challenge_modal';
|
||||||
@@ -20,11 +20,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-dialog-content] {
|
[data-reach-dialog-content] {
|
||||||
|
width: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: unset;
|
overflow: unset;
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
|
min-width: 400px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
.sessions-modal {
|
.sessions-modal {
|
||||||
|
min-width: 40vw;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
h2, ul, p {
|
h2, ul, p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -16,7 +16,7 @@
|
|||||||
"bundle:desktop:beta": "webpack --config webpack.prod.js --env.platform='desktop' --env.public_beta='true'",
|
"bundle:desktop:beta": "webpack --config webpack.prod.js --env.platform='desktop' --env.public_beta='true'",
|
||||||
"build": "bundle install && yarn install --pure-lockfile && bundle exec rails assets:precompile && yarn bundle",
|
"build": "bundle install && yarn install --pure-lockfile && bundle exec rails assets:precompile && yarn bundle",
|
||||||
"submodules": "git submodule update --init --force",
|
"submodules": "git submodule update --init --force",
|
||||||
"lint": "eslint --fix app/assets/javascripts/**/*.js",
|
"lint": "eslint --fix app/assets/javascripts/**/*.{ts,tsx}",
|
||||||
"tsc": "tsc --project app/assets/javascripts/tsconfig.json"
|
"tsc": "tsc --project app/assets/javascripts/tsconfig.json"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
"@types/mocha": "^7.0.2",
|
"@types/mocha": "^7.0.2",
|
||||||
"@types/pug": "^2.0.4",
|
"@types/pug": "^2.0.4",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||||
"@typescript-eslint/parser": "^3.10.1",
|
"@typescript-eslint/parser": "^4.14.0",
|
||||||
"angular": "^1.8.2",
|
"angular": "^1.8.2",
|
||||||
"apply-loader": "^2.0.0",
|
"apply-loader": "^2.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
"connect": "^3.7.0",
|
"connect": "^3.7.0",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.4.2",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^7.18.0",
|
||||||
"eslint-config-prettier": "^6.10.0",
|
"eslint-config-prettier": "^7.2.0",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-react": "^7.22.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
"eslint-plugin-standard": "^4.0.1",
|
||||||
"file-loader": "^5.1.0",
|
"file-loader": "^5.1.0",
|
||||||
"html-webpack-plugin": "^4.3.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
@@ -71,12 +71,9 @@
|
|||||||
"@reach/alert-dialog": "^0.12.1",
|
"@reach/alert-dialog": "^0.12.1",
|
||||||
"@reach/dialog": "^0.12.1",
|
"@reach/dialog": "^0.12.1",
|
||||||
"@standardnotes/sncrypto-web": "^1.2.9",
|
"@standardnotes/sncrypto-web": "^1.2.9",
|
||||||
"@standardnotes/snjs": "^2.0.45",
|
"@standardnotes/snjs": "^2.0.47",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"mobx": "^6.0.4",
|
"mobx": "^6.0.4",
|
||||||
"preact": "^10.5.7"
|
"preact": "^10.5.11"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user