diff options
author | Stefan Malzner <stefan@adlk.io> | 2019-02-01 10:35:18 +0100 |
---|---|---|
committer | Stefan Malzner <stefan@adlk.io> | 2019-02-01 10:35:18 +0100 |
commit | da92cd426cbf350313945e3459f96638a79bd44e (patch) | |
tree | 931d18bbf62854366ccf7021d6206de7e0c42636 /src | |
parent | Update stale.yml (diff) | |
parent | b23 (diff) | |
download | ferdium-app-da92cd426cbf350313945e3459f96638a79bd44e.tar.gz ferdium-app-da92cd426cbf350313945e3459f96638a79bd44e.tar.zst ferdium-app-da92cd426cbf350313945e3459f96638a79bd44e.zip |
Merge branch 'develop'v5.0.0-beta.23
Diffstat (limited to 'src')
-rw-r--r-- | src/app.js | 1 | ||||
-rw-r--r-- | src/components/layout/AppLayout.js | 2 | ||||
-rw-r--r-- | src/components/ui/Modal/index.js | 59 | ||||
-rw-r--r-- | src/components/ui/Modal/styles.js | 32 | ||||
-rw-r--r-- | src/containers/settings/AccountScreen.js | 8 | ||||
-rw-r--r-- | src/electron/ipc-api/settings.js | 1 | ||||
-rw-r--r-- | src/features/basicAuth/Component.js | 102 | ||||
-rw-r--r-- | src/features/basicAuth/Form.js | 17 | ||||
-rw-r--r-- | src/features/basicAuth/index.js | 68 | ||||
-rw-r--r-- | src/features/basicAuth/mainIpcHandler.js | 9 | ||||
-rw-r--r-- | src/features/basicAuth/styles.js | 12 | ||||
-rw-r--r-- | src/features/delayApp/Component.js | 14 | ||||
-rw-r--r-- | src/features/delayApp/index.js | 23 | ||||
-rw-r--r-- | src/helpers/password-helpers.js | 4 | ||||
-rw-r--r-- | src/index.js | 123 | ||||
-rw-r--r-- | src/lib/analytics.js | 3 | ||||
-rw-r--r-- | src/models/Service.js | 22 | ||||
-rw-r--r-- | src/stores/FeaturesStore.js | 11 | ||||
-rw-r--r-- | src/stores/ServicesStore.js | 16 | ||||
-rw-r--r-- | src/theme/dark/index.js | 3 | ||||
-rw-r--r-- | src/theme/default/index.js | 3 | ||||
-rw-r--r-- | src/webview/contextMenu.js | 2 | ||||
-rw-r--r-- | src/webview/recipe.js | 17 |
23 files changed, 486 insertions, 66 deletions
diff --git a/src/app.js b/src/app.js index 831dd93ce..6660feb46 100644 --- a/src/app.js +++ b/src/app.js | |||
@@ -62,6 +62,7 @@ window.addEventListener('load', () => { | |||
62 | menu, | 62 | menu, |
63 | touchBar, | 63 | touchBar, |
64 | analytics, | 64 | analytics, |
65 | features: {}, | ||
65 | render() { | 66 | render() { |
66 | const preparedApp = ( | 67 | const preparedApp = ( |
67 | <Provider stores={stores} actions={actions}> | 68 | <Provider stores={stores} actions={actions}> |
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js index dbe0bb4b6..28eaa7fdc 100644 --- a/src/components/layout/AppLayout.js +++ b/src/components/layout/AppLayout.js | |||
@@ -6,6 +6,7 @@ import { TitleBar } from 'electron-react-titlebar'; | |||
6 | 6 | ||
7 | import InfoBar from '../ui/InfoBar'; | 7 | import InfoBar from '../ui/InfoBar'; |
8 | import { Component as DelayApp } from '../../features/delayApp'; | 8 | import { Component as DelayApp } from '../../features/delayApp'; |
9 | import { Component as BasicAuth } from '../../features/basicAuth'; | ||
9 | import ErrorBoundary from '../util/ErrorBoundary'; | 10 | import ErrorBoundary from '../util/ErrorBoundary'; |
10 | 11 | ||
11 | import globalMessages from '../../i18n/globalMessages'; | 12 | import globalMessages from '../../i18n/globalMessages'; |
@@ -161,6 +162,7 @@ export default @observer class AppLayout extends Component { | |||
161 | </InfoBar> | 162 | </InfoBar> |
162 | )} | 163 | )} |
163 | {isDelayAppScreenVisible && (<DelayApp />)} | 164 | {isDelayAppScreenVisible && (<DelayApp />)} |
165 | <BasicAuth /> | ||
164 | {services} | 166 | {services} |
165 | </div> | 167 | </div> |
166 | </div> | 168 | </div> |
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js new file mode 100644 index 000000000..d84e4c713 --- /dev/null +++ b/src/components/ui/Modal/index.js | |||
@@ -0,0 +1,59 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import ReactModal from 'react-modal'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import classnames from 'classnames'; | ||
5 | import injectCSS from 'react-jss'; | ||
6 | |||
7 | import styles from './styles'; | ||
8 | |||
9 | export default @injectCSS(styles) class Modal extends Component { | ||
10 | static propTypes = { | ||
11 | children: PropTypes.node.isRequired, | ||
12 | className: PropTypes.string, | ||
13 | classes: PropTypes.object.isRequired, | ||
14 | isOpen: PropTypes.bool.isRequired, | ||
15 | portal: PropTypes.string, | ||
16 | close: PropTypes.func.isRequired, | ||
17 | } | ||
18 | |||
19 | static defaultProps = { | ||
20 | className: null, | ||
21 | portal: 'modal-portal', | ||
22 | } | ||
23 | |||
24 | render() { | ||
25 | const { | ||
26 | children, | ||
27 | className, | ||
28 | classes, | ||
29 | isOpen, | ||
30 | portal, | ||
31 | close, | ||
32 | } = this.props; | ||
33 | |||
34 | return ( | ||
35 | <ReactModal | ||
36 | isOpen={isOpen} | ||
37 | className={classnames({ | ||
38 | [`${classes.modal}`]: true, | ||
39 | [`${className}`]: className, | ||
40 | })} | ||
41 | portalClassName={classes.component} | ||
42 | overlayClassName={classes.overlay} | ||
43 | portal={portal} | ||
44 | onRequestClose={close} | ||
45 | > | ||
46 | {/* <button | ||
47 | type="button" | ||
48 | className={classnames({ | ||
49 | [`${classes.close}`]: true, | ||
50 | 'mdi mdi-close': true, | ||
51 | })} | ||
52 | /> */} | ||
53 | <div className={classes.content}> | ||
54 | {children} | ||
55 | </div> | ||
56 | </ReactModal> | ||
57 | ); | ||
58 | } | ||
59 | } | ||
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.js new file mode 100644 index 000000000..56fecbf55 --- /dev/null +++ b/src/components/ui/Modal/styles.js | |||
@@ -0,0 +1,32 @@ | |||
1 | export default theme => ({ | ||
2 | component: { | ||
3 | zIndex: 500, | ||
4 | position: 'absolute', | ||
5 | }, | ||
6 | overlay: { | ||
7 | background: theme.colorModalOverlayBackground, | ||
8 | position: 'fixed', | ||
9 | top: 0, | ||
10 | left: 0, | ||
11 | right: 0, | ||
12 | bottom: 0, | ||
13 | display: 'flex', | ||
14 | }, | ||
15 | modal: { | ||
16 | background: '#FFF', | ||
17 | maxWidth: '90%', | ||
18 | height: 'auto', | ||
19 | margin: 'auto auto', | ||
20 | borderRadius: 6, | ||
21 | boxShadow: '0px 13px 40px 0px rgba(0,0,0,0.2)', | ||
22 | position: 'relative', | ||
23 | }, | ||
24 | content: { | ||
25 | padding: 20, | ||
26 | }, | ||
27 | close: { | ||
28 | position: 'absolute', | ||
29 | top: 0, | ||
30 | right: 0, | ||
31 | }, | ||
32 | }); | ||
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js index 019b3d7d6..d681d5226 100644 --- a/src/containers/settings/AccountScreen.js +++ b/src/containers/settings/AccountScreen.js | |||
@@ -14,6 +14,14 @@ import ErrorBoundary from '../../components/util/ErrorBoundary'; | |||
14 | const { BrowserWindow } = remote; | 14 | const { BrowserWindow } = remote; |
15 | 15 | ||
16 | export default @inject('stores', 'actions') @observer class AccountScreen extends Component { | 16 | export default @inject('stores', 'actions') @observer class AccountScreen extends Component { |
17 | componentWillMount() { | ||
18 | const { | ||
19 | user, | ||
20 | } = this.props.stores; | ||
21 | |||
22 | user.getUserInfoRequest.invalidate({ immediately: true }); | ||
23 | } | ||
24 | |||
17 | componentDidMount() { | 25 | componentDidMount() { |
18 | gaPage('Settings/Account Dashboard'); | 26 | gaPage('Settings/Account Dashboard'); |
19 | } | 27 | } |
diff --git a/src/electron/ipc-api/settings.js b/src/electron/ipc-api/settings.js index ce006bb92..b651db306 100644 --- a/src/electron/ipc-api/settings.js +++ b/src/electron/ipc-api/settings.js | |||
@@ -2,7 +2,6 @@ import { ipcMain } from 'electron'; | |||
2 | 2 | ||
3 | export default (params) => { | 3 | export default (params) => { |
4 | ipcMain.on('getAppSettings', (event, type) => { | 4 | ipcMain.on('getAppSettings', (event, type) => { |
5 | console.log('getAppSettings', type, params.settings[type].all); | ||
6 | params.mainWindow.webContents.send('appSettings', { | 5 | params.mainWindow.webContents.send('appSettings', { |
7 | type, | 6 | type, |
8 | data: params.settings[type].all, | 7 | data: params.settings[type].all, |
diff --git a/src/features/basicAuth/Component.js b/src/features/basicAuth/Component.js new file mode 100644 index 000000000..13395fb40 --- /dev/null +++ b/src/features/basicAuth/Component.js | |||
@@ -0,0 +1,102 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import injectSheet from 'react-jss'; | ||
4 | import { observer } from 'mobx-react'; | ||
5 | import classnames from 'classnames'; | ||
6 | |||
7 | import Modal from '../../components/ui/Modal'; | ||
8 | import Input from '../../components/ui/Input'; | ||
9 | import Button from '../../components/ui/Button'; | ||
10 | |||
11 | import { | ||
12 | state, | ||
13 | resetState, | ||
14 | sendCredentials, | ||
15 | cancelLogin, | ||
16 | } from '.'; | ||
17 | import Form from './Form'; | ||
18 | |||
19 | import styles from './styles'; | ||
20 | |||
21 | export default @injectSheet(styles) @observer class BasicAuthModal extends Component { | ||
22 | static propTypes = { | ||
23 | classes: PropTypes.object.isRequired, | ||
24 | } | ||
25 | |||
26 | submit(e) { | ||
27 | e.preventDefault(); | ||
28 | |||
29 | const values = Form.values(); | ||
30 | console.log('form submit', values); | ||
31 | |||
32 | sendCredentials(values.user, values.password); | ||
33 | resetState(); | ||
34 | } | ||
35 | |||
36 | cancel() { | ||
37 | cancelLogin(); | ||
38 | this.close(); | ||
39 | } | ||
40 | |||
41 | close() { | ||
42 | resetState(); | ||
43 | state.isModalVisible = false; | ||
44 | } | ||
45 | |||
46 | render() { | ||
47 | const { | ||
48 | classes, | ||
49 | } = this.props; | ||
50 | |||
51 | const { | ||
52 | isModalVisible, | ||
53 | authInfo, | ||
54 | } = state; | ||
55 | |||
56 | if (!authInfo) { | ||
57 | return null; | ||
58 | } | ||
59 | |||
60 | return ( | ||
61 | <Modal | ||
62 | isOpen={isModalVisible} | ||
63 | className={classes.modal} | ||
64 | close={this.cancel.bind(this)} | ||
65 | > | ||
66 | <h1>Sign in</h1> | ||
67 | <p> | ||
68 | http | ||
69 | {authInfo.port === 443 && 's'} | ||
70 | :// | ||
71 | {authInfo.host} | ||
72 | </p> | ||
73 | <form | ||
74 | onSubmit={this.submit.bind(this)} | ||
75 | className={classnames('franz-form', classes.form)} | ||
76 | > | ||
77 | <Input | ||
78 | field={Form.$('user')} | ||
79 | showLabel={false} | ||
80 | /> | ||
81 | <Input | ||
82 | field={Form.$('password')} | ||
83 | showLabel={false} | ||
84 | showPasswordToggle | ||
85 | /> | ||
86 | <div className={classes.buttons}> | ||
87 | <Button | ||
88 | type="button" | ||
89 | label="Cancel" | ||
90 | buttonType="secondary" | ||
91 | onClick={this.cancel.bind(this)} | ||
92 | /> | ||
93 | <Button | ||
94 | type="submit" | ||
95 | label="Sign In" | ||
96 | /> | ||
97 | </div> | ||
98 | </form> | ||
99 | </Modal> | ||
100 | ); | ||
101 | } | ||
102 | } | ||
diff --git a/src/features/basicAuth/Form.js b/src/features/basicAuth/Form.js new file mode 100644 index 000000000..95721d0e9 --- /dev/null +++ b/src/features/basicAuth/Form.js | |||
@@ -0,0 +1,17 @@ | |||
1 | import Form from '../../lib/Form'; | ||
2 | |||
3 | export default new Form({ | ||
4 | fields: { | ||
5 | user: { | ||
6 | label: 'user', | ||
7 | placeholder: 'Username', | ||
8 | value: '', | ||
9 | }, | ||
10 | password: { | ||
11 | label: 'Password', | ||
12 | placeholder: 'Password', | ||
13 | value: '', | ||
14 | type: 'password', | ||
15 | }, | ||
16 | }, | ||
17 | }); | ||
diff --git a/src/features/basicAuth/index.js b/src/features/basicAuth/index.js new file mode 100644 index 000000000..03269582c --- /dev/null +++ b/src/features/basicAuth/index.js | |||
@@ -0,0 +1,68 @@ | |||
1 | import { ipcRenderer } from 'electron'; | ||
2 | import { observable } from 'mobx'; | ||
3 | |||
4 | import BasicAuthComponent from './Component'; | ||
5 | |||
6 | const debug = require('debug')('Franz:feature:basicAuth'); | ||
7 | |||
8 | const defaultState = { | ||
9 | isModalVisible: false, | ||
10 | service: null, | ||
11 | authInfo: null, | ||
12 | }; | ||
13 | |||
14 | export const state = observable(defaultState); | ||
15 | |||
16 | export function resetState() { | ||
17 | Object.assign(state, defaultState); | ||
18 | console.log('reset state', state); | ||
19 | } | ||
20 | |||
21 | export default function initialize() { | ||
22 | debug('Initialize basicAuth feature'); | ||
23 | |||
24 | window.franz.features.basicAuth = { | ||
25 | state, | ||
26 | }; | ||
27 | |||
28 | ipcRenderer.on('feature:basic-auth-request', (e, data) => { | ||
29 | debug(e, data); | ||
30 | // state.serviceId = data.serviceId; | ||
31 | state.authInfo = data.authInfo; | ||
32 | state.isModalVisible = true; | ||
33 | }); | ||
34 | |||
35 | // autorun(() => { | ||
36 | // // if (state.serviceId) { | ||
37 | // // const service = stores.services.one(state.serviceId); | ||
38 | // // if (service) { | ||
39 | // // state.service = service; | ||
40 | // // } | ||
41 | // // } | ||
42 | // }); | ||
43 | } | ||
44 | |||
45 | export function mainIpcHandler(mainWindow, authInfo) { | ||
46 | debug('Sending basic auth call', authInfo); | ||
47 | |||
48 | mainWindow.webContents.send('feature:basic-auth-request', { | ||
49 | authInfo, | ||
50 | }); | ||
51 | } | ||
52 | |||
53 | export function sendCredentials(user, password) { | ||
54 | debug('Sending credentials to main', user, password); | ||
55 | |||
56 | ipcRenderer.send('feature-basic-auth-credentials', { | ||
57 | user, | ||
58 | password, | ||
59 | }); | ||
60 | } | ||
61 | |||
62 | export function cancelLogin() { | ||
63 | debug('Cancel basic auth event'); | ||
64 | |||
65 | ipcRenderer.send('feature-basic-auth-cancel'); | ||
66 | } | ||
67 | |||
68 | export const Component = BasicAuthComponent; | ||
diff --git a/src/features/basicAuth/mainIpcHandler.js b/src/features/basicAuth/mainIpcHandler.js new file mode 100644 index 000000000..87ac0b6df --- /dev/null +++ b/src/features/basicAuth/mainIpcHandler.js | |||
@@ -0,0 +1,9 @@ | |||
1 | const debug = require('debug')('Franz:feature:basicAuth:main'); | ||
2 | |||
3 | export default function mainIpcHandler(mainWindow, authInfo) { | ||
4 | debug('Sending basic auth call', authInfo); | ||
5 | |||
6 | mainWindow.webContents.send('feature:basic-auth', { | ||
7 | authInfo, | ||
8 | }); | ||
9 | } | ||
diff --git a/src/features/basicAuth/styles.js b/src/features/basicAuth/styles.js new file mode 100644 index 000000000..6bdaf9a6e --- /dev/null +++ b/src/features/basicAuth/styles.js | |||
@@ -0,0 +1,12 @@ | |||
1 | export default { | ||
2 | modal: { | ||
3 | width: 300, | ||
4 | }, | ||
5 | buttons: { | ||
6 | display: 'flex', | ||
7 | justifyContent: 'space-between', | ||
8 | }, | ||
9 | form: { | ||
10 | marginTop: 15, | ||
11 | }, | ||
12 | }; | ||
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js index 6e0532c9a..ff84510e8 100644 --- a/src/features/delayApp/Component.js +++ b/src/features/delayApp/Component.js | |||
@@ -4,6 +4,8 @@ import { inject, observer } from 'mobx-react'; | |||
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import injectSheet from 'react-jss'; | 5 | import injectSheet from 'react-jss'; |
6 | 6 | ||
7 | import { gaEvent } from '../../lib/analytics'; | ||
8 | |||
7 | import Button from '../../components/ui/Button'; | 9 | import Button from '../../components/ui/Button'; |
8 | 10 | ||
9 | import { config } from '.'; | 11 | import { config } from '.'; |
@@ -59,8 +61,16 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp | |||
59 | clearInterval(this.countdownInterval); | 61 | clearInterval(this.countdownInterval); |
60 | } | 62 | } |
61 | 63 | ||
64 | handleCTAClick() { | ||
65 | const { actions } = this.props; | ||
66 | |||
67 | actions.ui.openSettings({ path: 'user' }); | ||
68 | |||
69 | gaEvent('DelayApp', 'subscribe_click', 'Delay App Feature'); | ||
70 | } | ||
71 | |||
62 | render() { | 72 | render() { |
63 | const { classes, actions } = this.props; | 73 | const { classes } = this.props; |
64 | const { intl } = this.context; | 74 | const { intl } = this.context; |
65 | 75 | ||
66 | return ( | 76 | return ( |
@@ -70,7 +80,7 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp | |||
70 | label={intl.formatMessage(messages.action)} | 80 | label={intl.formatMessage(messages.action)} |
71 | className={classes.button} | 81 | className={classes.button} |
72 | buttonType="inverted" | 82 | buttonType="inverted" |
73 | onClick={() => actions.ui.openSettings({ path: 'user' })} | 83 | onClick={this.handleCTAClick.bind(this)} |
74 | /> | 84 | /> |
75 | <p className="footnote"> | 85 | <p className="footnote"> |
76 | {intl.formatMessage(messages.text, { | 86 | {intl.formatMessage(messages.text, { |
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js index d5c544b78..28aa50eb2 100644 --- a/src/features/delayApp/index.js +++ b/src/features/delayApp/index.js | |||
@@ -3,6 +3,7 @@ import moment from 'moment'; | |||
3 | import DelayAppComponent from './Component'; | 3 | import DelayAppComponent from './Component'; |
4 | 4 | ||
5 | import { DEFAULT_FEATURES_CONFIG } from '../../config'; | 5 | import { DEFAULT_FEATURES_CONFIG } from '../../config'; |
6 | import { gaEvent } from '../../lib/analytics'; | ||
6 | 7 | ||
7 | const debug = require('debug')('Franz:feature:delayApp'); | 8 | const debug = require('debug')('Franz:feature:delayApp'); |
8 | 9 | ||
@@ -22,19 +23,18 @@ function setVisibility(value) { | |||
22 | } | 23 | } |
23 | 24 | ||
24 | export default function init(stores) { | 25 | export default function init(stores) { |
25 | reaction( | 26 | debug('Initializing `delayApp` feature'); |
26 | () => stores.features.features.needToWaitToProceed, | ||
27 | (enabled, r) => { | ||
28 | if (enabled) { | ||
29 | debug('Initializing `delayApp` feature'); | ||
30 | 27 | ||
31 | // Dispose the reaction to run this only once | 28 | let shownAfterLaunch = false; |
32 | r.dispose(); | 29 | let timeLastDelay = moment(); |
33 | 30 | ||
34 | const { needToWaitToProceedConfig: globalConfig } = stores.features.features; | 31 | reaction( |
32 | () => stores.features.features.needToWaitToProceed && !stores.user.data.isPremium, | ||
33 | (isEnabled) => { | ||
34 | if (isEnabled) { | ||
35 | debug('Enabling `delayApp` feature'); | ||
35 | 36 | ||
36 | let shownAfterLaunch = false; | 37 | const { needToWaitToProceedConfig: globalConfig } = stores.features.features; |
37 | let timeLastDelay = moment(); | ||
38 | 38 | ||
39 | config.delayOffset = globalConfig.delayOffset !== undefined ? globalConfig.delayOffset : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset; | 39 | config.delayOffset = globalConfig.delayOffset !== undefined ? globalConfig.delayOffset : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset; |
40 | config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait; | 40 | config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait; |
@@ -50,6 +50,7 @@ export default function init(stores) { | |||
50 | debug(`App will be delayed for ${config.delayDuration / 1000}s`); | 50 | debug(`App will be delayed for ${config.delayDuration / 1000}s`); |
51 | 51 | ||
52 | setVisibility(true); | 52 | setVisibility(true); |
53 | gaEvent('delayApp', 'show', 'Delay App Feature'); | ||
53 | 54 | ||
54 | timeLastDelay = moment(); | 55 | timeLastDelay = moment(); |
55 | shownAfterLaunch = true; | 56 | shownAfterLaunch = true; |
@@ -61,6 +62,8 @@ export default function init(stores) { | |||
61 | }, DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait + 1000); // timer needs to be able to hit 0 | 62 | }, DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait + 1000); // timer needs to be able to hit 0 |
62 | } | 63 | } |
63 | }); | 64 | }); |
65 | } else { | ||
66 | setVisibility(false); | ||
64 | } | 67 | } |
65 | }, | 68 | }, |
66 | ); | 69 | ); |
diff --git a/src/helpers/password-helpers.js b/src/helpers/password-helpers.js index 7aacaa4d0..cf461e4f7 100644 --- a/src/helpers/password-helpers.js +++ b/src/helpers/password-helpers.js | |||
@@ -1,7 +1,7 @@ | |||
1 | import { SHA256 } from 'jshashes'; | 1 | import crypto from 'crypto'; |
2 | 2 | ||
3 | export function hash(password) { | 3 | export function hash(password) { |
4 | return new SHA256().b64(password); | 4 | return crypto.createHash('sha256').update(password).digest('base64'); |
5 | } | 5 | } |
6 | 6 | ||
7 | export function scorePassword(password) { | 7 | export function scorePassword(password) { |
diff --git a/src/index.js b/src/index.js index 830166dcf..f34df8c17 100644 --- a/src/index.js +++ b/src/index.js | |||
@@ -1,5 +1,8 @@ | |||
1 | import { | 1 | import { |
2 | app, BrowserWindow, shell, ipcMain, | 2 | app, |
3 | BrowserWindow, | ||
4 | shell, | ||
5 | ipcMain, | ||
3 | } from 'electron'; | 6 | } from 'electron'; |
4 | 7 | ||
5 | import fs from 'fs-extra'; | 8 | import fs from 'fs-extra'; |
@@ -7,9 +10,14 @@ import path from 'path'; | |||
7 | import windowStateKeeper from 'electron-window-state'; | 10 | import windowStateKeeper from 'electron-window-state'; |
8 | 11 | ||
9 | import { | 12 | import { |
10 | isDevMode, isMac, isWindows, isLinux, | 13 | isDevMode, |
14 | isMac, | ||
15 | isWindows, | ||
16 | isLinux, | ||
11 | } from './environment'; | 17 | } from './environment'; |
12 | 18 | ||
19 | import { mainIpcHandler as basicAuthHandler } from './features/basicAuth'; | ||
20 | |||
13 | // DEV MODE: Save user data into FranzDev | 21 | // DEV MODE: Save user data into FranzDev |
14 | if (isDevMode) { | 22 | if (isDevMode) { |
15 | app.setPath('userData', path.join(app.getPath('appData'), 'FranzDev')); | 23 | app.setPath('userData', path.join(app.getPath('appData'), 'FranzDev')); |
@@ -46,35 +54,69 @@ if (isWindows) { | |||
46 | } | 54 | } |
47 | 55 | ||
48 | // Force single window | 56 | // Force single window |
49 | const isSecondInstance = app.makeSingleInstance((argv) => { | 57 | const gotTheLock = app.requestSingleInstanceLock(); |
50 | if (mainWindow) { | 58 | if (!gotTheLock) { |
51 | if (mainWindow.isMinimized()) mainWindow.restore(); | 59 | app.quit(); |
52 | mainWindow.focus(); | 60 | } else { |
61 | app.on('second-instance', (event, argv) => { | ||
62 | // Someone tried to run a second instance, we should focus our window. | ||
63 | if (mainWindow) { | ||
64 | if (mainWindow.isMinimized()) mainWindow.restore(); | ||
65 | mainWindow.focus(); | ||
53 | 66 | ||
54 | if (process.platform === 'win32') { | 67 | if (isWindows) { |
55 | // Keep only command line / deep linked arguments | 68 | // Keep only command line / deep linked arguments |
56 | const url = argv.slice(1); | 69 | const url = argv.slice(1); |
57 | 70 | ||
58 | if (url) { | 71 | if (url) { |
59 | handleDeepLink(mainWindow, url.toString()); | 72 | handleDeepLink(mainWindow, url.toString()); |
73 | } | ||
60 | } | 74 | } |
61 | } | ||
62 | } | ||
63 | 75 | ||
64 | if (argv.includes('--reset-window')) { | 76 | if (argv.includes('--reset-window')) { |
65 | // Needs to be delayed to not interfere with mainWindow.restore(); | 77 | // Needs to be delayed to not interfere with mainWindow.restore(); |
66 | setTimeout(() => { | 78 | setTimeout(() => { |
67 | debug('Resetting windows via Task'); | 79 | debug('Resetting windows via Task'); |
68 | mainWindow.setPosition(DEFAULT_WINDOW_OPTIONS.x + 100, DEFAULT_WINDOW_OPTIONS.y + 100); | 80 | mainWindow.setPosition(DEFAULT_WINDOW_OPTIONS.x + 100, DEFAULT_WINDOW_OPTIONS.y + 100); |
69 | mainWindow.setSize(DEFAULT_WINDOW_OPTIONS.width, DEFAULT_WINDOW_OPTIONS.height); | 81 | mainWindow.setSize(DEFAULT_WINDOW_OPTIONS.width, DEFAULT_WINDOW_OPTIONS.height); |
70 | }, 1); | 82 | }, 1); |
71 | } | 83 | } |
72 | }); | 84 | } |
85 | }); | ||
73 | 86 | ||
74 | if (isSecondInstance) { | 87 | // Create myWindow, load the rest of the app, etc... |
75 | console.log('An instance of Franz is already running. Exiting...'); | 88 | app.on('ready', () => { |
76 | app.exit(); | 89 | }); |
77 | } | 90 | } |
91 | // const isSecondInstance = app.makeSingleInstance((argv) => { | ||
92 | // if (mainWindow) { | ||
93 | // if (mainWindow.isMinimized()) mainWindow.restore(); | ||
94 | // mainWindow.focus(); | ||
95 | |||
96 | // if (process.platform === 'win32') { | ||
97 | // // Keep only command line / deep linked arguments | ||
98 | // const url = argv.slice(1); | ||
99 | |||
100 | // if (url) { | ||
101 | // handleDeepLink(mainWindow, url.toString()); | ||
102 | // } | ||
103 | // } | ||
104 | // } | ||
105 | |||
106 | // if (argv.includes('--reset-window')) { | ||
107 | // // Needs to be delayed to not interfere with mainWindow.restore(); | ||
108 | // setTimeout(() => { | ||
109 | // debug('Resetting windows via Task'); | ||
110 | // mainWindow.setPosition(DEFAULT_WINDOW_OPTIONS.x + 100, DEFAULT_WINDOW_OPTIONS.y + 100); | ||
111 | // mainWindow.setSize(DEFAULT_WINDOW_OPTIONS.width, DEFAULT_WINDOW_OPTIONS.height); | ||
112 | // }, 1); | ||
113 | // } | ||
114 | // }); | ||
115 | |||
116 | // if (isSecondInstance) { | ||
117 | // console.log('An instance of Franz is already running. Exiting...'); | ||
118 | // app.exit(); | ||
119 | // } | ||
78 | 120 | ||
79 | // Fix Unity indicator issue | 121 | // Fix Unity indicator issue |
80 | // https://github.com/electron/electron/issues/9046 | 122 | // https://github.com/electron/electron/issues/9046 |
@@ -119,6 +161,9 @@ const createWindow = () => { | |||
119 | titleBarStyle: isMac ? 'hidden' : '', | 161 | titleBarStyle: isMac ? 'hidden' : '', |
120 | frame: isLinux, | 162 | frame: isLinux, |
121 | backgroundColor: !settings.get('darkMode') ? '#3498db' : '#1E1E1E', | 163 | backgroundColor: !settings.get('darkMode') ? '#3498db' : '#1E1E1E', |
164 | webPreferences: { | ||
165 | nodeIntegration: true, | ||
166 | }, | ||
122 | }); | 167 | }); |
123 | 168 | ||
124 | // Initialize System Tray | 169 | // Initialize System Tray |
@@ -229,23 +274,43 @@ app.on('ready', () => { | |||
229 | }); | 274 | }); |
230 | 275 | ||
231 | // This is the worst possible implementation as the webview.webContents based callback doesn't work 🖕 | 276 | // This is the worst possible implementation as the webview.webContents based callback doesn't work 🖕 |
277 | // TODO: rewrite to handle multiple login calls | ||
278 | const noop = () => null; | ||
279 | let authCallback = noop; | ||
232 | app.on('login', (event, webContents, request, authInfo, callback) => { | 280 | app.on('login', (event, webContents, request, authInfo, callback) => { |
233 | event.preventDefault(); | 281 | authCallback = callback; |
234 | debug('browser login event', authInfo); | 282 | debug('browser login event', authInfo); |
283 | event.preventDefault(); | ||
235 | if (authInfo.isProxy && authInfo.scheme === 'basic') { | 284 | if (authInfo.isProxy && authInfo.scheme === 'basic') { |
236 | webContents.send('get-service-id'); | 285 | webContents.send('get-service-id'); |
237 | 286 | ||
238 | ipcMain.on('service-id', (e, id) => { | 287 | ipcMain.once('service-id', (e, id) => { |
239 | debug('Received service id', id); | 288 | debug('Received service id', id); |
240 | 289 | ||
241 | const ps = proxySettings.get(id); | 290 | const ps = proxySettings.get(id); |
242 | callback(ps.user, ps.password); | 291 | callback(ps.user, ps.password); |
243 | }); | 292 | }); |
244 | } else { | 293 | } else if (authInfo.scheme === 'basic') { |
245 | // TODO: implement basic auth | 294 | debug('basic auth handler', authInfo); |
295 | basicAuthHandler(mainWindow, authInfo); | ||
246 | } | 296 | } |
247 | }); | 297 | }); |
248 | 298 | ||
299 | // TODO: evaluate if we need to store the authCallback for every service | ||
300 | ipcMain.on('feature-basic-auth-credentials', (e, { user, password }) => { | ||
301 | debug('Received basic auth credentials', user, '********'); | ||
302 | |||
303 | authCallback(user, password); | ||
304 | authCallback = noop; | ||
305 | }); | ||
306 | |||
307 | ipcMain.on('feature-basic-auth-cancel', () => { | ||
308 | debug('Cancel basic auth'); | ||
309 | |||
310 | authCallback(null); | ||
311 | authCallback = noop; | ||
312 | }); | ||
313 | |||
249 | // Quit when all windows are closed. | 314 | // Quit when all windows are closed. |
250 | app.on('window-all-closed', () => { | 315 | app.on('window-all-closed', () => { |
251 | // On OS X it is common for applications and their menu bar | 316 | // On OS X it is common for applications and their menu bar |
diff --git a/src/lib/analytics.js b/src/lib/analytics.js index 8b9a44579..7044e5bb7 100644 --- a/src/lib/analytics.js +++ b/src/lib/analytics.js | |||
@@ -1,6 +1,5 @@ | |||
1 | import { remote } from 'electron'; | 1 | import { remote } from 'electron'; |
2 | import { GA_ID } from '../config'; | 2 | import { GA_ID } from '../config'; |
3 | // import { isDevMode } from '../environment'; | ||
4 | 3 | ||
5 | const debug = require('debug')('Franz:Analytics'); | 4 | const debug = require('debug')('Franz:Analytics'); |
6 | 5 | ||
@@ -36,7 +35,7 @@ export function gaPage(page) { | |||
36 | export function gaEvent(category, action, label) { | 35 | export function gaEvent(category, action, label) { |
37 | ga('send', 'event', category, action, label); | 36 | ga('send', 'event', category, action, label); |
38 | 37 | ||
39 | debug('GA track page', category, action); | 38 | debug('GA track event', category, action); |
40 | } | 39 | } |
41 | 40 | ||
42 | setTimeout(() => { | 41 | setTimeout(() => { |
diff --git a/src/models/Service.js b/src/models/Service.js index cb66676d4..eee8df8ca 100644 --- a/src/models/Service.js +++ b/src/models/Service.js | |||
@@ -114,6 +114,13 @@ export default class Service { | |||
114 | }); | 114 | }); |
115 | } | 115 | } |
116 | 116 | ||
117 | @computed get shareWithWebview() { | ||
118 | return { | ||
119 | spellcheckerLanguage: this.spellcheckerLanguage, | ||
120 | isDarkModeEnabled: this.isDarkModeEnabled, | ||
121 | }; | ||
122 | } | ||
123 | |||
117 | @computed get url() { | 124 | @computed get url() { |
118 | if (this.recipe.hasCustomUrl && this.customUrl) { | 125 | if (this.recipe.hasCustomUrl && this.customUrl) { |
119 | let url; | 126 | let url; |
@@ -162,14 +169,14 @@ export default class Service { | |||
162 | return userAgent; | 169 | return userAgent; |
163 | } | 170 | } |
164 | 171 | ||
165 | initializeWebViewEvents(store) { | 172 | initializeWebViewEvents({ handleIPCMessage, openWindow }) { |
166 | this.webview.addEventListener('ipc-message', e => store.actions.service.handleIPCMessage({ | 173 | this.webview.addEventListener('ipc-message', e => handleIPCMessage({ |
167 | serviceId: this.id, | 174 | serviceId: this.id, |
168 | channel: e.channel, | 175 | channel: e.channel, |
169 | args: e.args, | 176 | args: e.args, |
170 | })); | 177 | })); |
171 | 178 | ||
172 | this.webview.addEventListener('new-window', (event, url, frameName, options) => store.actions.service.openWindow({ | 179 | this.webview.addEventListener('new-window', (event, url, frameName, options) => openWindow({ |
173 | event, | 180 | event, |
174 | url, | 181 | url, |
175 | frameName, | 182 | frameName, |
@@ -182,17 +189,20 @@ export default class Service { | |||
182 | this.isError = false; | 189 | this.isError = false; |
183 | }); | 190 | }); |
184 | 191 | ||
185 | this.webview.addEventListener('did-frame-finish-load', () => { | 192 | const didLoad = () => { |
186 | this.isLoading = false; | 193 | this.isLoading = false; |
187 | 194 | ||
188 | if (!this.isError) { | 195 | if (!this.isError) { |
189 | this.isFirstLoad = false; | 196 | this.isFirstLoad = false; |
190 | } | 197 | } |
191 | }); | 198 | }; |
199 | |||
200 | this.webview.addEventListener('did-frame-finish-load', didLoad.bind(this)); | ||
201 | this.webview.addEventListener('did-navigate', didLoad.bind(this)); | ||
192 | 202 | ||
193 | this.webview.addEventListener('did-fail-load', (event) => { | 203 | this.webview.addEventListener('did-fail-load', (event) => { |
194 | debug('Service failed to load', this.name, event); | 204 | debug('Service failed to load', this.name, event); |
195 | if (event.isMainFrame) { | 205 | if (event.isMainFrame && event.errorCode !== -27 && event.errorCode !== -3) { |
196 | this.isError = true; | 206 | this.isError = true; |
197 | this.errorMessage = event.errorDescription; | 207 | this.errorMessage = event.errorDescription; |
198 | this.isLoading = false; | 208 | this.isLoading = false; |
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index 2a0713b6f..0adee6adf 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js | |||
@@ -1,4 +1,4 @@ | |||
1 | import { computed, observable } from 'mobx'; | 1 | import { computed, observable, reaction } from 'mobx'; |
2 | 2 | ||
3 | import Store from './lib/Store'; | 3 | import Store from './lib/Store'; |
4 | import CachedRequest from './lib/CachedRequest'; | 4 | import CachedRequest from './lib/CachedRequest'; |
@@ -6,6 +6,7 @@ import CachedRequest from './lib/CachedRequest'; | |||
6 | import delayApp from '../features/delayApp'; | 6 | import delayApp from '../features/delayApp'; |
7 | import spellchecker from '../features/spellchecker'; | 7 | import spellchecker from '../features/spellchecker'; |
8 | import serviceProxy from '../features/serviceProxy'; | 8 | import serviceProxy from '../features/serviceProxy'; |
9 | import basicAuth from '../features/basicAuth'; | ||
9 | 10 | ||
10 | import { DEFAULT_FEATURES_CONFIG } from '../config'; | 11 | import { DEFAULT_FEATURES_CONFIG } from '../config'; |
11 | 12 | ||
@@ -21,6 +22,13 @@ export default class FeaturesStore extends Store { | |||
21 | 22 | ||
22 | await this.featuresRequest._promise; | 23 | await this.featuresRequest._promise; |
23 | setTimeout(this._enableFeatures.bind(this), 1); | 24 | setTimeout(this._enableFeatures.bind(this), 1); |
25 | |||
26 | // single key reaction | ||
27 | reaction(() => this.stores.user.data.isPremium, () => { | ||
28 | if (this.stores.user.isLoggedIn) { | ||
29 | this.featuresRequest.invalidate({ immediately: true }); | ||
30 | } | ||
31 | }); | ||
24 | } | 32 | } |
25 | 33 | ||
26 | @computed get anonymousFeatures() { | 34 | @computed get anonymousFeatures() { |
@@ -47,5 +55,6 @@ export default class FeaturesStore extends Store { | |||
47 | delayApp(this.stores, this.actions); | 55 | delayApp(this.stores, this.actions); |
48 | spellchecker(this.stores, this.actions); | 56 | spellchecker(this.stores, this.actions); |
49 | serviceProxy(this.stores, this.actions); | 57 | serviceProxy(this.stores, this.actions); |
58 | basicAuth(this.stores, this.actions); | ||
50 | } | 59 | } |
51 | } | 60 | } |
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index a618da547..f79197c38 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js | |||
@@ -1,5 +1,8 @@ | |||
1 | import { | 1 | import { |
2 | action, reaction, computed, observable, | 2 | action, |
3 | reaction, | ||
4 | computed, | ||
5 | observable, | ||
3 | } from 'mobx'; | 6 | } from 'mobx'; |
4 | import { debounce, remove } from 'lodash'; | 7 | import { debounce, remove } from 'lodash'; |
5 | 8 | ||
@@ -324,7 +327,11 @@ export default class ServicesStore extends Store { | |||
324 | service.webview = webview; | 327 | service.webview = webview; |
325 | 328 | ||
326 | if (!service.isAttached) { | 329 | if (!service.isAttached) { |
327 | service.initializeWebViewEvents(this); | 330 | debug('Webview is not attached, initializing'); |
331 | service.initializeWebViewEvents({ | ||
332 | handleIPCMessage: this.actions.service.handleIPCMessage, | ||
333 | openWindow: this.actions.service.openWindow, | ||
334 | }); | ||
328 | service.initializeWebViewListener(); | 335 | service.initializeWebViewListener(); |
329 | } | 336 | } |
330 | 337 | ||
@@ -659,14 +666,15 @@ export default class ServicesStore extends Store { | |||
659 | const service = this.one(serviceId); | 666 | const service = this.one(serviceId); |
660 | 667 | ||
661 | if (service.webview) { | 668 | if (service.webview) { |
662 | service.webview.send('initialize-recipe', service); | 669 | debug('Initialize recipe', service.recipe.id, service.name); |
670 | service.webview.send('initialize-recipe', service.shareWithWebview, service.recipe); | ||
663 | } | 671 | } |
664 | } | 672 | } |
665 | 673 | ||
666 | _initRecipePolling(serviceId) { | 674 | _initRecipePolling(serviceId) { |
667 | const service = this.one(serviceId); | 675 | const service = this.one(serviceId); |
668 | 676 | ||
669 | const delay = 1000; | 677 | const delay = 2000; |
670 | 678 | ||
671 | if (service) { | 679 | if (service) { |
672 | if (service.timer !== null) { | 680 | if (service.timer !== null) { |
diff --git a/src/theme/dark/index.js b/src/theme/dark/index.js index 2b7f780d9..8fdb321f3 100644 --- a/src/theme/dark/index.js +++ b/src/theme/dark/index.js | |||
@@ -14,3 +14,6 @@ export const colorWebviewErrorHandlerBackground = legacyStyles.darkThemeGrayDark | |||
14 | // Loader | 14 | // Loader |
15 | export const colorFullscreenLoaderSpinner = '#FFF'; | 15 | export const colorFullscreenLoaderSpinner = '#FFF'; |
16 | export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.darkThemeGrayDarkest, 0.5); | 16 | export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.darkThemeGrayDarkest, 0.5); |
17 | |||
18 | // Modal | ||
19 | export const colorModalOverlayBackground = hexToRgba(legacyStyles.darkThemeGrayDarkest, 0.8); | ||
diff --git a/src/theme/default/index.js b/src/theme/default/index.js index 34e3f7265..61410073d 100644 --- a/src/theme/default/index.js +++ b/src/theme/default/index.js | |||
@@ -30,3 +30,6 @@ export const colorWebviewErrorHandlerBackground = legacyStyles.themeGrayLighter; | |||
30 | export const colorAppLoaderSpinner = '#FFF'; | 30 | export const colorAppLoaderSpinner = '#FFF'; |
31 | export const colorFullscreenLoaderSpinner = legacyStyles.themeGrayDark; | 31 | export const colorFullscreenLoaderSpinner = legacyStyles.themeGrayDark; |
32 | export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.themeGrayLighter, 0.8); | 32 | export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.themeGrayLighter, 0.8); |
33 | |||
34 | // Modal | ||
35 | export const colorModalOverlayBackground = hexToRgba(legacyStyles.themeGrayLighter, 0.8); | ||
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js index bd099987d..a76c03e5a 100644 --- a/src/webview/contextMenu.js +++ b/src/webview/contextMenu.js | |||
@@ -277,6 +277,6 @@ export default function contextMenu(spellcheckProvider, isSpellcheckEnabled, get | |||
277 | ), | 277 | ), |
278 | ); | 278 | ); |
279 | 279 | ||
280 | menu.popup(remote.getCurrentWindow()); | 280 | menu.popup(); |
281 | }); | 281 | }); |
282 | } | 282 | } |
diff --git a/src/webview/recipe.js b/src/webview/recipe.js index 944883899..9aa89ce01 100644 --- a/src/webview/recipe.js +++ b/src/webview/recipe.js | |||
@@ -42,9 +42,9 @@ class RecipeController { | |||
42 | 42 | ||
43 | async initialize() { | 43 | async initialize() { |
44 | Object.keys(this.ipcEvents).forEach((channel) => { | 44 | Object.keys(this.ipcEvents).forEach((channel) => { |
45 | ipcRenderer.on(channel, (event, data) => { | 45 | ipcRenderer.on(channel, (...args) => { |
46 | debug('Received IPC event for channel', channel, 'with', data); | 46 | debug('Received IPC event for channel', channel, 'with', ...args); |
47 | this[this.ipcEvents[channel]](event, data); | 47 | this[this.ipcEvents[channel]](...args); |
48 | }); | 48 | }); |
49 | }); | 49 | }); |
50 | 50 | ||
@@ -62,17 +62,18 @@ class RecipeController { | |||
62 | autorun(() => this.update()); | 62 | autorun(() => this.update()); |
63 | } | 63 | } |
64 | 64 | ||
65 | loadRecipeModule(event, data) { | 65 | loadRecipeModule(event, config, recipe) { |
66 | debug('loadRecipeModule'); | 66 | debug('loadRecipeModule'); |
67 | const modulePath = path.join(data.recipe.path, 'webview.js'); | 67 | const modulePath = path.join(recipe.path, 'webview.js'); |
68 | debug('module path', modulePath); | ||
68 | // Delete module from cache | 69 | // Delete module from cache |
69 | delete require.cache[require.resolve(modulePath)]; | 70 | delete require.cache[require.resolve(modulePath)]; |
70 | try { | 71 | try { |
71 | // eslint-disable-next-line | 72 | // eslint-disable-next-line |
72 | require(modulePath)(new RecipeWebview(), data); | 73 | require(modulePath)(new RecipeWebview(), {...config, recipe,}); |
73 | debug('Initialize Recipe', data); | 74 | debug('Initialize Recipe', config, recipe); |
74 | 75 | ||
75 | this.settings.service = data; | 76 | this.settings.service = config; |
76 | } catch (err) { | 77 | } catch (err) { |
77 | console.error('Recipe initialization failed', err); | 78 | console.error('Recipe initialization failed', err); |
78 | } | 79 | } |