summaryrefslogtreecommitdiffhomepage
path: root/app/lib
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-07-18 15:07:37 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-08-15 17:39:38 +0200
commit71592249b2dd669b6f24f37bfb7b0f4e43b74998 (patch)
treea6097dc7e5d94d06e99c65fdfe160e824395f50c /app/lib
parente84e87f4ce5a8c242f756566cdc6fb59a45f7bea (diff)
downloadmullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.tar.xz
mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.zip
Add workspaces
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/daemon-rpc.js458
-rw-r--r--app/lib/formatters.js10
-rw-r--r--app/lib/jsonrpc-transport.js321
-rw-r--r--app/lib/keyframe-animation.js257
-rw-r--r--app/lib/platform.android.js33
-rw-r--r--app/lib/platform.js118
-rw-r--r--app/lib/problem-report.android.js13
-rw-r--r--app/lib/problem-report.js44
-rw-r--r--app/lib/proc.js26
-rw-r--r--app/lib/reconnection-backoff.js24
-rw-r--r--app/lib/relay-settings-builder.js194
-rw-r--r--app/lib/rpc-address-file.js115
-rw-r--r--app/lib/tempdir.js21
-rw-r--r--app/lib/transition-rule.js46
-rw-r--r--app/lib/window-state-observer.js64
15 files changed, 0 insertions, 1744 deletions
diff --git a/app/lib/daemon-rpc.js b/app/lib/daemon-rpc.js
deleted file mode 100644
index 8e2925f83e..0000000000
--- a/app/lib/daemon-rpc.js
+++ /dev/null
@@ -1,458 +0,0 @@
-// @flow
-
-import JsonRpcTransport, {
- RemoteError as JsonRpcRemoteError,
- TimeOutError as JsonRpcTimeOutError,
-} from './jsonrpc-transport';
-import { CommunicationError, InvalidAccountError, NoDaemonError } from '../errors';
-
-import {
- object,
- maybe,
- string,
- number,
- boolean,
- enumeration,
- arrayOf,
- oneOf,
-} from 'validated/schema';
-import { validate } from 'validated/object';
-
-import type { Node as SchemaNode } from 'validated/schema';
-
-export type AccountData = { expiry: string };
-export type AccountToken = string;
-export type Ip = string;
-export type Location = {
- ip: Ip,
- country: string,
- city: ?string,
- latitude: number,
- longitude: number,
- mullvad_exit_ip: boolean,
-};
-const LocationSchema = object({
- ip: string,
- country: string,
- city: maybe(string),
- latitude: number,
- longitude: number,
- mullvad_exit_ip: boolean,
-});
-
-export type SecurityState = 'secured' | 'unsecured';
-export type BackendState = {
- state: SecurityState,
- target_state: SecurityState,
-};
-
-export type RelayProtocol = 'tcp' | 'udp';
-export type RelayLocation = {| city: [string, string] |} | {| country: string |};
-
-type OpenVpnConstraints = {
- port: 'any' | { only: number },
- protocol: 'any' | { only: RelayProtocol },
-};
-
-type TunnelConstraints<TOpenVpnConstraints> = {
- openvpn: TOpenVpnConstraints,
-};
-
-type RelaySettingsNormal<TTunnelConstraints> = {
- location:
- | 'any'
- | {
- only: RelayLocation,
- },
- tunnel:
- | 'any'
- | {
- only: TTunnelConstraints,
- },
-};
-
-// types describing the structure of RelaySettings
-export type RelaySettingsCustom = {
- host: string,
- tunnel: {
- openvpn: {
- port: number,
- protocol: RelayProtocol,
- },
- },
-};
-export type RelaySettings =
- | {|
- normal: RelaySettingsNormal<TunnelConstraints<OpenVpnConstraints>>,
- |}
- | {|
- custom_tunnel_endpoint: RelaySettingsCustom,
- |};
-
-// types describing the partial update of RelaySettings
-export type RelaySettingsNormalUpdate = $Shape<
- RelaySettingsNormal<TunnelConstraints<$Shape<OpenVpnConstraints>>>,
->;
-export type RelaySettingsUpdate =
- | {|
- normal: RelaySettingsNormalUpdate,
- |}
- | {|
- custom_tunnel_endpoint: RelaySettingsCustom,
- |};
-
-const constraint = <T>(constraintValue: SchemaNode<T>) => {
- return oneOf(
- string, // any
- object({
- only: constraintValue,
- }),
- );
-};
-
-const RelaySettingsSchema = oneOf(
- object({
- normal: object({
- location: constraint(
- oneOf(
- object({
- city: arrayOf(string),
- }),
- object({
- country: string,
- }),
- ),
- ),
- tunnel: constraint(
- object({
- openvpn: object({
- port: constraint(number),
- protocol: constraint(enumeration('udp', 'tcp')),
- }),
- }),
- ),
- }),
- }),
- object({
- custom_tunnel_endpoint: object({
- host: string,
- tunnel: object({
- openvpn: object({
- port: number,
- protocol: enumeration('udp', 'tcp'),
- }),
- }),
- }),
- }),
-);
-
-export type RelayList = {
- countries: Array<RelayListCountry>,
-};
-
-export type RelayListCountry = {
- name: string,
- code: string,
- cities: Array<RelayListCity>,
-};
-
-export type RelayListCity = {
- name: string,
- code: string,
- latitude: number,
- longitude: number,
- has_active_relays: boolean,
-};
-
-const RelayListSchema = object({
- countries: arrayOf(
- object({
- name: string,
- code: string,
- cities: arrayOf(
- object({
- name: string,
- code: string,
- latitude: number,
- longitude: number,
- has_active_relays: boolean,
- }),
- ),
- }),
- ),
-});
-
-export type TunnelOptions = {
- openvpn: {
- enableIpv6: boolean,
- },
-};
-
-const TunnelOptionsSchema = object({
- openvpn: object({
- enable_ipv6: boolean,
- mssfix: maybe(number),
- }),
-});
-
-const AccountDataSchema = object({
- expiry: string,
-});
-
-const allSecurityStates: Array<SecurityState> = ['secured', 'unsecured'];
-const BackendStateSchema = object({
- state: enumeration(...allSecurityStates),
- target_state: enumeration(...allSecurityStates),
-});
-
-export interface DaemonRpcProtocol {
- connect(string): void;
- disconnect(): void;
- getAccountData(AccountToken): Promise<AccountData>;
- getRelayLocations(): Promise<RelayList>;
- getAccount(): Promise<?AccountToken>;
- setAccount(accountToken: ?AccountToken): Promise<void>;
- updateRelaySettings(RelaySettingsUpdate): Promise<void>;
- getRelaySettings(): Promise<RelaySettings>;
- setAllowLan(boolean): Promise<void>;
- getAllowLan(): Promise<boolean>;
- setOpenVpnEnableIpv6(boolean): Promise<void>;
- getTunnelOptions(): Promise<TunnelOptions>;
- setAutoConnect(boolean): Promise<void>;
- getAutoConnect(): Promise<boolean>;
- connectTunnel(): Promise<void>;
- disconnectTunnel(): Promise<void>;
- getLocation(): Promise<Location>;
- getState(): Promise<BackendState>;
- subscribeStateListener((state: ?BackendState, error: ?Error) => void): Promise<void>;
- addOpenConnectionObserver(() => void): ConnectionObserver;
- addCloseConnectionObserver((error: ?Error) => void): ConnectionObserver;
- authenticate(sharedSecret: string): Promise<void>;
- getAccountHistory(): Promise<Array<AccountToken>>;
- removeAccountFromHistory(accountToken: AccountToken): Promise<void>;
-}
-
-export class ResponseParseError extends Error {
- _validationError: ?Error;
-
- constructor(message: string, validationError: ?Error) {
- super(message);
- this._validationError = validationError;
- }
-
- get validationError(): ?Error {
- return this._validationError;
- }
-}
-
-export type ConnectionObserver = {
- unsubscribe: () => void,
-};
-
-export class DaemonRpc implements DaemonRpcProtocol {
- _transport = new JsonRpcTransport();
-
- async authenticate(sharedSecret: string): Promise<void> {
- await this._transport.send('auth', sharedSecret);
- }
-
- connect(connectionString: string) {
- this._transport.connect(connectionString);
- }
-
- disconnect() {
- this._transport.disconnect();
- }
-
- addOpenConnectionObserver(handler: () => void): ConnectionObserver {
- this._transport.on('open', handler);
- return {
- unsubscribe: () => {
- this._transport.off('open', handler);
- },
- };
- }
-
- addCloseConnectionObserver(handler: (error: ?Error) => void): ConnectionObserver {
- this._transport.on('close', handler);
- return {
- unsubscribe: () => {
- this._transport.off('close', handler);
- },
- };
- }
-
- async getAccountData(accountToken: AccountToken): Promise<AccountData> {
- // send the IPC with 30s timeout since the backend will wait
- // for a HTTP request before replying
- let response;
- try {
- response = await this._transport.send('get_account_data', accountToken, 30000);
- } catch (error) {
- if (error instanceof JsonRpcRemoteError) {
- switch (error.code) {
- case -200: // Account doesn't exist
- throw new InvalidAccountError();
- case -32603: // Internal error
- throw new CommunicationError();
- }
- } else if (error instanceof JsonRpcTimeOutError) {
- throw new NoDaemonError();
- } else {
- throw error;
- }
- }
-
- try {
- return validate(AccountDataSchema, response);
- } catch (error) {
- throw new ResponseParseError('Invalid response from get_account_data', error);
- }
- }
-
- async getRelayLocations(): Promise<RelayList> {
- const response = await this._transport.send('get_relay_locations');
- try {
- return validate(RelayListSchema, response);
- } catch (error) {
- throw new ResponseParseError('Invalid response from get_relay_locations', error);
- }
- }
-
- async getAccount(): Promise<?AccountToken> {
- const response = await this._transport.send('get_account');
- if (response === null || typeof response === 'string') {
- return response;
- } else {
- throw new ResponseParseError('Invalid response from get_account', null);
- }
- }
-
- async setAccount(accountToken: ?AccountToken): Promise<void> {
- await this._transport.send('set_account', accountToken);
- }
-
- async updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise<void> {
- await this._transport.send('update_relay_settings', [relaySettings]);
- }
-
- async getRelaySettings(): Promise<RelaySettings> {
- const response = await this._transport.send('get_relay_settings');
- try {
- const validatedObject = validate(RelaySettingsSchema, response);
-
- /* $FlowFixMe:
- There is no way to express constraints with string literals, i.e:
-
- RelaySettingsSchema constraint:
- oneOf(string, object)
-
- RelaySettings constraint:
- 'any' | object
-
- These two are incompatible so we simply enforce the type for now.
- */
- return ((validatedObject: any): RelaySettings);
- } catch (e) {
- throw new ResponseParseError('Invalid response from get_relay_settings', e);
- }
- }
-
- async setAllowLan(allowLan: boolean): Promise<void> {
- await this._transport.send('set_allow_lan', [allowLan]);
- }
-
- async getAllowLan(): Promise<boolean> {
- const response = await this._transport.send('get_allow_lan');
- if (typeof response === 'boolean') {
- return response;
- } else {
- throw new ResponseParseError('Invalid response from get_allow_lan', null);
- }
- }
-
- async setOpenVpnEnableIpv6(enableIpv6: boolean): Promise<void> {
- await this._transport.send('set_openvpn_enable_ipv6', [enableIpv6]);
- }
-
- async getTunnelOptions(): Promise<TunnelOptions> {
- const response = await this._transport.send('get_tunnel_options');
- try {
- const validatedObject = validate(TunnelOptionsSchema, response);
-
- return {
- openvpn: {
- enableIpv6: validatedObject.openvpn.enable_ipv6,
- },
- };
- } catch (error) {
- throw new ResponseParseError('Invalid response from get_tunnel_options', error);
- }
- }
-
- async setAutoConnect(autoConnect: boolean): Promise<void> {
- await this._transport.send('set_auto_connect', [autoConnect]);
- }
-
- async getAutoConnect(): Promise<boolean> {
- const response = await this._transport.send('get_auto_connect');
- if (typeof response === 'boolean') {
- return response;
- } else {
- throw new ResponseParseError('Invalid response from get_auto_connect', null);
- }
- }
-
- async connectTunnel(): Promise<void> {
- await this._transport.send('connect');
- }
-
- async disconnectTunnel(): Promise<void> {
- await this._transport.send('disconnect');
- }
-
- async getLocation(): Promise<Location> {
- // send the IPC with 30s timeout since the backend will wait
- // for a HTTP request before replying
-
- const response = await this._transport.send('get_current_location', [], 30000);
- try {
- return validate(LocationSchema, response);
- } catch (error) {
- throw new ResponseParseError('Invalid response from get_current_location', error);
- }
- }
-
- async getState(): Promise<BackendState> {
- const response = await this._transport.send('get_state');
- try {
- return validate(BackendStateSchema, response);
- } catch (error) {
- throw new ResponseParseError('Invalid response from get_state', error);
- }
- }
-
- subscribeStateListener(listener: (state: ?BackendState, error: ?Error) => void): Promise<void> {
- return this._transport.subscribe('new_state', (payload) => {
- try {
- const newState = validate(BackendStateSchema, payload);
- listener(newState, null);
- } catch (error) {
- listener(null, new ResponseParseError('Invalid payload from new_state', error));
- }
- });
- }
-
- async getAccountHistory(): Promise<Array<AccountToken>> {
- const response = await this._transport.send('get_account_history');
- try {
- return validate(arrayOf(string), response);
- } catch (error) {
- throw new ResponseParseError('Invalid response from get_account_history', null);
- }
- }
-
- async removeAccountFromHistory(accountToken: AccountToken): Promise<void> {
- await this._transport.send('remove_account_from_history', accountToken);
- }
-}
diff --git a/app/lib/formatters.js b/app/lib/formatters.js
deleted file mode 100644
index 37d7460768..0000000000
--- a/app/lib/formatters.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// @flow
-
-export const formatAccount = (val: string): string => {
- // display number altogether when longer than 12
- if (val.length > 12) {
- return val;
- }
- // display quartets
- return val.replace(/([0-9]{4})/g, '$1 ').trim();
-};
diff --git a/app/lib/jsonrpc-transport.js b/app/lib/jsonrpc-transport.js
deleted file mode 100644
index cbf1e7dce8..0000000000
--- a/app/lib/jsonrpc-transport.js
+++ /dev/null
@@ -1,321 +0,0 @@
-// @flow
-
-import { EventEmitter } from 'events';
-import jsonrpc from 'jsonrpc-lite';
-import uuid from 'uuid';
-import { log } from '../lib/platform';
-
-export type UnansweredRequest = {
- resolve: (mixed) => void,
- reject: (mixed) => void,
- timerId: TimeoutID,
- message: Object,
-};
-
-export type JsonRpcErrorResponse = {
- type: 'error',
- payload: {
- id: string,
- error: {
- code: number,
- message: string,
- },
- },
-};
-export type JsonRpcNotification = {
- type: 'notification',
- payload: {
- method: string,
- params: {
- subscription: string,
- result: mixed,
- },
- },
-};
-export type JsonRpcSuccess = {
- type: 'success',
- payload: {
- id: string,
- result: mixed,
- },
-};
-export type JsonRpcMessage = JsonRpcErrorResponse | JsonRpcNotification | JsonRpcSuccess;
-
-export class RemoteError extends Error {
- _code: number;
- _details: string;
-
- constructor(code: number, details: string) {
- super(`Remote JSON-RPC error ${code}: ${details}`);
- this._code = code;
- this._details = details;
- }
-
- get code(): number {
- return this._code;
- }
-
- get details(): string {
- return this._details;
- }
-}
-
-export class TimeOutError extends Error {
- _jsonRpcMessage: Object;
-
- constructor(jsonRpcMessage: Object) {
- super('Request timed out');
-
- this._jsonRpcMessage = jsonRpcMessage;
- }
-
- get jsonRpcMessage(): Object {
- return this._jsonRpcMessage;
- }
-}
-
-export class SubscriptionError extends Error {
- _reply: mixed;
-
- constructor(message: string, reply: mixed) {
- const replyString = JSON.stringify(reply);
-
- super(`${message}: ${replyString}`);
-
- this._reply = reply;
- }
-
- get reply(): mixed {
- return this._reply;
- }
-}
-
-export class ConnectionError extends Error {
- _code: number;
-
- constructor(code: number) {
- super(ConnectionError.reason(code));
- this._code = code;
- }
-
- get code(): number {
- return this._code;
- }
-
- static reason(code: number): string {
- switch (code) {
- case 1006:
- return 'Abnormal closure';
- case 1011:
- return 'Internal error';
- case 1012:
- return 'Service restart';
- case 1014:
- return 'Bad gateway';
- default:
- return `Unknown (${code})`;
- }
- }
-}
-
-const DEFAULT_TIMEOUT_MILLIS = 5000;
-
-export default class JsonRpcTransport extends EventEmitter {
- _unansweredRequests: Map<string, UnansweredRequest> = new Map();
- _subscriptions: Map<string | number, (mixed) => void> = new Map();
- _webSocket: ?WebSocket;
- _websocketFactory: (string) => WebSocket;
-
- constructor(websocketFactory: ?(string) => WebSocket) {
- super();
- this._websocketFactory =
- websocketFactory || ((connectionString) => new WebSocket(connectionString));
- }
-
- /// Connect websocket
- connect(connectionString: string): Promise<void> {
- return new Promise((resolve, reject) => {
- this.disconnect();
-
- log.info('Connecting to websocket', connectionString);
-
- const webSocket = this._websocketFactory(connectionString);
-
- // A flag used to determine if Promise was resolved.
- let isPromiseResolved = false;
-
- webSocket.onopen = () => {
- log.info('Websocket is connected');
- this.emit('open');
-
- // Resolve the Promise
- resolve();
- isPromiseResolved = true;
- };
-
- webSocket.onmessage = (event) => {
- const data = event.data;
- if (typeof data === 'string') {
- this._onMessage(data);
- } else {
- log.error('Got invalid reply from the server', event);
- }
- };
-
- webSocket.onclose = (event) => {
- log.info(`The websocket connection closed with code: ${event.code}`);
-
- // Remove all subscriptions since they are connection based
- this._subscriptions.clear();
-
- // 1000 is a code used for normal connection closure.
- const connectionError = event.code === 1000 ? null : new ConnectionError(event.code);
-
- this.emit('close', connectionError);
-
- // Prevent rejecting a previously resolved Promise.
- if (!isPromiseResolved) {
- reject(connectionError);
- }
- };
-
- this._webSocket = webSocket;
- });
- }
-
- disconnect() {
- if (this._webSocket) {
- this._webSocket.close();
- this._webSocket = null;
- }
- }
-
- async subscribe(event: string, listener: (mixed) => void): Promise<*> {
- log.silly(`Adding a listener to ${event}`);
-
- try {
- const subscriptionId = await this.send(`${event}_subscribe`);
- if (typeof subscriptionId === 'string' || typeof subscriptionId === 'number') {
- this._subscriptions.set(subscriptionId, listener);
- } else {
- throw new SubscriptionError(
- 'The subscription id was not a string or a number',
- subscriptionId,
- );
- }
- } catch (e) {
- log.error(`Failed adding listener to ${event}: ${e.message}`);
- throw e;
- }
- }
-
- send(action: string, data: mixed, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<mixed> {
- return new Promise((resolve, reject) => {
- const webSocket = this._webSocket;
- if (!webSocket) {
- reject(new Error('Websocket is not connected.'));
- return;
- }
-
- const id = uuid.v4();
- const payload = this._prepareParams(data);
- const timerId = setTimeout(() => this._onTimeout(id), timeout);
- const message = jsonrpc.request(id, action, payload);
- this._unansweredRequests.set(id, {
- resolve,
- reject,
- timerId,
- message,
- });
-
- try {
- log.silly('Sending message', id, action);
- webSocket.send(JSON.stringify(message));
- } catch (error) {
- log.error(`Failed sending RPC message "${action}": ${error.message}`);
-
- // clean up on error
- this._unansweredRequests.delete(id);
- clearTimeout(timerId);
-
- throw error;
- }
- });
- }
-
- _prepareParams(data: mixed): Array<mixed> | Object {
- // JSONRPC only accepts arrays and objects as params, but
- // this isn't very nice to use, so this method wraps other
- // types in an array. The choice of array is based on try-and-error
-
- if (data === undefined) {
- return [];
- } else if (data === null) {
- return [null];
- } else if (Array.isArray(data) || typeof data === 'object') {
- return data;
- } else {
- return [data];
- }
- }
-
- _onTimeout(requestId) {
- const request = this._unansweredRequests.get(requestId);
-
- this._unansweredRequests.delete(requestId);
-
- if (request) {
- log.warn(`Request ${requestId} timed out: `, request.message);
- request.reject(new TimeOutError(request.message));
- } else {
- log.warn(`Request ${requestId} timed out but it seems to already have been answered`);
- }
- }
-
- _onMessage(message: string) {
- const result = jsonrpc.parse(message);
- const messages = Array.isArray(result) ? result : [result];
-
- for (const message of messages) {
- if (message.type === 'notification') {
- this._onNotification(message);
- } else {
- this._onReply(message);
- }
- }
- }
-
- _onNotification(message: JsonRpcNotification) {
- const subscriptionId = message.payload.params.subscription;
- const listener = this._subscriptions.get(subscriptionId);
-
- if (listener) {
- log.silly('Got notification', message.payload.method, message.payload.params.result);
- listener(message.payload.params.result);
- } else {
- log.warn('Got notification for', message.payload.method, 'but no one is listening for it');
- }
- }
-
- _onReply(message: JsonRpcErrorResponse | JsonRpcSuccess) {
- const id = message.payload.id;
- const request = this._unansweredRequests.get(id);
- this._unansweredRequests.delete(id);
-
- if (request) {
- log.silly('Got answer to', id, message.type);
-
- clearTimeout(request.timerId);
-
- if (message.type === 'error') {
- const error = message.payload.error;
- request.reject(new RemoteError(error.code, error.message));
- } else {
- const reply = message.payload.result;
- request.resolve(reply);
- }
- } else {
- log.warn(`Got reply to ${id} but no one was waiting for it`);
- }
- }
-}
diff --git a/app/lib/keyframe-animation.js b/app/lib/keyframe-animation.js
deleted file mode 100644
index 028c95769a..0000000000
--- a/app/lib/keyframe-animation.js
+++ /dev/null
@@ -1,257 +0,0 @@
-// @flow
-
-import { nativeImage } from 'electron';
-import type { NativeImage } from 'electron';
-
-export type OnFrameFn = (image: NativeImage) => void;
-export type OnFinishFn = (void) => void;
-export type KeyframeAnimationOptions = {
- startFrame?: number,
- endFrame?: number,
- beginFromCurrentState?: boolean,
- advanceTo?: 'end',
-};
-export type KeyframeAnimationRange = [number, number];
-
-export default class KeyframeAnimation {
- _speed: number = 200; // ms
- _repeat: boolean = false;
- _reverse: boolean = false;
- _alternate: boolean = false;
-
- _onFrame: ?OnFrameFn;
- _onFinish: ?OnFinishFn;
-
- _nativeImages: Array<NativeImage>;
- _frameRange: KeyframeAnimationRange;
- _numFrames: number;
- _currentFrame: number = 0;
-
- _isRunning: boolean = false;
- _isFinished: boolean = false;
- _isFirstRun: boolean = true;
-
- _timeout = null;
-
- set onFrame(newValue: ?OnFrameFn) {
- this._onFrame = newValue;
- }
- get onFrame(): ?OnFrameFn {
- return this._onFrame;
- }
-
- // called when animation finished for non-repeating animations.
- set onFinish(newValue: ?OnFinishFn) {
- this._onFinish = newValue;
- }
- get onFinish(): ?OnFinishFn {
- return this._onFinish;
- }
-
- // pace per frame in ms
- set speed(newValue: number) {
- this._speed = parseInt(newValue);
- }
- get speed(): number {
- return this._speed;
- }
-
- set repeat(newValue: boolean) {
- this._repeat = newValue;
- }
- get repeat(): boolean {
- return this._repeat;
- }
-
- set reverse(newValue: boolean) {
- this._reverse = newValue;
- }
- get reverse(): boolean {
- return this._repeat;
- }
-
- // alternates the animation direction when it reaches the end
- // only for repeating animations
- set alternate(newValue: boolean) {
- this._alternate = !!newValue;
- }
- get alternate(): boolean {
- return this._alternate;
- }
-
- get nativeImages(): Array<NativeImage> {
- return this._nativeImages.slice();
- }
- get isFinished(): boolean {
- return this._isFinished;
- }
-
- // create animation from files matching filename pattern. i.e (bubble-frame-{}.png)
- static fromFilePattern(filePattern: string, range: KeyframeAnimationRange): KeyframeAnimation {
- const images: Array<NativeImage> = [];
-
- if (range.length !== 2 || range[0] > range[1]) {
- throw new Error('the animation range is invalid');
- }
-
- for (let i = range[0]; i <= range[1]; i++) {
- const filePath = filePattern.replace('{}', i.toString());
- const image = nativeImage.createFromPath(filePath);
- images.push(image);
- }
- return new KeyframeAnimation(images);
- }
-
- static fromFileSequence(files: Array<string>): KeyframeAnimation {
- const images: Array<NativeImage> = files.map((filePath) =>
- nativeImage.createFromPath(filePath),
- );
- return new KeyframeAnimation(images);
- }
-
- constructor(images: Array<NativeImage>) {
- const len = images.length;
- if (len < 1) {
- throw new Error('too few images in animation');
- }
-
- this._nativeImages = images.slice();
- this._numFrames = len;
- this._frameRange = [0, len];
- }
-
- get currentImage(): NativeImage {
- return this._nativeImages[this._currentFrame];
- }
-
- play(options: KeyframeAnimationOptions = {}) {
- const { startFrame, endFrame, beginFromCurrentState, advanceTo } = options;
-
- if (startFrame !== undefined && endFrame !== undefined) {
- if (startFrame < 0 || startFrame >= this._numFrames) {
- throw new Error('Invalid start frame');
- }
-
- if (endFrame < 0 || endFrame >= this._numFrames) {
- throw new Error('Invalid end frame');
- }
-
- if (startFrame < endFrame) {
- this._frameRange = [startFrame, endFrame];
- } else {
- this._frameRange = [endFrame, startFrame];
- }
- } else {
- this._frameRange = [0, this._numFrames - 1];
- }
-
- if (!beginFromCurrentState || this._isFirstRun) {
- this._currentFrame = this._frameRange[this._reverse ? 1 : 0];
- }
-
- if (this._isFirstRun) {
- this._isFirstRun = false;
- }
-
- if (advanceTo === 'end') {
- this._currentFrame = this._frameRange[this._reverse ? 0 : 1];
- }
-
- this._isRunning = true;
- this._isFinished = false;
-
- this._unscheduleUpdate();
-
- this._render();
- this._scheduleUpdate();
- }
-
- stop() {
- this._isRunning = false;
- this._unscheduleUpdate();
- }
-
- _unscheduleUpdate() {
- if (this._timeout) {
- clearTimeout(this._timeout);
- this._timeout = null;
- }
- }
-
- _scheduleUpdate() {
- this._timeout = setTimeout(() => this._onUpdateFrame(), this._speed);
- }
-
- _render() {
- if (this._onFrame) {
- this._onFrame(this._nativeImages[this._currentFrame]);
- }
- }
-
- _didFinish() {
- this._isFinished = true;
-
- if (this._onFinish) {
- this._onFinish();
- }
- }
-
- _onUpdateFrame() {
- this._advanceFrame();
-
- if (this._isFinished) {
- // mark animation as not running when finished
- this._isRunning = false;
- } else {
- this._render();
-
- // check once again since onFrame() may stop animation
- if (this._isRunning) {
- this._scheduleUpdate();
- }
- }
- }
-
- _advanceFrame() {
- if (this._isFinished) {
- return;
- }
-
- const lastFrame = this._frameRange[this._reverse ? 0 : 1];
- if (this._currentFrame === lastFrame) {
- // mark animation as finished if it's not repeating
- if (!this._repeat) {
- this._didFinish();
- return;
- }
-
- // change animation direction if marked for alternation
- if (this._alternate) {
- this._reverse = !this._reverse;
-
- this._currentFrame = this._nextFrame(this._currentFrame, this._frameRange, this._reverse);
- } else {
- this._currentFrame = this._frameRange[this._reverse ? 1 : 0];
- }
- } else {
- this._currentFrame = this._nextFrame(this._currentFrame, this._frameRange, this._reverse);
- }
- }
-
- _nextFrame(cur: number, frameRange: KeyframeAnimationRange, isReverse: boolean): number {
- if (isReverse) {
- if (cur < frameRange[0]) {
- return cur + 1;
- } else if (cur > frameRange[0]) {
- return cur - 1;
- }
- } else {
- if (cur > frameRange[1]) {
- return cur - 1;
- } else if (cur < frameRange[1]) {
- return cur + 1;
- }
- }
- return cur;
- }
-}
diff --git a/app/lib/platform.android.js b/app/lib/platform.android.js
deleted file mode 100644
index e71c1facf7..0000000000
--- a/app/lib/platform.android.js
+++ /dev/null
@@ -1,33 +0,0 @@
-// @flow
-
-import { BackHandler, Linking } from 'react-native';
-import { MobileAppBridge } from 'NativeModules';
-import { version } from '../../package.json';
-
-const log = console.log;
-
-const getAppVersion = () => {
- return version;
-};
-
-const getOpenAtLogin = () => {
- throw new Error('Not implemented');
-};
-
-const setOpenAtLogin = (_autoStart: boolean) => {
- throw new Error('Not implemented');
-};
-
-const exit = () => {
- BackHandler.exitApp();
-};
-
-const openLink = (link: string) => {
- Linking.openURL(link);
-};
-
-const openItem = (path: string) => {
- MobileAppBridge.openItem(path);
-};
-
-export { log, exit, openLink, openItem, getAppVersion, getOpenAtLogin, setOpenAtLogin };
diff --git a/app/lib/platform.js b/app/lib/platform.js
deleted file mode 100644
index cabb4951d9..0000000000
--- a/app/lib/platform.js
+++ /dev/null
@@ -1,118 +0,0 @@
-// @flow
-
-import fs from 'fs';
-import { remote, shell } from 'electron';
-import electronLog from 'electron-log';
-import { execFile } from 'child_process';
-import path from 'path';
-import { promisify } from 'util';
-
-const DESKTOP_FILE_NAME = 'mullvad-vpn.desktop';
-
-const execFileAsync = promisify(execFile);
-const mkdirAsync = promisify(fs.mkdir);
-const statAsync = promisify(fs.stat);
-const symlinkAsync = promisify(fs.symlink);
-const unlinkAsync = promisify(fs.unlink);
-
-const log = electronLog;
-
-const getAppVersion = () => {
- return remote.app.getVersion();
-};
-
-const getOpenAtLogin = (): boolean => {
- if (process.platform === 'linux') {
- try {
- const autostartDir = path.join(remote.app.getPath('appData'), 'autostart');
- const autostartFilePath = path.join(autostartDir, DESKTOP_FILE_NAME);
-
- fs.accessSync(autostartFilePath);
-
- return true;
- } catch (error) {
- log.debug(`Failed to check autostart file: ${error.message}`);
- return false;
- }
- } else {
- return remote.app.getLoginItemSettings().openAtLogin;
- }
-};
-
-const setOpenAtLogin = async (openAtLogin: boolean) => {
- // setLoginItemSettings is broken on macOS and cannot delete login items.
- // Issue: https://github.com/electron/electron/issues/10880
- if (process.platform === 'darwin') {
- if (openAtLogin === false) {
- // process.execPath in renderer process points to the sub-bundle of Electron Helper.
- // This regular expression extracts the path to the app bundle, which is the first occurrence of
- // file with .app extension.
- const matches = process.execPath.match(/([a-z0-9 ]+)\.app/i);
- if (matches && matches.length > 1) {
- const bundleName = matches[1];
- const appleScript = `on run argv
- set itemName to item 1 of argv
- tell application "System Events" to delete login item itemName
- end run`;
- await execFileAsync('osascript', ['-e', appleScript, bundleName]);
- } else {
- log.error(`Cannot extract the app bundle name from ${process.execPath}`);
- }
- } else {
- remote.app.setLoginItemSettings({ openAtLogin });
- }
- } else if (process.platform === 'linux') {
- try {
- const desktopFilePath = path.join('/usr/share/applications', DESKTOP_FILE_NAME);
- const autostartDir = path.join(remote.app.getPath('appData'), 'autostart');
- const autostartFilePath = path.join(autostartDir, DESKTOP_FILE_NAME);
-
- if (openAtLogin) {
- await createDirIfNecessary(autostartDir);
- await symlinkAsync(desktopFilePath, autostartFilePath);
- } else {
- await unlinkAsync(autostartFilePath);
- }
- } catch (error) {
- log.error(`Failed to set auto-start: ${error.message}`);
- }
- } else {
- remote.app.setLoginItemSettings({ openAtLogin });
- }
-};
-
-const createDirIfNecessary = async (directory: string) => {
- let stat;
- try {
- stat = await statAsync(directory);
- } catch (error) {
- // Path doesn't exist, so it has to be created
- return mkdirAsync(directory);
- }
-
- // Is there a file instead of a directory?
- if (!stat.isDirectory()) {
- // Try to remove existing file and replace it with a new directory
- try {
- await unlinkAsync(directory);
- } catch (error) {
- log.debug(`Failed to remove path before creating a directory for it: ${error.message}`);
- }
-
- return mkdirAsync(directory);
- }
-};
-
-const exit = () => {
- remote.app.quit();
-};
-
-const openLink = (link: string) => {
- shell.openExternal(link);
-};
-
-const openItem = (path: string) => {
- shell.openItem(path);
-};
-
-export { log, exit, openLink, openItem, getAppVersion, getOpenAtLogin, setOpenAtLogin };
diff --git a/app/lib/problem-report.android.js b/app/lib/problem-report.android.js
deleted file mode 100644
index 30a944e178..0000000000
--- a/app/lib/problem-report.android.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// @flow
-
-import { MobileAppBridge } from 'NativeModules';
-
-const collectProblemReport = (toRedact: string) => {
- return MobileAppBridge.collectProblemReport(toRedact);
-};
-
-const sendProblemReport = (email: string, message: string, savedReport: string) => {
- return MobileAppBridge.sendProblemReport(email, message, savedReport);
-};
-
-export { collectProblemReport, sendProblemReport };
diff --git a/app/lib/problem-report.js b/app/lib/problem-report.js
deleted file mode 100644
index 19d24f3e0f..0000000000
--- a/app/lib/problem-report.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// @flow
-
-import { ipcRenderer } from 'electron';
-import uuid from 'uuid';
-
-const collectProblemReport = (toRedact: Array<string>): Promise<string> => {
- return new Promise((resolve, reject) => {
- const requestId = uuid.v4();
- const responseListener = (_event, responseId, result) => {
- if (responseId === requestId) {
- ipcRenderer.removeListener('collect-logs-reply', responseListener);
- if (result.success) {
- resolve(result.reportPath);
- } else {
- reject(new Error(result.error));
- }
- }
- };
-
- ipcRenderer.on('collect-logs-reply', responseListener);
- ipcRenderer.send('collect-logs', requestId, toRedact);
- });
-};
-
-const sendProblemReport = (email: string, message: string, savedReport: string): Promise<void> => {
- return new Promise((resolve, reject) => {
- const requestId = uuid.v4();
- const responseListener = (_event, responseId, result) => {
- if (requestId === responseId) {
- ipcRenderer.removeListener('send-problem-report-reply', responseListener);
- if (result.success) {
- resolve();
- } else {
- reject(new Error(result.error));
- }
- }
- };
-
- ipcRenderer.on('send-problem-report-reply', responseListener);
- ipcRenderer.send('send-problem-report', requestId, email, message, savedReport);
- });
-};
-
-export { collectProblemReport, sendProblemReport };
diff --git a/app/lib/proc.js b/app/lib/proc.js
deleted file mode 100644
index 5e7396fa9a..0000000000
--- a/app/lib/proc.js
+++ /dev/null
@@ -1,26 +0,0 @@
-// @flow
-
-import path from 'path';
-
-export function resolveBin(binaryName: string) {
- const basepath = getBasePath();
- return path.resolve(basepath, binaryName + getExtension());
-}
-
-function getBasePath() {
- if (process.env.NODE_ENV === 'development') {
- return process.env.MULLVAD_PATH || './target/debug';
- } else {
- return process.resourcesPath;
- }
-}
-
-function getExtension() {
- switch (process.platform) {
- case 'win32':
- return '.exe';
-
- default:
- return '';
- }
-}
diff --git a/app/lib/reconnection-backoff.js b/app/lib/reconnection-backoff.js
deleted file mode 100644
index d777e86e57..0000000000
--- a/app/lib/reconnection-backoff.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// @flow
-
-/*
- * Used to calculate the time to wait before reconnecting to the daemon.
- * It uses a linear backoff function that goes from 500ms to 3000ms.
- */
-export default class ReconnectionBackoff {
- _attempt = 0;
-
- attempt(handler: () => void) {
- setTimeout(handler, this._getIncreasedBackoff());
- }
-
- reset() {
- this._attempt = 0;
- }
-
- _getIncreasedBackoff() {
- if (this._attempt < 6) {
- this._attempt++;
- }
- return this._attempt * 500;
- }
-}
diff --git a/app/lib/relay-settings-builder.js b/app/lib/relay-settings-builder.js
deleted file mode 100644
index 5f6c279e3f..0000000000
--- a/app/lib/relay-settings-builder.js
+++ /dev/null
@@ -1,194 +0,0 @@
-// @flow
-
-import type {
- RelayLocation,
- RelayProtocol,
- RelaySettingsUpdate,
- RelaySettingsNormalUpdate,
- RelaySettingsCustom,
-} from './daemon-rpc';
-
-type LocationBuilder<Self> = {
- country: (country: string) => Self,
- city: (country: string, city: string) => Self,
- any: () => Self,
- fromRaw: (location: 'any' | RelayLocation) => Self,
-};
-
-type OpenVPNConfigurator<Self> = {
- port: {
- exact: (port: number) => Self,
- any: () => Self,
- },
- protocol: {
- exact: (protocol: RelayProtocol) => Self,
- any: () => Self,
- },
-};
-
-type TunnelBuilder<Self> = {
- openvpn: (configurator: (OpenVPNConfigurator<*>) => void) => Self,
-};
-
-class NormalRelaySettingsBuilder {
- _payload: RelaySettingsNormalUpdate = {};
-
- build(): RelaySettingsUpdate {
- return {
- normal: this._payload,
- };
- }
-
- get location(): LocationBuilder<NormalRelaySettingsBuilder> {
- return {
- country: (country: string) => {
- this._payload.location = { only: { country } };
- return this;
- },
- city: (country: string, city: string) => {
- this._payload.location = { only: { city: [country, city] } };
- return this;
- },
- any: () => {
- this._payload.location = 'any';
- return this;
- },
- fromRaw: function(location: 'any' | RelayLocation) {
- if (location === 'any') {
- return this.any();
- }
-
- if (location.city) {
- const [country, city] = location.city;
- return this.city(country, city);
- }
-
- if (location.country) {
- return this.country(location.country);
- }
-
- throw new Error(
- 'Unsupported value of RelayLocation' + (location && JSON.stringify(location)),
- );
- },
- };
- }
-
- get tunnel(): TunnelBuilder<NormalRelaySettingsBuilder> {
- const updateOpenvpn = (next) => {
- const tunnel = this._payload.tunnel;
- if (typeof tunnel === 'string' || typeof tunnel === 'undefined') {
- this._payload.tunnel = {
- only: {
- openvpn: next,
- },
- };
- } else if (typeof tunnel === 'object') {
- const prev = (tunnel.only && tunnel.only.openvpn) || {};
- this._payload.tunnel = {
- only: {
- openvpn: { ...prev, ...next },
- },
- };
- }
- };
-
- return {
- openvpn: (configurator) => {
- const openvpnBuilder = {
- get port() {
- const apply = (port) => {
- updateOpenvpn({ port });
- return this;
- };
- return {
- exact: (value: number) => apply({ only: value }),
- any: () => apply('any'),
- };
- },
- get protocol() {
- const apply = (protocol) => {
- updateOpenvpn({ protocol });
- return this;
- };
- return {
- exact: (value: RelayProtocol) => apply({ only: value }),
- any: () => apply('any'),
- };
- },
- };
-
- configurator(openvpnBuilder);
-
- return this;
- },
- any: () => {
- this._payload.tunnel = 'any';
- return this;
- },
- };
- }
-}
-
-type CustomOpenVPNConfigurator<Self> = {
- port: (port: number) => Self,
- protocol: (protocol: RelayProtocol) => Self,
-};
-
-type CustomTunnelBuilder<Self> = {
- openvpn: (configurator: (CustomOpenVPNConfigurator<*>) => void) => Self,
-};
-
-class CustomRelaySettingsBuilder {
- _payload: RelaySettingsCustom = {
- host: '',
- tunnel: {
- openvpn: {
- port: 0,
- protocol: 'udp',
- },
- },
- };
-
- build(): RelaySettingsUpdate {
- return {
- custom_tunnel_endpoint: this._payload,
- };
- }
-
- host(value: string) {
- this._payload.host = value;
- return this;
- }
-
- get tunnel(): CustomTunnelBuilder<CustomRelaySettingsBuilder> {
- const updateOpenvpn = (next) => {
- const tunnel = this._payload.tunnel || {};
- const prev = tunnel.openvpn || {};
- this._payload.tunnel = {
- openvpn: { ...prev, ...next },
- };
- };
-
- return {
- openvpn: (configurator) => {
- configurator({
- port: function(port: number) {
- updateOpenvpn({ port });
- return this;
- },
- protocol: function(protocol: RelayProtocol) {
- updateOpenvpn({ protocol });
- return this;
- },
- });
- return this;
- },
- };
- }
-}
-
-export default {
- normal: () => new NormalRelaySettingsBuilder(),
- custom: () => new CustomRelaySettingsBuilder(),
-};
diff --git a/app/lib/rpc-address-file.js b/app/lib/rpc-address-file.js
deleted file mode 100644
index f05cc54fdc..0000000000
--- a/app/lib/rpc-address-file.js
+++ /dev/null
@@ -1,115 +0,0 @@
-// @flow
-
-import fs from 'fs';
-import path from 'path';
-import { app } from 'electron';
-import { promisify } from 'util';
-import { getSystemTemporaryDirectory } from './tempdir';
-
-const fsReadFileAsync = promisify(fs.readFile);
-
-const POLL_INTERVAL = 200;
-
-export type RpcCredentials = {
- connectionString: string,
- sharedSecret: string,
-};
-
-export class RpcAddressFile {
- _filePath = getRpcAddressFilePath();
- _pollIntervalId: ?IntervalID;
- _pollPromise: ?Promise<void>;
-
- get filePath(): string {
- return this._filePath;
- }
-
- waitUntilExists(): Promise<void> {
- let promise = this._pollPromise;
-
- if (!promise) {
- promise = new Promise((resolve) => {
- const timer = setInterval(() => {
- fs.exists(this._filePath, (exists) => {
- if (exists) {
- clearInterval(timer);
- resolve();
-
- this._pollPromise = null;
- }
- });
- }, POLL_INTERVAL);
- });
-
- this._pollPromise = promise;
- }
-
- return promise;
- }
-
- async parse(): Promise<RpcCredentials> {
- const data = await fsReadFileAsync(this._filePath, 'utf8');
- const [connectionString, sharedSecret] = data.split('\n', 2);
-
- if (connectionString && sharedSecret !== undefined) {
- return {
- connectionString,
- sharedSecret,
- };
- } else {
- throw new Error('Cannot parse the RPC address file');
- }
- }
-
- isTrusted() {
- const filePath = this._filePath;
- switch (process.platform) {
- case 'win32':
- return isOwnedByLocalSystem(filePath);
- case 'darwin':
- case 'linux':
- return isOwnedAndOnlyWritableByRoot(filePath);
- default:
- throw new Error(`Unknown platform: ${process.platform}`);
- }
- }
-}
-
-function getRpcAddressFilePath() {
- const rpcAddressFileName = '.mullvad_rpc_address';
-
- switch (process.platform) {
- case 'win32': {
- // Windows: %ALLUSERSPROFILE%\{appname}
- const programDataDirectory = process.env.ALLUSERSPROFILE;
- if (programDataDirectory) {
- const appDataDirectory = path.join(programDataDirectory, app.getName());
- return path.join(appDataDirectory, rpcAddressFileName);
- } else {
- throw new Error('Missing %ALLUSERSPROFILE% environment variable');
- }
- }
- default:
- return path.join(getSystemTemporaryDirectory(), rpcAddressFileName);
- }
-}
-
-function isOwnedAndOnlyWritableByRoot(path: string): boolean {
- const stat = fs.statSync(path);
- const isOwnedByRoot = stat.uid === 0;
- const isOnlyWritableByOwner = (stat.mode & parseInt('022', 8)) === 0;
-
- return isOwnedByRoot && isOnlyWritableByOwner;
-}
-
-function isOwnedByLocalSystem(path: string): boolean {
- // $FlowFixMe: this module is only available on Windows
- const winsec = require('windows-security');
- const ownerSid = winsec.getFileOwnerSid(path, null);
- const isWellKnownSid = winsec.isWellKnownSid(
- ownerSid,
- winsec.WellKnownSid.BuiltinAdministratorsSid,
- );
-
- return isWellKnownSid;
-}
diff --git a/app/lib/tempdir.js b/app/lib/tempdir.js
deleted file mode 100644
index a3b4fa6d89..0000000000
--- a/app/lib/tempdir.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// @flow
-
-import path from 'path';
-
-export function getSystemTemporaryDirectory() {
- switch (process.platform) {
- case 'win32': {
- const windowsPath = process.env.windir;
- if (windowsPath) {
- return path.join(windowsPath, 'Temp');
- } else {
- throw new Error('Missing windir in environment variables.');
- }
- }
- case 'darwin':
- case 'linux':
- return '/tmp';
- default:
- throw new Error(`Not implemented for ${process.platform}`);
- }
-}
diff --git a/app/lib/transition-rule.js b/app/lib/transition-rule.js
deleted file mode 100644
index dfb56b02ce..0000000000
--- a/app/lib/transition-rule.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// @flow
-
-export type TransitionDescriptor = {
- name: string,
- duration: number,
-};
-
-export type TransitionFork = {
- forward: TransitionDescriptor,
- backward: TransitionDescriptor,
-};
-
-export type TransitionMatch = {
- direction: 'forward' | 'backward',
- descriptor: TransitionDescriptor,
-};
-
-export default class TransitionRule {
- _from: ?string;
- _to: string;
- _fork: TransitionFork;
-
- constructor(from: ?string, to: string, fork: TransitionFork) {
- this._from = from;
- this._to = to;
- this._fork = fork;
- }
-
- match(fromRoute: ?string, toRoute: string): ?TransitionMatch {
- if ((!this._from || this._from === fromRoute) && this._to === toRoute) {
- return {
- direction: 'forward',
- descriptor: this._fork['forward'],
- };
- }
-
- if ((!this._from || this._from === toRoute) && this._to === fromRoute) {
- return {
- direction: 'backward',
- descriptor: this._fork['backward'],
- };
- }
-
- return null;
- }
-}
diff --git a/app/lib/window-state-observer.js b/app/lib/window-state-observer.js
deleted file mode 100644
index 67252d8f6a..0000000000
--- a/app/lib/window-state-observer.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// @flow
-
-import { remote } from 'electron';
-
-type EventListener = () => void;
-
-// Tiny helper for detecting the window state.
-export default class WindowStateObserver {
- _onShow: ?EventListener;
- _onHide: ?EventListener;
-
- get onShow() {
- return this._onShow;
- }
-
- set onShow(listener: ?EventListener) {
- const currentWindow = remote.getCurrentWindow();
- const oldListener = this._onShow;
- if (oldListener) {
- currentWindow.removeListener('show', oldListener);
- }
-
- if (listener) {
- currentWindow.addListener('show', listener);
- }
-
- this._onShow = listener;
- }
-
- get onHide() {
- return this._onHide;
- }
-
- set onHide(listener: ?EventListener) {
- const currentWindow = remote.getCurrentWindow();
- const oldListener = this._onHide;
- if (oldListener) {
- currentWindow.removeListener('hide', oldListener);
- }
-
- if (listener) {
- currentWindow.addListener('hide', listener);
- }
-
- this._onHide = listener;
- }
-
- constructor() {
- // Because BrowserWindow persists between page reloads,
- // it's important to release event handlers when that happens.
- window.addEventListener('beforeunload', this._onBeforeUnload);
- }
-
- _onBeforeUnload = () => {
- this.dispose();
- };
-
- dispose() {
- this.onShow = null;
- this.onHide = null;
-
- window.removeEventListener('beforeunload', this._onBeforeUnload);
- }
-}