summaryrefslogtreecommitdiffhomepage
path: root/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/components')
-rw-r--r--app/components/Account.js47
-rw-r--r--app/components/AccountInput.js187
-rw-r--r--app/components/Connect.js52
-rw-r--r--app/components/CustomScrollbars.js23
-rw-r--r--app/components/HeaderBar.js21
-rw-r--r--app/components/Layout.js69
-rw-r--r--app/components/Login.js140
-rw-r--r--app/components/SelectLocation.js39
-rw-r--r--app/components/Settings.js45
-rw-r--r--app/components/Switch.js89
-rw-r--r--app/components/WindowChrome.js18
11 files changed, 321 insertions, 409 deletions
diff --git a/app/components/Account.js b/app/components/Account.js
index 7aaec4f2e6..e817baa280 100644
--- a/app/components/Account.js
+++ b/app/components/Account.js
@@ -1,43 +1,40 @@
+// @flow
import moment from 'moment';
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { If, Then, Else } from 'react-if';
import { Layout, Container, Header } from './Layout';
import { formatAccount } from '../lib/formatters';
import ExternalLinkSVG from '../assets/images/icon-extLink.svg';
-export default class Account extends Component {
-
- static propTypes = {
- onLogout: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- onExternalLink: PropTypes.func.isRequired
- }
+import type { UserReduxState } from '../reducers/user';
- onClose() {
- this.props.onClose();
- }
+export type AccountProps = {
+ user: UserReduxState;
+ onLogout: () => void;
+ onClose: () => void;
+ onExternalLink: (type: string) => void;
+};
- onExternalLink(type) {
- this.props.onExternalLink(type);
- }
+export default class Account extends Component {
+ props: AccountProps;
- onLogout() {
- this.props.onLogout();
- }
+ onBuyMore = () => this.props.onExternalLink('purchase');
+ onClose = () => this.props.onClose();
+ onLogout = () => this.props.onLogout();
- render() {
- let paidUntil = moment(this.props.user.paidUntil);
- let formattedAccountId = formatAccount(this.props.user.account);
- let formattedPaidUntil = paidUntil.format('hA, D MMMM YYYY').toUpperCase();
- let isOutOfTime = paidUntil.isSameOrBefore(moment());
+ render(): React.Element<*> {
+ const user = this.props.user;
+ const paidUntil = moment(user.paidUntil);
+ const formattedAccountId = formatAccount(user.account || '');
+ const formattedPaidUntil = paidUntil.format('hA, D MMMM YYYY').toUpperCase();
+ const isOutOfTime = paidUntil.isSameOrBefore(moment());
return (
<Layout>
<Header hidden={ true } style={ 'defaultDark' } />
<Container>
<div className="account">
- <div className="account__close" onClick={ ::this.onClose }>
+ <div className="account__close" onClick={ this.onClose }>
<img className="account__close-icon" src="./assets/images/icon-back.svg" />
<span className="account__close-title">Settings</span>
</div>
@@ -68,11 +65,11 @@ export default class Account extends Component {
</div>
<div className="account__footer">
- <button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }>
+ <button className="button button--positive" onClick={ this.onBuyMore }>
<span className="button-label">Buy more time</span>
<ExternalLinkSVG className="button-icon button-icon--16" />
</button>
- <button className="button button--negative" onClick={ ::this.onLogout }>Logout</button>
+ <button className="button button--negative" onClick={ this.onLogout }>Logout</button>
</div>
</div>
diff --git a/app/components/AccountInput.js b/app/components/AccountInput.js
index 3e6993a9bb..0aeed76551 100644
--- a/app/components/AccountInput.js
+++ b/app/components/AccountInput.js
@@ -1,56 +1,57 @@
+// @flow
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { formatAccount } from '../lib/formatters';
-/**
- * Account input field with automatic formatting
- *
- * @export
- * @class AccountInput
- * @extends {React.Component}
- */
+// @TODO: move it into types.js
+
+// ESLint issue: https://github.com/babel/babel-eslint/issues/445
+/* eslint-disable no-unused-vars */
+declare class ClipboardData {
+ setData(type: string, data: string): void;
+ getData(type: string): string;
+}
+
+declare class ClipboardEvent extends Event {
+ clipboardData: ClipboardData;
+}
+
+type AccountInputProps = {
+ value: string;
+ onEnter: () => void;
+ onChange: (newValue: string) => void;
+};
+
+type AccountInputState = {
+ value: string;
+ selectionRange: SelectionRange;
+};
+
+type SelectionRange = [number, number];
+
export default class AccountInput extends Component {
+ props: AccountInputProps;
+ state: AccountInputState = {
+ value: '',
+ selectionRange: [0, 0]
+ };
- /**
- * Prop types
- * @static
- *
- * @memberOf AccountInput
- */
- static propTypes = {
- value: PropTypes.string,
- onEnter: PropTypes.func,
- onChange: PropTypes.func
- }
+ _ref: ?HTMLInputElement;
+ _ignoreSelect = false;
- /**
- * Creates an instance of AccountInput.
- * @param {object} props
- *
- * @memberOf AccountInput
- */
- constructor(props) {
+ constructor(props: AccountInputProps) {
super(props);
// selection range holds selection converted from DOM selection range to
// internal unformatted representation of account number
const val = this.sanitize(props.value);
- /**
- * @type {object}
- * @property {string} value - raw text value
- * @property {number[]} selectionRange - raw text mapped selection range [start, end]
- */
this.state = {
value: val,
selectionRange: [val.length, val.length]
};
}
- /**
- * @override
- */
- componentWillReceiveProps(nextProps) {
+ componentWillReceiveProps(nextProps: AccountInputProps) {
const nextVal = this.sanitize(nextProps.value);
if(nextVal !== this.state.value) {
const len = nextVal.length;
@@ -58,10 +59,7 @@ export default class AccountInput extends Component {
}
}
- /**
- * @override
- */
- shouldComponentUpdate(nextProps, nextState) {
+ shouldComponentUpdate(nextProps: AccountInputProps, nextState: AccountInputState) {
return (this.props.value !== nextProps.value ||
this.props.onEnter !== nextProps.onEnter ||
this.props.onChange !== nextProps.onChange ||
@@ -70,31 +68,20 @@ export default class AccountInput extends Component {
this.state.selectionRange[1] !== nextState.selectionRange[1]);
}
- /**
- * @override
- */
- render() {
+ render(): React.Element<*> {
const displayString = formatAccount(this.state.value || '');
- const props = Object.assign({}, this.props);
-
- // exclude built-in props
- for(let key of Object.keys(AccountInput.propTypes)) {
- if(props.hasOwnProperty(key)) {
- delete props[key];
- }
- }
-
+ const { value, onChange, onEnter, ...otherProps } = this.props;
return (
- <input type="text"
+ <input { ...otherProps }
+ type="text"
value={ displayString }
onChange={ () => {} }
- onSelect={ ::this.onSelect }
- onKeyUp={ ::this.onKeyUp }
- onKeyDown={ ::this.onKeyDown }
- onPaste={ ::this.onPaste }
- onCut={ ::this.onCut }
- ref={ ::this.onRef }
- { ...props } />
+ onSelect={ this.onSelect }
+ onKeyUp={ this.onKeyUp }
+ onKeyDown={ this.onKeyDown }
+ onPaste={ this.onPaste }
+ onCut={ this.onCut }
+ ref={ this.onRef } />
);
}
@@ -102,14 +89,8 @@ export default class AccountInput extends Component {
/**
* Modify original string inserting substring using selection range
- *
- * @private
- * @param {String?} val string
- * @returns {String}
- *
- * @memberOf AccountInput
*/
- sanitize(val) {
+ sanitize(val: ?string): string {
return (val || '').replace(/[^0-9]/g, '');
}
@@ -121,10 +102,8 @@ export default class AccountInput extends Component {
* @param {String} insert insertion string
* @param {Array} selRange selection range ([x,y])
* @returns {Object}
- *
- * @memberOf AccountInput
*/
- insert(val, insert, selRange) {
+ insert(val: string, insert: string, selRange: SelectionRange): AccountInputState {
const head = val.slice(0, selRange[0]);
const tail = val.slice(selRange[1], val.length);
const newVal = head + insert + tail;
@@ -144,7 +123,7 @@ export default class AccountInput extends Component {
*
* @memberOf AccountInput
*/
- remove(val, selRange) {
+ remove(val: string, selRange: SelectionRange): AccountInputState {
let newVal, selectionOffset;
if(selRange[0] === selRange[1]) {
@@ -174,7 +153,7 @@ export default class AccountInput extends Component {
*
* @memberOf AccountInput
*/
- toInternalSelectionRange(val, domRange) {
+ toInternalSelectionRange(val: string, domRange: SelectionRange): SelectionRange {
const countSpaces = (val) => {
return (val.match(/\s/g) || []).length;
};
@@ -202,7 +181,7 @@ export default class AccountInput extends Component {
*
* @memberOf AccountInput
*/
- toDomSelection(val, selRange) {
+ toDomSelection(val: string, selRange: SelectionRange): SelectionRange {
const countSpaces = (val, untilIndex) => {
if(val.length > 12) { return 0; }
return Math.floor(untilIndex / 4); // groups of 4 digits
@@ -221,13 +200,7 @@ export default class AccountInput extends Component {
// Events
- /**
- * Key down handler
- * @private
- * @param {event} e
- * @memberOf AccountInput
- */
- onKeyDown(e) {
+ onKeyDown = (e: KeyboardEvent) => {
const { value, selectionRange } = this.state;
if(e.which === 8) { // backspace
@@ -255,13 +228,7 @@ export default class AccountInput extends Component {
}
}
- /**
- * Key up handler
- * @private
- * @param {event} e
- * @memberOf AccountInput
- */
- onKeyUp(e) {
+ onKeyUp = (e: KeyboardEvent) => {
this._ignoreSelect = false;
if(e.which === 13 && this.props.onEnter) {
@@ -269,14 +236,11 @@ export default class AccountInput extends Component {
}
}
- /**
- * Select handler
- * @private
- * @param {event} e
- * @memberOf AccountInput
- */
- onSelect(e) {
+ onSelect = (e: Event) => {
const ref = e.target;
+ if(!(ref instanceof HTMLInputElement)) {
+ throw new Error('ref must be an instance of HTMLInputElement');
+ }
if(this._ignoreSelect) {
return;
@@ -288,13 +252,7 @@ export default class AccountInput extends Component {
this.setState({ selectionRange: selRange });
}
- /**
- * Paste handler
- * @private
- * @param {event} e
- * @memberOf AccountInput
- */
- onPaste(e) {
+ onPaste = (e: ClipboardEvent) => {
const { value, selectionRange } = this.state;
const pastedData = e.clipboardData.getData('text');
const filteredData = this.sanitize(pastedData);
@@ -307,13 +265,12 @@ export default class AccountInput extends Component {
});
}
- /**
- * Cut handler
- * @private
- * @param {event} e
- * @memberOf AccountInput
- */
- onCut(e) {
+ onCut = (e: ClipboardEvent) => {
+ const target = e.target;
+ if(!(target instanceof HTMLInputElement)) {
+ throw new Error('ref must be an instance of HTMLInputElement');
+ }
+
const { value, selectionRange } = this.state;
e.preventDefault();
@@ -322,7 +279,7 @@ export default class AccountInput extends Component {
if(selectionRange[0] !== selectionRange[1]) {
const result = this.remove(value, selectionRange);
const domSelectionRange = this.toDomSelection(value, selectionRange);
- const slice = e.target.value.slice(domSelectionRange[0], domSelectionRange[1]);
+ const slice = target.value.slice(domSelectionRange[0], domSelectionRange[1]);
e.clipboardData.setData('text', slice);
@@ -334,13 +291,7 @@ export default class AccountInput extends Component {
}
}
- /**
- * Reference handler
- * @private
- * @param {DOMElement} ref
- * @memberOf AccountInput
- */
- onRef(ref) {
+ onRef = (ref: HTMLInputElement) => {
this._ref = ref;
if(!ref) { return; }
@@ -351,10 +302,6 @@ export default class AccountInput extends Component {
ref.selectionEnd = domRange[1];
}
- /**
- * Focus on text field
- * @memberOf AccountInput
- */
focus() {
if(this._ref) {
this._ref.focus();
diff --git a/app/components/Connect.js b/app/components/Connect.js
index 6ffb516ec6..7ad0d4cc8b 100644
--- a/app/components/Connect.js
+++ b/app/components/Connect.js
@@ -22,21 +22,21 @@ type DisplayLocation = {
city: ?string;
};
-export default class Connect extends Component {
-
- props: {
- user: UserReduxState,
- connect: ConnectReduxState,
- settings: SettingsReduxState,
- onSettings: () => void,
- onSelectLocation: () => void,
- onConnect: (address: string) => void,
- onCopyIP: () => void,
- onDisconnect: () => void,
- onExternalLink: (type: string) => void,
- getServerInfo: (identifier: string) => ?ServerInfo
- };
+export type ConnectProps = {
+ user: UserReduxState,
+ connect: ConnectReduxState,
+ settings: SettingsReduxState,
+ onSettings: () => void,
+ onSelectLocation: () => void,
+ onConnect: (address: string) => void,
+ onCopyIP: () => void,
+ onDisconnect: () => void,
+ onExternalLink: (type: string) => void,
+ getServerInfo: (identifier: string) => ?ServerInfo
+};
+export default class Connect extends Component {
+ props: ConnectProps;
state = {
isFirstPass: true,
showCopyIPMessage: false
@@ -89,12 +89,12 @@ export default class Connect extends Component {
</div>
<If condition={ error.type === 'NO_CREDIT' }>
<Then>
- <div>
- <button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }>
- <span className="button-label">Buy more time</span>
- <ExternalLinkSVG className="button-icon button-icon--16" />
- </button>
- </div>
+ <div>
+ <button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }>
+ <span className="button-label">Buy more time</span>
+ <ExternalLinkSVG className="button-icon button-icon--16" />
+ </button>
+ </div>
</Then>
</If>
</div>
@@ -129,12 +129,12 @@ export default class Connect extends Component {
<div className="connect">
<div className="connect__map">
<ReactMapboxGl
- style={ mapboxConfig.styleURL }
- accessToken={ mapboxConfig.accessToken }
- containerStyle={{ height: '100%' }}
- interactive={ false }
- fitBounds={ mapBounds }
- fitBoundsOptions={ mapBoundsOptions }>
+ style={ mapboxConfig.styleURL }
+ accessToken={ mapboxConfig.accessToken }
+ containerStyle={{ height: '100%' }}
+ interactive={ false }
+ fitBounds={ mapBounds }
+ fitBoundsOptions={ mapBoundsOptions }>
<If condition={ isConnected }>
<Then>
<Marker coordinates={ serverLocation } offset={ [0, -10] }>
diff --git a/app/components/CustomScrollbars.js b/app/components/CustomScrollbars.js
index e74cbd7c65..4a0899c1be 100644
--- a/app/components/CustomScrollbars.js
+++ b/app/components/CustomScrollbars.js
@@ -1,28 +1,13 @@
+// @flow
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { Scrollbars } from 'react-custom-scrollbars';
-/**
- * Custom scrollbars component
- *
- * @export
- * @class CustomScrollbars
- * @extends {React.Component}
- */
export default class CustomScrollbars extends Component {
- /**
- * PropTypes
- * @static
- * @memberOf CustomScrollbars
- */
- static propTypes = {
- children: PropTypes.element
+ props: {
+ children: ?React.Element<*>
}
- /**
- * @override
- */
- render() {
+ render(): React.Element<*> {
return (
<Scrollbars
{ ...this.props }
diff --git a/app/components/HeaderBar.js b/app/components/HeaderBar.js
index 5574951a06..bae3c4fddf 100644
--- a/app/components/HeaderBar.js
+++ b/app/components/HeaderBar.js
@@ -3,20 +3,21 @@ import React, { Component } from 'react';
import { If, Then } from 'react-if';
export type HeaderBarStyle = 'default' | 'defaultDark' | 'error' | 'success';
+export type HeaderBarProps = {
+ style: HeaderBarStyle;
+ hidden: boolean;
+ showSettings: boolean;
+ onSettings: ?(() => void);
+};
-/**
- * Header bar component
- */
export default class HeaderBar extends Component {
-
- props: {
- style: HeaderBarStyle,
- hidden: boolean,
- showSettings: boolean,
- onSettings: () => void
+ props: HeaderBarProps;
+ static defaultProps: $Shape<HeaderBarProps> = {
+ hidden: false,
+ showSettings: false
};
- render() {
+ render(): React.Element<*> {
let containerClass = [
'headerbar',
'headerbar--' + process.platform,
diff --git a/app/components/Layout.js b/app/components/Layout.js
index 58af3d0813..5c0e1f5bcb 100644
--- a/app/components/Layout.js
+++ b/app/components/Layout.js
@@ -1,20 +1,14 @@
+// @flow
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import HeaderBar from './HeaderBar';
-/**
- * Layout header
- *
- * @export
- * @class Header
- * @extends {React.Component}
- */
+import type { HeaderBarProps } from './HeaderBar';
+
export class Header extends Component {
+ props: HeaderBarProps;
+ static defaultProps = HeaderBar.defaultProps;
- /**
- * @override
- */
- render() {
+ render(): React.Element<*> {
return (
<div className="layout__header">
<HeaderBar { ...this.props } />
@@ -23,28 +17,12 @@ export class Header extends Component {
}
}
-/**
- * Content container
- *
- * @export
- * @class Container
- * @extends {React.Component}
- */
export class Container extends Component {
+ props: {
+ children: React.Element<*>
+ }
- /**
- * PropTypes
- * @static
- * @memberOf Container
- */
- static propTypes = {
- children: PropTypes.element.isRequired
- };
-
- /**
- * @override
- */
- render() {
+ render(): React.Element<*> {
return (
<div className="layout__container">
{ this.props.children }
@@ -53,31 +31,12 @@ export class Container extends Component {
}
}
-/**
- * Layout container
- *
- * @export
- * @class Layout
- * @extends {React.Component}
- */
export class Layout extends Component {
+ props: {
+ children: Array<React.Element<*>> | React.Element<*>
+ }
- /**
- * PropTypes
- * @static
- * @memberOf Container
- */
- static propTypes = {
- children: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.element),
- PropTypes.element,
- ])
- };
-
- /**
- * @override
- */
- render() {
+ render(): React.Element<*> {
return (
<div className="layout">
{ this.props.children }
diff --git a/app/components/Login.js b/app/components/Login.js
index 72ef933082..9daa3f7e78 100644
--- a/app/components/Login.js
+++ b/app/components/Login.js
@@ -1,58 +1,41 @@
+// @flow
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { If, Then } from 'react-if';
import { Layout, Container, Header } from './Layout';
import AccountInput from './AccountInput';
import ExternalLinkSVG from '../assets/images/icon-extLink.svg';
import LoginArrowSVG from '../assets/images/icon-arrow.svg';
-export default class Login extends Component {
- static propTypes = {
- user: PropTypes.object.isRequired,
- onLogin: PropTypes.func.isRequired,
- onSettings: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onFirstChangeAfterFailure: PropTypes.func.isRequired,
- onExternalLink: PropTypes.func.isRequired,
- };
+import type { LoginState } from '../enums';
+import type { UserReduxState } from '../reducers/user';
- constructor(props) {
- super(props);
- this.state = {
- notifyOnFirstChangeAfterFailure: false,
- isActive: false
- };
- }
+export type LoginPropTypes = {
+ user: UserReduxState,
+ onLogin: (accountNumber: string) => void,
+ onSettings: ?(() => void),
+ onChange: (input: string) => void,
+ onFirstChangeAfterFailure: () => void,
+ onExternalLink: (type: string) => void,
+};
- componentWillReceiveProps(nextProps) {
- const prev = this.props.user || {};
- const next = nextProps.user || {};
-
- if(prev.status !== next.status && next.status === 'failed') {
- this.setState({ notifyOnFirstChangeAfterFailure: true });
- }
+export default class Login extends Component {
+ props: LoginPropTypes;
+ state = {
+ notifyOnFirstChangeAfterFailure: false,
+ isActive: false
}
- onLogin() {
+ onCreateAccount = () => this.props.onExternalLink('createAccount');
+ onFocus = () => this.setState({ isActive: true });
+ onBlur = () => this.setState({ isActive: false });
+ onLogin = () => {
const { account } = this.props.user;
- if(account.length > 0) {
+ if(account && account.length > 0) {
this.props.onLogin(account);
}
}
- onCreateAccount() {
- this.props.onExternalLink('createAccount');
- }
-
- onFocus() {
- this.setState({ isActive: true });
- }
-
- onBlur() {
- this.setState({ isActive: false });
- }
-
- onInputChange(val) {
+ onInputChange = (val: string) => {
// notify delegate on first change after login failure
if(this.state.notifyOnFirstChangeAfterFailure) {
this.setState({ notifyOnFirstChangeAfterFailure: false });
@@ -61,7 +44,7 @@ export default class Login extends Component {
this.props.onChange(val);
}
- formTitle(s) {
+ formTitle(s: LoginState): string {
switch(s) {
case 'connecting': return 'Logging in...';
case 'failed': return 'Login failed';
@@ -70,22 +53,22 @@ export default class Login extends Component {
}
}
- formSubtitle(s, e) {
+ formSubtitle(s: LoginState, e: ?Error): string {
switch(s) {
- case 'failed': return e.message;
+ case 'failed': return (e && e.message) || 'Unknown error';
case 'connecting': return 'Checking account number';
default: return 'Enter your account number';
}
}
- inputWrapClass(user) {
+ inputWrapClass(s: LoginState): string {
const classes = ['login-form__input-wrap'];
if(this.state.isActive) {
classes.push('login-form__input-wrap--active');
}
- switch(user.status) {
+ switch(s) {
case 'connecting':
classes.push('login-form__input-wrap--inactive');
break;
@@ -97,9 +80,9 @@ export default class Login extends Component {
return classes.join(' ');
}
- footerClass(user) {
+ footerClass(s: LoginState): string {
const classes = ['login-footer'];
- switch(user.status) {
+ switch(s) {
case 'ok':
case 'connecting':
classes.push('login-footer--invisible');
@@ -108,31 +91,46 @@ export default class Login extends Component {
return classes.join(' ');
}
- submitClass(user) {
+ submitClass(s: LoginState, account: ?string): string {
const classes = ['login-form__submit'];
- if(typeof(user.account) === 'string' && user.account.length > 0) {
+ if(account && account.length > 0) {
classes.push('login-form__submit--active');
}
- if(user.status === 'connecting') {
+ if(s === 'connecting') {
classes.push('login-form__submit--invisible');
}
return classes.join(' ');
}
- render() {
+ componentWillReceiveProps(nextProps: LoginPropTypes) {
+ const prev = this.props.user || {};
+ const next = nextProps.user || {};
+
+ if(prev.status !== next.status && next.status === 'failed') {
+ this.setState({ notifyOnFirstChangeAfterFailure: true });
+ }
+ }
+
+ render(): React.Element<*> {
const { account, status, error } = this.props.user;
const title = this.formTitle(status);
const subtitle = this.formSubtitle(status, error);
- const isConnecting = status === 'connecting';
- const isFailed = status === 'failed';
- const isLoggedIn = status === 'ok';
- const inputWrapClass = this.inputWrapClass(this.props.user);
- const footerClass = this.footerClass(this.props.user);
- const submitClass = this.submitClass(this.props.user);
+ let isConnecting = false;
+ let isFailed = false;
+ let isLoggedIn = false;
+ switch(status) {
+ case 'connecting': isConnecting = true; break;
+ case 'failed': isFailed = true; break;
+ case 'ok': isLoggedIn = true; break;
+ }
+
+ const inputWrapClass = this.inputWrapClass(status);
+ const footerClass = this.footerClass(status);
+ const submitClass = this.submitClass(status, account);
const autoFocusRef = input => {
if(isFailed && input) {
@@ -178,25 +176,25 @@ export default class Login extends Component {
<div className="login-form__subtitle">{ subtitle }</div>
<div className={ inputWrapClass }>
<AccountInput className="login-form__input-field"
- type="text"
- placeholder="e.g 0000 0000 0000"
- onFocus={ ::this.onFocus }
- onBlur={ ::this.onBlur }
- onChange={ ::this.onInputChange }
- onEnter={ ::this.onLogin }
- value={ account }
- disabled={ isConnecting }
- autoFocus={ true }
- ref={ autoFocusRef } />
- <button className={ submitClass } onClick={ ::this.onLogin }>
- <LoginArrowSVG className="login-form__submit-icon" />
- </button>
+ type="text"
+ placeholder="e.g 0000 0000 0000"
+ onFocus={ this.onFocus }
+ onBlur={ this.onBlur }
+ onChange={ this.onInputChange }
+ onEnter={ this.onLogin }
+ value={ account || '' }
+ disabled={ isConnecting }
+ autoFocus={ true }
+ ref={ autoFocusRef } />
+ <button className={ submitClass } onClick={ this.onLogin }>
+ <LoginArrowSVG className="login-form__submit-icon" />
+ </button>
</div>
</div>
</div>
<div className={ footerClass }>
- <div className="login-footer__prompt">Don't have an account number?</div>
- <button className="button button--primary" onClick={ ::this.onCreateAccount }>
+ <div className="login-footer__prompt">{ 'Don\'t have an account number?' }</div>
+ <button className="button button--primary" onClick={ this.onCreateAccount }>
<span className="button-label">Create account</span>
<ExternalLinkSVG className="button-icon button-icon--16" />
</button>
diff --git a/app/components/SelectLocation.js b/app/components/SelectLocation.js
index 0dd52ab2bb..c94df2a11d 100644
--- a/app/components/SelectLocation.js
+++ b/app/components/SelectLocation.js
@@ -1,26 +1,31 @@
+// @flow
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { If, Then } from 'react-if';
import { Layout, Container, Header } from './Layout';
import { servers } from '../config';
import CustomScrollbars from './CustomScrollbars';
-export default class SelectLocation extends Component {
+import type { SettingsReduxState } from '../reducers/settings';
- static propTypes = {
- onClose: PropTypes.func.isRequired,
- onSelect: PropTypes.func.isRequired
- }
+export type SelectLocationProps = {
+ settings: SettingsReduxState,
+ onClose: () => void;
+ onSelect: (server: string) => void;
+};
+
+export default class SelectLocation extends Component {
+ props: SelectLocationProps;
+ _selectedCell: ?HTMLElement;
- onSelect(name) {
+ onSelect(name: string) {
this.props.onSelect(name);
}
- isSelected(key) {
- return key === this.props.settings.preferredServer;
+ isSelected(server: string) {
+ return server === this.props.settings.preferredServer;
}
- drawCell(key, name, icon, onClick) {
+ drawCell(key: string, name: string, icon: ?string, onClick: (e: Event) => void): React.Element<*> {
const classes = ['select-location__cell'];
const selected = this.isSelected(key);
@@ -51,7 +56,7 @@ export default class SelectLocation extends Component {
);
}
- onCellRef(key, element) {
+ onCellRef(key: string, element: HTMLElement) {
// save reference to selected cell
if(this.isSelected(key)) {
this._selectedCell = element;
@@ -60,12 +65,18 @@ export default class SelectLocation extends Component {
componentDidMount() {
// restore scroll to selected cell
- if(this._selectedCell) {
- this._selectedCell.scrollIntoViewIfNeeded(true);
+ const cell = this._selectedCell;
+ if(cell) {
+ // this is non-standard webkit method but it works great!
+ if(typeof(cell.scrollIntoViewIfNeeded) !== 'function') {
+ console.warn('HTMLElement.scrollIntoViewIfNeeded() is not available anymore! Please replace it with viable alternative.');
+ return;
+ }
+ cell.scrollIntoViewIfNeeded(true);
}
}
- render() {
+ render(): React.Element<*> {
return (
<Layout>
<Header hidden={ true } style={ 'defaultDark' } />
diff --git a/app/components/Settings.js b/app/components/Settings.js
index eaec0355f2..9b8eff5321 100644
--- a/app/components/Settings.js
+++ b/app/components/Settings.js
@@ -1,39 +1,36 @@
+// @flow
import moment from 'moment';
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { If, Then, Else } from 'react-if';
import { Layout, Container, Header } from './Layout';
import Switch from './Switch';
import CustomScrollbars from './CustomScrollbars';
-export default class Settings extends Component {
+import type { UserReduxState } from '../reducers/user';
+import type { SettingsReduxState } from '../reducers/settings';
- static propTypes = {
- onQuit: PropTypes.func.isRequired,
- onLogout: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- onViewAccount: PropTypes.func.isRequired,
- onExternalLink: PropTypes.func.isRequired,
- onUpdateSettings: PropTypes.func.isRequired
- }
+export type SettingsProps = {
+ user: UserReduxState,
+ settings: SettingsReduxState,
+ onQuit: () => void,
+ onClose: () => void,
+ onViewAccount: () => void,
+ onExternalLink: (type: string) => void,
+ onUpdateSettings: (update: $Shape<SettingsReduxState>) => void
+};
- onClose() {
- this.props.onClose();
- }
+export default class Settings extends Component {
- onAutoSecure(isOn) {
- this.props.onUpdateSettings({ autoSecure: isOn });
- }
+ props: SettingsProps;
- onExternalLink(type) {
- this.props.onExternalLink(type);
- }
+ onClose = () => this.props.onClose();
+ onAutoSecure = (autoSecure: boolean) => this.props.onUpdateSettings({ autoSecure });
- onLogout() {
- this.props.onLogout();
+ onExternalLink(type: string) {
+ this.props.onExternalLink(type);
}
- render() {
+ render(): React.Element<*> {
const isLoggedIn = this.props.user.status === 'ok';
let isOutOfTime = false, formattedPaidUntil = '';
let paidUntilIso = this.props.user.paidUntil;
@@ -49,7 +46,7 @@ export default class Settings extends Component {
<Header hidden={ true } style={ 'defaultDark' } />
<Container>
<div className="settings">
- <button className="settings__close" onClick={ ::this.onClose } />
+ <button className="settings__close" onClick={ this.onClose } />
<div className="settings__container">
<div className="settings__header">
<h2 className="settings__title">Settings</h2>
@@ -82,7 +79,7 @@ export default class Settings extends Component {
<div className="settings__cell">
<div className="settings__cell-label">Auto-connect</div>
<div className="settings__cell-value">
- <Switch onChange={ ::this.onAutoSecure } isOn={ this.props.settings.autoSecure } />
+ <Switch onChange={ this.onAutoSecure } isOn={ this.props.settings.autoSecure } />
</div>
</div>
<div className="settings__cell-footer">
diff --git a/app/components/Switch.js b/app/components/Switch.js
index 4cb0ea34c3..46baeb8b22 100644
--- a/app/components/Switch.js
+++ b/app/components/Switch.js
@@ -1,27 +1,32 @@
+// @flow
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
+
+import type { Point2d } from '../types';
const CLICK_TIMEOUT = 1000;
const MOVE_THRESHOLD = 10;
export default class Switch extends Component {
+ props: {
+ isOn: boolean;
+ onChange: ?((isOn: boolean) => void);
+ }
- static propTypes = {
- isOn: PropTypes.bool,
- onChange: PropTypes.func
+ defaultProps = {
+ isOn: false
}
- constructor(props) {
- super(props);
- this.state = {
- isTracking: false,
- ignoreChange: false,
- initialPos: null,
- startTime: null
- };
+ ref: ?HTMLInputElement;
+ onRef = (e: HTMLInputElement) => this.ref = e;
+
+ state = {
+ isTracking: false,
+ ignoreChange: false,
+ initialPos: (null: ?Point2d),
+ startTime: (null: ?number)
}
- handleMouseDown(e) {
+ handleMouseDown = (e: MouseEvent) => {
const { pageX: x, pageY: y } = e;
this.setState({
isTracking: true,
@@ -30,14 +35,19 @@ export default class Switch extends Component {
});
}
- handleMouseMove(e) {
- if(!this.state.isTracking) { return; }
+ handleMouseMove = (e: MouseEvent) => {
+ if(!this.state.isTracking) {
+ return;
+ }
+ const inputElement = this.ref;
const { x: x0 } = this.state.initialPos;
const { pageX: x, pageY: y } = e;
const dx = Math.abs(x0 - x);
- if(dx < MOVE_THRESHOLD) { return; }
+ if(dx < MOVE_THRESHOLD) {
+ return;
+ }
const isOn = !!this.props.isOn;
let nextOn = isOn;
@@ -53,12 +63,16 @@ export default class Switch extends Component {
initialPos: { x, y },
ignoreChange: true
});
- this.refs.input.checked = nextOn;
+
+ if(inputElement) {
+ inputElement.checked = nextOn;
+ }
+
this.notify(nextOn);
}
}
- handleMouseUp() {
+ handleMouseUp = () => {
if(this.state.isTracking) {
this.setState({
isTracking: false,
@@ -67,8 +81,19 @@ export default class Switch extends Component {
}
}
- handleChange(e) {
- const dt = e.timeStamp - this.state.startTime;
+ handleChange = (e: Event) => {
+ const startTime = this.state.startTime;
+ const eventTarget = e.target;
+
+ if(typeof(startTime) !== 'number') {
+ throw new Error('startTime must be a number.');
+ }
+
+ if(!(eventTarget instanceof HTMLInputElement)) {
+ throw new Error('e.target must be an instance of HTMLInputElement.');
+ }
+
+ const dt = e.timeStamp - startTime;
if(this.state.ignoreChange) {
this.setState({ ignoreChange: false });
@@ -76,30 +101,32 @@ export default class Switch extends Component {
} else if(dt > CLICK_TIMEOUT) {
e.preventDefault();
} else {
- this.notify(e.target.checked);
+ this.notify(eventTarget.checked);
}
}
- notify(isOn) {
- if(this.props.onChange) {
- this.props.onChange(isOn);
+ notify(isOn: boolean) {
+ const onChange = this.props.onChange;
+ if(onChange) {
+ onChange(isOn);
}
}
componentDidMount() {
- document.addEventListener('mousemove', ::this.handleMouseMove);
- document.addEventListener('mouseup', ::this.handleMouseUp);
+ document.addEventListener('mousemove', this.handleMouseMove);
+ document.addEventListener('mouseup', this.handleMouseUp);
}
componentWillUnmount() {
- document.removeEventListener('mousemove', ::this.handleMouseMove);
- document.removeEventListener('mouseup', ::this.handleMouseUp);
+ document.removeEventListener('mousemove', this.handleMouseMove);
+ document.removeEventListener('mouseup', this.handleMouseUp);
}
- render() {
+ render(): React.Element<*> {
return (
- <input type="checkbox" ref="input" className="switch" checked={ this.props.isOn }
- onMouseDown={ ::this.handleMouseDown } onChange={ ::this.handleChange } />
+ <input type="checkbox" ref={ this.onRef } className="switch" checked={ this.props.isOn }
+ onMouseDown={ this.handleMouseDown }
+ onChange={ this.handleChange } />
);
}
}
diff --git a/app/components/WindowChrome.js b/app/components/WindowChrome.js
index 16bdb70e28..141c99fdb4 100644
--- a/app/components/WindowChrome.js
+++ b/app/components/WindowChrome.js
@@ -1,20 +1,10 @@
+// @flow
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-/**
- * A component used to chip out arrow in the app header using CSS mask
- *
- * @export
- * @class WindowChrome
- * @extends {Component}
- */
export default class WindowChrome extends Component {
- static propTypes = {
- children: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.element),
- PropTypes.element,
- ])
- };
+ props: {
+ children: Array<React.Element<*>> | React.Element<*>
+ }
render() {
const chromeClass = ['window-chrome', 'window-chrome--' + process.platform];