aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2019-02-01 10:35:18 +0100
committerLibravatar Stefan Malzner <stefan@adlk.io>2019-02-01 10:35:18 +0100
commitda92cd426cbf350313945e3459f96638a79bd44e (patch)
tree931d18bbf62854366ccf7021d6206de7e0c42636 /src
parentUpdate stale.yml (diff)
parentb23 (diff)
downloadferdium-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.js1
-rw-r--r--src/components/layout/AppLayout.js2
-rw-r--r--src/components/ui/Modal/index.js59
-rw-r--r--src/components/ui/Modal/styles.js32
-rw-r--r--src/containers/settings/AccountScreen.js8
-rw-r--r--src/electron/ipc-api/settings.js1
-rw-r--r--src/features/basicAuth/Component.js102
-rw-r--r--src/features/basicAuth/Form.js17
-rw-r--r--src/features/basicAuth/index.js68
-rw-r--r--src/features/basicAuth/mainIpcHandler.js9
-rw-r--r--src/features/basicAuth/styles.js12
-rw-r--r--src/features/delayApp/Component.js14
-rw-r--r--src/features/delayApp/index.js23
-rw-r--r--src/helpers/password-helpers.js4
-rw-r--r--src/index.js123
-rw-r--r--src/lib/analytics.js3
-rw-r--r--src/models/Service.js22
-rw-r--r--src/stores/FeaturesStore.js11
-rw-r--r--src/stores/ServicesStore.js16
-rw-r--r--src/theme/dark/index.js3
-rw-r--r--src/theme/default/index.js3
-rw-r--r--src/webview/contextMenu.js2
-rw-r--r--src/webview/recipe.js17
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
7import InfoBar from '../ui/InfoBar'; 7import InfoBar from '../ui/InfoBar';
8import { Component as DelayApp } from '../../features/delayApp'; 8import { Component as DelayApp } from '../../features/delayApp';
9import { Component as BasicAuth } from '../../features/basicAuth';
9import ErrorBoundary from '../util/ErrorBoundary'; 10import ErrorBoundary from '../util/ErrorBoundary';
10 11
11import globalMessages from '../../i18n/globalMessages'; 12import 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 @@
1import React, { Component } from 'react';
2import ReactModal from 'react-modal';
3import PropTypes from 'prop-types';
4import classnames from 'classnames';
5import injectCSS from 'react-jss';
6
7import styles from './styles';
8
9export 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 @@
1export 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';
14const { BrowserWindow } = remote; 14const { BrowserWindow } = remote;
15 15
16export default @inject('stores', 'actions') @observer class AccountScreen extends Component { 16export 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
3export default (params) => { 3export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import injectSheet from 'react-jss';
4import { observer } from 'mobx-react';
5import classnames from 'classnames';
6
7import Modal from '../../components/ui/Modal';
8import Input from '../../components/ui/Input';
9import Button from '../../components/ui/Button';
10
11import {
12 state,
13 resetState,
14 sendCredentials,
15 cancelLogin,
16} from '.';
17import Form from './Form';
18
19import styles from './styles';
20
21export 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 @@
1import Form from '../../lib/Form';
2
3export 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 @@
1import { ipcRenderer } from 'electron';
2import { observable } from 'mobx';
3
4import BasicAuthComponent from './Component';
5
6const debug = require('debug')('Franz:feature:basicAuth');
7
8const defaultState = {
9 isModalVisible: false,
10 service: null,
11 authInfo: null,
12};
13
14export const state = observable(defaultState);
15
16export function resetState() {
17 Object.assign(state, defaultState);
18 console.log('reset state', state);
19}
20
21export 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
45export 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
53export 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
62export function cancelLogin() {
63 debug('Cancel basic auth event');
64
65 ipcRenderer.send('feature-basic-auth-cancel');
66}
67
68export 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 @@
1const debug = require('debug')('Franz:feature:basicAuth:main');
2
3export 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 @@
1export 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';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6 6
7import { gaEvent } from '../../lib/analytics';
8
7import Button from '../../components/ui/Button'; 9import Button from '../../components/ui/Button';
8 10
9import { config } from '.'; 11import { 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';
3import DelayAppComponent from './Component'; 3import DelayAppComponent from './Component';
4 4
5import { DEFAULT_FEATURES_CONFIG } from '../../config'; 5import { DEFAULT_FEATURES_CONFIG } from '../../config';
6import { gaEvent } from '../../lib/analytics';
6 7
7const debug = require('debug')('Franz:feature:delayApp'); 8const debug = require('debug')('Franz:feature:delayApp');
8 9
@@ -22,19 +23,18 @@ function setVisibility(value) {
22} 23}
23 24
24export default function init(stores) { 25export 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 @@
1import { SHA256 } from 'jshashes'; 1import crypto from 'crypto';
2 2
3export function hash(password) { 3export function hash(password) {
4 return new SHA256().b64(password); 4 return crypto.createHash('sha256').update(password).digest('base64');
5} 5}
6 6
7export function scorePassword(password) { 7export 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 @@
1import { 1import {
2 app, BrowserWindow, shell, ipcMain, 2 app,
3 BrowserWindow,
4 shell,
5 ipcMain,
3} from 'electron'; 6} from 'electron';
4 7
5import fs from 'fs-extra'; 8import fs from 'fs-extra';
@@ -7,9 +10,14 @@ import path from 'path';
7import windowStateKeeper from 'electron-window-state'; 10import windowStateKeeper from 'electron-window-state';
8 11
9import { 12import {
10 isDevMode, isMac, isWindows, isLinux, 13 isDevMode,
14 isMac,
15 isWindows,
16 isLinux,
11} from './environment'; 17} from './environment';
12 18
19import { mainIpcHandler as basicAuthHandler } from './features/basicAuth';
20
13// DEV MODE: Save user data into FranzDev 21// DEV MODE: Save user data into FranzDev
14if (isDevMode) { 22if (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
49const isSecondInstance = app.makeSingleInstance((argv) => { 57const gotTheLock = app.requestSingleInstanceLock();
50 if (mainWindow) { 58if (!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
74if (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
278const noop = () => null;
279let authCallback = noop;
232app.on('login', (event, webContents, request, authInfo, callback) => { 280app.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
300ipcMain.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
307ipcMain.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.
250app.on('window-all-closed', () => { 315app.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 @@
1import { remote } from 'electron'; 1import { remote } from 'electron';
2import { GA_ID } from '../config'; 2import { GA_ID } from '../config';
3// import { isDevMode } from '../environment';
4 3
5const debug = require('debug')('Franz:Analytics'); 4const debug = require('debug')('Franz:Analytics');
6 5
@@ -36,7 +35,7 @@ export function gaPage(page) {
36export function gaEvent(category, action, label) { 35export 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
42setTimeout(() => { 41setTimeout(() => {
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 @@
1import { computed, observable } from 'mobx'; 1import { computed, observable, reaction } from 'mobx';
2 2
3import Store from './lib/Store'; 3import Store from './lib/Store';
4import CachedRequest from './lib/CachedRequest'; 4import CachedRequest from './lib/CachedRequest';
@@ -6,6 +6,7 @@ import CachedRequest from './lib/CachedRequest';
6import delayApp from '../features/delayApp'; 6import delayApp from '../features/delayApp';
7import spellchecker from '../features/spellchecker'; 7import spellchecker from '../features/spellchecker';
8import serviceProxy from '../features/serviceProxy'; 8import serviceProxy from '../features/serviceProxy';
9import basicAuth from '../features/basicAuth';
9 10
10import { DEFAULT_FEATURES_CONFIG } from '../config'; 11import { 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 @@
1import { 1import {
2 action, reaction, computed, observable, 2 action,
3 reaction,
4 computed,
5 observable,
3} from 'mobx'; 6} from 'mobx';
4import { debounce, remove } from 'lodash'; 7import { 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
15export const colorFullscreenLoaderSpinner = '#FFF'; 15export const colorFullscreenLoaderSpinner = '#FFF';
16export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.darkThemeGrayDarkest, 0.5); 16export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.darkThemeGrayDarkest, 0.5);
17
18// Modal
19export 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;
30export const colorAppLoaderSpinner = '#FFF'; 30export const colorAppLoaderSpinner = '#FFF';
31export const colorFullscreenLoaderSpinner = legacyStyles.themeGrayDark; 31export const colorFullscreenLoaderSpinner = legacyStyles.themeGrayDark;
32export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.themeGrayLighter, 0.8); 32export const colorWebviewLoaderBackground = hexToRgba(legacyStyles.themeGrayLighter, 0.8);
33
34// Modal
35export 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 }