aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2018-02-19 11:50:11 +0100
committerLibravatar Stefan Malzner <stefan@adlk.io>2018-02-19 11:50:11 +0100
commit857221ad84b31819d22bb4ce028dd8cb8d606f86 (patch)
treefecf60fe2e210197f3eac53e55ad44ac94d3ca3c
parentAutomatic i18n update (i18n.meetfranz.com) (diff)
parentfix(Linux): Fix window restore on notification click (@closingin) (diff)
downloadferdium-app-857221ad84b31819d22bb4ce028dd8cb8d606f86.tar.gz
ferdium-app-857221ad84b31819d22bb4ce028dd8cb8d606f86.tar.zst
ferdium-app-857221ad84b31819d22bb4ce028dd8cb8d606f86.zip
Merge branch 'develop' into i18n
-rw-r--r--.github/ISSUE_TEMPLATE.md2
-rw-r--r--CHANGELOG.md36
-rw-r--r--build-helpers/images/icon.icobin370070 -> 325490 bytes
-rw-r--r--package.json6
-rw-r--r--src/api/LocalApi.js12
-rw-r--r--src/api/server/LocalApi.js32
-rw-r--r--src/api/server/ServerApi.js7
-rw-r--r--src/app.js4
-rw-r--r--src/assets/images/tray/linux/tray-unread.pngbin1264 -> 2786 bytes
-rw-r--r--src/assets/images/tray/linux/tray-unread@2x.pngbin2026 -> 6062 bytes
-rw-r--r--src/assets/images/tray/linux/tray.pngbin1230 -> 2847 bytes
-rw-r--r--src/assets/images/tray/linux/tray@2x.pngbin1545 -> 6318 bytes
-rw-r--r--src/components/auth/Import.js17
-rw-r--r--src/components/auth/Invite.js128
-rw-r--r--src/components/auth/Login.js4
-rw-r--r--src/components/auth/Password.js2
-rw-r--r--src/components/auth/Pricing.js2
-rw-r--r--src/components/auth/Signup.js10
-rw-r--r--src/components/auth/Welcome.js2
-rw-r--r--src/components/layout/Sidebar.js4
-rw-r--r--src/components/settings/account/AccountDashboard.js54
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js11
-rw-r--r--src/components/settings/services/ServiceError.js2
-rw-r--r--src/components/subscription/SubscriptionForm.js (renamed from src/components/ui/Subscription.js)110
-rw-r--r--src/components/subscription/SubscriptionPopup.js (renamed from src/components/ui/SubscriptionPopup.js)0
-rw-r--r--src/containers/auth/InviteScreen.js2
-rw-r--r--src/containers/settings/AccountScreen.js12
-rw-r--r--src/containers/settings/EditServiceScreen.js12
-rw-r--r--src/containers/settings/EditUserScreen.js12
-rw-r--r--src/containers/settings/InviteScreen.js44
-rw-r--r--src/containers/subscription/SubscriptionFormScreen.js (renamed from src/containers/ui/SubscriptionFormScreen.js)57
-rw-r--r--src/containers/subscription/SubscriptionPopupScreen.js (renamed from src/containers/ui/SubscriptionPopupScreen.js)2
-rw-r--r--src/helpers/validation-helpers.js2
-rw-r--r--src/i18n/locales/en-US.json16
-rw-r--r--src/lib/Form.js5
-rw-r--r--src/lib/Menu.js8
-rw-r--r--src/lib/Miner.js72
-rw-r--r--src/lib/Tray.js6
-rw-r--r--src/models/Service.js1
-rw-r--r--src/models/Settings.js1
-rw-r--r--src/models/User.js4
-rw-r--r--src/stores/AppStore.js61
-rw-r--r--src/stores/ServicesStore.js13
-rw-r--r--src/stores/SettingsStore.js26
-rw-r--r--src/stores/UserStore.js20
-rw-r--r--src/styles/invite.scss15
-rw-r--r--src/styles/main.scss1
-rw-r--r--src/styles/subscription.scss8
-rw-r--r--src/styles/welcome.scss9
-rw-r--r--yarn.lock21
50 files changed, 394 insertions, 481 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 5317684cf..9cb983771 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,6 +1,6 @@
1<!--- Provide a general summary of the issue in the Title above --> 1<!--- Provide a general summary of the issue in the Title above -->
2 2
3<!-- This repository is only for the Franz client. Please use this form ( https://goo.gl/forms/zbwlx3VFvAo2oink2q ) for service requests or check out the guide ( https://github.com/meetfranz/plugins ) to create your own service integration. --> 3<!-- This repository is only for the Franz client. Please use this form ( https://bitly.com/franz-service-request ) for service requests or check out the guide ( https://github.com/meetfranz/plugins ) to create your own service integration. -->
4 4
5<!--- If you want to propose a feature, use this template: https://raw.githubusercontent.com/meetfranz/franz/master/.github/FEATURE_PROPOSAL_TEMPLATE.md --> 5<!--- If you want to propose a feature, use this template: https://raw.githubusercontent.com/meetfranz/franz/master/.github/FEATURE_PROPOSAL_TEMPLATE.md -->
6 6
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef7e1c586..2dd42eec2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,39 @@
1<a name="5.0.0-beta.15"></a>
2# [5.0.0-beta.15](https://github.com/meetfranz/franz/compare/v5.0.0-beta.14...v5.0.0-beta.15) (2018-01-10)
3
4
5### Features
6
7* **App:** Add option to clear app cache ([@dannyqiu](https://github.com/dannyqiu)) ([be801ff](https://github.com/meetfranz/franz/commit/be801ff))
8* **App:** Add option to show/hide notification badges for muted ([893a9f6](https://github.com/meetfranz/franz/commit/893a9f6))
9* **Recipes:** Add semver version validation ([5826dc3](https://github.com/meetfranz/franz/commit/5826dc3))
10* **Recipes:** Add `hasHostedOption` to enable hosted & self hosted services ([03610f2](https://github.com/meetfranz/franz/commit/03610f2))
11* **Services:** Add custom service icon upload ([6b97e42](https://github.com/meetfranz/franz/commit/6b97e42))
12* **Services:** Add option to completely disable message badges ([cea7a5c](https://github.com/meetfranz/franz/commit/cea7a5c))
13* **Services:** Improve handling of external links ([e2d6edf](https://github.com/meetfranz/franz/commit/e2d6edf))
14* **Services:** Improve user experience of service search ([7e784c6](https://github.com/meetfranz/franz/commit/7e784c6))
15* **Account:** Enable a user to delete their own account ([1f3df73](https://github.com/meetfranz/franz/commit/1f3df73))
16* **Translations:** Improved translations. **[A million thanks to the amazing community. 🎉](http://i18n.meetfranz.com/)**
17
18
19
20### Bug Fixes
21
22* **App:** Allow to turn on notifications when system dnd is enabled ([3045b47](https://github.com/meetfranz/franz/commit/3045b47))
23* **App:** App mute now disables notifications as well ([0fa1caf](https://github.com/meetfranz/franz/commit/0fa1caf))
24* **App:** Fix service reload after waking machine up ([531531e](https://github.com/meetfranz/franz/commit/531531e))
25* **App:** Fix "add services manually" message ([ac417dc](https://github.com/meetfranz/franz/commit/ac417dc))
26* **i18n:** Fallback to system language or english ([9733eaf](https://github.com/meetfranz/franz/commit/9733eaf))
27* **Notification:** Remove notification sound when app is muted ([53fde0c](https://github.com/meetfranz/franz/commit/53fde0c))
28* **Recipes:** Enable `urlInputPrefix` for team and customURL ([873957d](https://github.com/meetfranz/franz/commit/873957d))
29* **Services:** Ctrl/Cmd+R now navigates back to the service root ([7293492](https://github.com/meetfranz/franz/commit/7293492))
30* **Services:** Fix transparent service background ([ed0098f](https://github.com/meetfranz/franz/commit/ed0098f)), closes [#361](https://github.com/meetfranz/franz/issues/361)
31* **Shortcuts:** Fixed settings shortcut inconsistency ([ca74846](https://github.com/meetfranz/franz/commit/ca74846))
32* **Spell checker:** Fixed issues with spell checker ([965fdf2](https://github.com/meetfranz/franz/commit/965fdf2))
33* **Translations:** Re-add Spanish to available languages. ([ad936f2](https://github.com/meetfranz/franz/commit/ad936f2))
34* **Windows:** Open window when clicking on toast notification ([b82bbc8](https://github.com/meetfranz/franz/commit/b82bbc8)), closes [#370](https://github.com/meetfranz/franz/issues/370)
35
36
1<a name="5.0.0-beta.14"></a> 37<a name="5.0.0-beta.14"></a>
2# [5.0.0-beta.14](https://github.com/meetfranz/franz/compare/v5.0.0-beta.13...v5.0.0-beta.14) (2017-11-23) 38# [5.0.0-beta.14](https://github.com/meetfranz/franz/compare/v5.0.0-beta.13...v5.0.0-beta.14) (2017-11-23)
3 39
diff --git a/build-helpers/images/icon.ico b/build-helpers/images/icon.ico
index 947f703a3..db626c20b 100644
--- a/build-helpers/images/icon.ico
+++ b/build-helpers/images/icon.ico
Binary files differ
diff --git a/package.json b/package.json
index 5f5dd4182..a14367bf2 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
2 "name": "franz", 2 "name": "franz",
3 "productName": "Franz", 3 "productName": "Franz",
4 "appId": "com.meetfranz.franz", 4 "appId": "com.meetfranz.franz",
5 "version": "5.0.0-beta.14", 5 "version": "5.0.0-beta.15",
6 "description": "Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many more.", 6 "description": "Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many more.",
7 "copyright": "adlk x franz - Stefan Malzner", 7 "copyright": "adlk x franz - Stefan Malzner",
8 "main": "index.js", 8 "main": "index.js",
@@ -28,7 +28,6 @@
28 "license": "Apache-2.0", 28 "license": "Apache-2.0",
29 "dependencies": { 29 "dependencies": {
30 "@meetfranz/electron-notification-state": "^1.0.0", 30 "@meetfranz/electron-notification-state": "^1.0.0",
31 "@paulcbetts/system-idle-time": "^1.0.4",
32 "address-rfc2822": "^2.0.1", 31 "address-rfc2822": "^2.0.1",
33 "auto-launch": "https://github.com/meetfranz/node-auto-launch.git", 32 "auto-launch": "https://github.com/meetfranz/node-auto-launch.git",
34 "babel-polyfill": "^6.23.0", 33 "babel-polyfill": "^6.23.0",
@@ -50,6 +49,7 @@
50 "minimist": "^1.2.0", 49 "minimist": "^1.2.0",
51 "mkdirp": "^0.5.1", 50 "mkdirp": "^0.5.1",
52 "mobx": "^3.1.0", 51 "mobx": "^3.1.0",
52 "mobx-localstorage": "^0.1.7",
53 "mobx-react": "^4.1.0", 53 "mobx-react": "^4.1.0",
54 "mobx-react-form": "^1.32.2", 54 "mobx-react-form": "^1.32.2",
55 "mobx-react-router": "^3.1.2", 55 "mobx-react-router": "^3.1.2",
@@ -91,7 +91,7 @@
91 "cz-conventional-changelog": "^2.0.0", 91 "cz-conventional-changelog": "^2.0.0",
92 "del": "^2.2.2", 92 "del": "^2.2.2",
93 "dotenv": "^4.0.0", 93 "dotenv": "^4.0.0",
94 "electron": "^1.7.9", 94 "electron": "^1.7.12",
95 "electron-builder": "19.15.1", 95 "electron-builder": "19.15.1",
96 "electron-packager": "^8.7.0", 96 "electron-packager": "^8.7.0",
97 "electron-rebuild": "^1.6.0", 97 "electron-rebuild": "^1.6.0",
diff --git a/src/api/LocalApi.js b/src/api/LocalApi.js
index 3f84f8a0b..59d7d8fa2 100644
--- a/src/api/LocalApi.js
+++ b/src/api/LocalApi.js
@@ -4,18 +4,6 @@ export default class LocalApi {
4 this.local = local; 4 this.local = local;
5 } 5 }
6 6
7 getSettings() {
8 return this.local.getAppSettings();
9 }
10
11 updateSettings(data) {
12 return this.local.updateAppSettings(data);
13 }
14
15 removeKey(key) {
16 return this.local.removeKey(key);
17 }
18
19 getAppCacheSize() { 7 getAppCacheSize() {
20 return this.local.getAppCacheSize(); 8 return this.local.getAppCacheSize();
21 } 9 }
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js
index e95d750ac..aa3a7d655 100644
--- a/src/api/server/LocalApi.js
+++ b/src/api/server/LocalApi.js
@@ -6,38 +6,6 @@ import { getServicePartitionsDirectory } from '../../helpers/service-helpers.js'
6const { session } = remote; 6const { session } = remote;
7 7
8export default class LocalApi { 8export default class LocalApi {
9 // App
10 async updateAppSettings(data) {
11 const currentSettings = await this.getAppSettings();
12 const settings = Object.assign(currentSettings, data);
13
14 localStorage.setItem('app', JSON.stringify(settings));
15 console.debug('LocalApi::updateAppSettings resolves', settings);
16
17 return settings;
18 }
19
20 async getAppSettings() {
21 const settingsString = localStorage.getItem('app');
22 try {
23 const settings = JSON.parse(settingsString) || {};
24 console.debug('LocalApi::getAppSettings resolves', settings);
25
26 return settings;
27 } catch (err) {
28 return {};
29 }
30 }
31
32 async removeKey(key) {
33 const settings = await this.getAppSettings();
34
35 if (Object.hasOwnProperty.call(settings, key)) {
36 delete settings[key];
37 localStorage.setItem('app', JSON.stringify(settings));
38 }
39 }
40
41 // Services 9 // Services
42 async getAppCacheSize() { 10 async getAppCacheSize() {
43 const partitionsDir = getServicePartitionsDirectory(); 11 const partitionsDir = getServicePartitionsDirectory();
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index a684ff98b..8f3297d13 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -3,6 +3,7 @@ import path from 'path';
3import tar from 'tar'; 3import tar from 'tar';
4import fs from 'fs-extra'; 4import fs from 'fs-extra';
5import { remote } from 'electron'; 5import { remote } from 'electron';
6import localStorage from 'mobx-localstorage';
6 7
7import ServiceModel from '../../models/Service'; 8import ServiceModel from '../../models/Service';
8import RecipePreviewModel from '../../models/RecipePreview'; 9import RecipePreviewModel from '../../models/RecipePreview';
@@ -173,9 +174,9 @@ export default class ServerApi {
173 const serviceData = await request.json(); 174 const serviceData = await request.json();
174 175
175 if (data.iconFile) { 176 if (data.iconFile) {
176 const iconUrl = await this.uploadServiceIcon(serviceData.data.id, data.iconFile); 177 const iconData = await this.uploadServiceIcon(serviceData.data.id, data.iconFile);
177 178
178 serviceData.data.iconUrl = iconUrl; 179 serviceData.data = iconData;
179 } 180 }
180 181
181 const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); 182 const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) });
@@ -227,7 +228,7 @@ export default class ServerApi {
227 228
228 const serviceData = await request.json(); 229 const serviceData = await request.json();
229 230
230 return serviceData.data.iconUrl; 231 return serviceData.data;
231 } 232 }
232 233
233 async reorderService(data) { 234 async reorderService(data) {
diff --git a/src/app.js b/src/app.js
index 8e62776d2..814bfacf1 100644
--- a/src/app.js
+++ b/src/app.js
@@ -27,6 +27,7 @@ import EditServiceScreen from './containers/settings/EditServiceScreen';
27import AccountScreen from './containers/settings/AccountScreen'; 27import AccountScreen from './containers/settings/AccountScreen';
28import EditUserScreen from './containers/settings/EditUserScreen'; 28import EditUserScreen from './containers/settings/EditUserScreen';
29import EditSettingsScreen from './containers/settings/EditSettingsScreen'; 29import EditSettingsScreen from './containers/settings/EditSettingsScreen';
30import InviteSettingsScreen from './containers/settings/InviteScreen';
30import WelcomeScreen from './containers/auth/WelcomeScreen'; 31import WelcomeScreen from './containers/auth/WelcomeScreen';
31import LoginScreen from './containers/auth/LoginScreen'; 32import LoginScreen from './containers/auth/LoginScreen';
32import PasswordScreen from './containers/auth/PasswordScreen'; 33import PasswordScreen from './containers/auth/PasswordScreen';
@@ -35,7 +36,7 @@ import ImportScreen from './containers/auth/ImportScreen';
35import PricingScreen from './containers/auth/PricingScreen'; 36import PricingScreen from './containers/auth/PricingScreen';
36import InviteScreen from './containers/auth/InviteScreen'; 37import InviteScreen from './containers/auth/InviteScreen';
37import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; 38import AuthLayoutContainer from './containers/auth/AuthLayoutContainer';
38import SubscriptionPopupScreen from './containers/ui/SubscriptionPopupScreen'; 39import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen';
39 40
40// Add Polyfills 41// Add Polyfills
41smoothScroll.polyfill(); 42smoothScroll.polyfill();
@@ -74,6 +75,7 @@ window.addEventListener('load', () => {
74 <Route path="/settings/user" component={AccountScreen} /> 75 <Route path="/settings/user" component={AccountScreen} />
75 <Route path="/settings/user/edit" component={EditUserScreen} /> 76 <Route path="/settings/user/edit" component={EditUserScreen} />
76 <Route path="/settings/app" component={EditSettingsScreen} /> 77 <Route path="/settings/app" component={EditSettingsScreen} />
78 <Route path="/settings/invite" component={InviteSettingsScreen} />
77 </Route> 79 </Route>
78 </Route> 80 </Route>
79 <Route path="/auth" component={AuthLayoutContainer}> 81 <Route path="/auth" component={AuthLayoutContainer}>
diff --git a/src/assets/images/tray/linux/tray-unread.png b/src/assets/images/tray/linux/tray-unread.png
index a94ad81fb..5697141e0 100644
--- a/src/assets/images/tray/linux/tray-unread.png
+++ b/src/assets/images/tray/linux/tray-unread.png
Binary files differ
diff --git a/src/assets/images/tray/linux/tray-unread@2x.png b/src/assets/images/tray/linux/tray-unread@2x.png
index 56e74b16a..d31e1401d 100644
--- a/src/assets/images/tray/linux/tray-unread@2x.png
+++ b/src/assets/images/tray/linux/tray-unread@2x.png
Binary files differ
diff --git a/src/assets/images/tray/linux/tray.png b/src/assets/images/tray/linux/tray.png
index 583f34df8..29a13da12 100644
--- a/src/assets/images/tray/linux/tray.png
+++ b/src/assets/images/tray/linux/tray.png
Binary files differ
diff --git a/src/assets/images/tray/linux/tray@2x.png b/src/assets/images/tray/linux/tray@2x.png
index 479a2cf95..256c52dcc 100644
--- a/src/assets/images/tray/linux/tray@2x.png
+++ b/src/assets/images/tray/linux/tray@2x.png
Binary files differ
diff --git a/src/components/auth/Import.js b/src/components/auth/Import.js
index 078244434..9ba14e768 100644
--- a/src/components/auth/Import.js
+++ b/src/components/auth/Import.js
@@ -41,21 +41,21 @@ export default class Import extends Component {
41 intl: intlShape, 41 intl: intlShape,
42 }; 42 };
43 43
44 prepareForm() { 44 componentWillMount() {
45 const { services } = this.props;
46
47 const config = { 45 const config = {
48 fields: { 46 fields: {
49 import: [...services.filter(s => s.recipe).map(s => ({ 47 import: [...this.props.services.filter(s => s.recipe).map(s => ({
50 add: { 48 fields: {
51 default: true, 49 add: {
52 options: s, 50 default: true,
51 options: s,
52 },
53 }, 53 },
54 }))], 54 }))],
55 }, 55 },
56 }; 56 };
57 57
58 return new Form(config, this.context.intl); 58 this.form = new Form(config, this.context.intl);
59 } 59 }
60 60
61 submit(e) { 61 submit(e) {
@@ -74,7 +74,6 @@ export default class Import extends Component {
74 } 74 }
75 75
76 render() { 76 render() {
77 this.form = this.prepareForm();
78 const { intl } = this.context; 77 const { intl } = this.context;
79 const { services, isSubmitting, inviteRoute } = this.props; 78 const { services, isSubmitting, inviteRoute } = this.props;
80 79
diff --git a/src/components/auth/Invite.js b/src/components/auth/Invite.js
index c1d815dcd..f1c16986b 100644
--- a/src/components/auth/Invite.js
+++ b/src/components/auth/Invite.js
@@ -3,13 +3,20 @@ import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6import classnames from 'classnames';
6 7
8import Infobox from '../ui/Infobox';
9import Appear from '../ui/effects/Appear';
7import Form from '../../lib/Form'; 10import Form from '../../lib/Form';
8import { email } from '../../helpers/validation-helpers'; 11import { email } from '../../helpers/validation-helpers';
9import Input from '../ui/Input'; 12import Input from '../ui/Input';
10import Button from '../ui/Button'; 13import Button from '../ui/Button';
11 14
12const messages = defineMessages({ 15const messages = defineMessages({
16 settingsHeadline: {
17 id: 'settings.invite.headline',
18 defaultMessage: '!!!Invite Friends',
19 },
13 headline: { 20 headline: {
14 id: 'invite.headline.friends', 21 id: 'invite.headline.friends',
15 defaultMessage: '!!!Invite 3 of your friends or colleagues', 22 defaultMessage: '!!!Invite 3 of your friends or colleagues',
@@ -30,41 +37,77 @@ const messages = defineMessages({
30 id: 'invite.skip.label', 37 id: 'invite.skip.label',
31 defaultMessage: '!!!I want to do this later', 38 defaultMessage: '!!!I want to do this later',
32 }, 39 },
40 inviteSuccessInfo: {
41 id: 'invite.successInfo',
42 defaultMessage: '!!!Invitations sent successfully',
43 },
33}); 44});
34 45
35@observer 46@observer
36export default class Invite extends Component { 47export default class Invite extends Component {
37 static propTypes = { 48 static propTypes = {
38 onSubmit: PropTypes.func.isRequired, 49 onSubmit: PropTypes.func.isRequired,
50 embed: PropTypes.bool,
51 isInviteSuccessful: PropTypes.bool,
52 isLoadingInvite: PropTypes.bool,
53 };
54
55 static defaultProps = {
56 embed: false,
57 isInviteSuccessful: false,
58 isLoadingInvite: false,
39 }; 59 };
40 60
41 static contextTypes = { 61 static contextTypes = {
42 intl: intlShape, 62 intl: intlShape,
43 }; 63 };
44 64
45 form = new Form({ 65 state = { showSuccessInfo: false };
46 fields: { 66
47 invite: [...Array(3).fill({ 67 componentWillMount() {
48 name: { 68 const handlers = {
49 label: this.context.intl.formatMessage(messages.nameLabel), 69 onChange: () => {
50 // value: '', 70 this.setState({ showSuccessInfo: false });
51 placeholder: this.context.intl.formatMessage(messages.nameLabel), 71 },
52 }, 72 };
53 email: { 73
54 label: this.context.intl.formatMessage(messages.emailLabel), 74 this.form = new Form({
55 // value: '', 75 fields: {
56 validate: [email], 76 invite: [...Array(3).fill({
57 placeholder: this.context.intl.formatMessage(messages.emailLabel), 77 fields: {
58 }, 78 name: {
59 })], 79 label: this.context.intl.formatMessage(messages.nameLabel),
60 }, 80 placeholder: this.context.intl.formatMessage(messages.nameLabel),
61 }, this.context.intl); 81 handlers,
82 // related: ['invite.0.email'], // path accepted but does not work
83 },
84 email: {
85 label: this.context.intl.formatMessage(messages.emailLabel),
86 placeholder: this.context.intl.formatMessage(messages.emailLabel),
87 handlers,
88 validators: [email],
89 },
90 },
91 })],
92 },
93 }, this.context.intl);
94 }
95
96 componentDidMount() {
97 document.querySelector('input:first-child').focus();
98 }
62 99
63 submit(e) { 100 submit(e) {
64 e.preventDefault(); 101 e.preventDefault();
102
65 this.form.submit({ 103 this.form.submit({
66 onSuccess: (form) => { 104 onSuccess: (form) => {
67 this.props.onSubmit({ invites: form.values().invite }); 105 this.props.onSubmit({ invites: form.values().invite });
106
107 this.form.clear();
108 // this.form.$('invite.0.name').focus(); // path accepted but does not focus ;(
109 document.querySelector('input:first-child').focus();
110 this.setState({ showSuccessInfo: true });
68 }, 111 },
69 onError: () => {}, 112 onError: () => {},
70 }); 113 });
@@ -73,16 +116,38 @@ export default class Invite extends Component {
73 render() { 116 render() {
74 const { form } = this; 117 const { form } = this;
75 const { intl } = this.context; 118 const { intl } = this.context;
119 const { embed, isInviteSuccessful, isLoadingInvite } = this.props;
120
121 const atLeastOneEmailAddress = form.$('invite')
122 .map(invite => invite.$('email').value)
123 .some(emailValue => emailValue.trim() !== '');
124
125 const sendButtonClassName = classnames({
126 auth__button: true,
127 'invite__embed--button': embed,
128 });
129
130 const renderForm = (
131 <div>
132 {this.state.showSuccessInfo && isInviteSuccessful && (
133 <Appear>
134 <Infobox
135 type="success"
136 icon="checkbox-marked-circle-outline"
137 dismissable
138 >
139 {intl.formatMessage(messages.inviteSuccessInfo)}
140 </Infobox>
141 </Appear>
142 )}
76 143
77 return (
78 <div className="auth__container auth__container--signup">
79 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 144 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}>
80 <img 145 {!embed && (<img
81 src="./assets/images/logo.svg" 146 src="./assets/images/logo.svg"
82 className="auth__logo" 147 className="auth__logo"
83 alt="" 148 alt=""
84 /> 149 />)}
85 <h1> 150 <h1 className={embed && 'invite__embed'}>
86 {intl.formatMessage(messages.headline)} 151 {intl.formatMessage(messages.headline)}
87 </h1> 152 </h1>
88 {form.$('invite').map(invite => ( 153 {form.$('invite').map(invite => (
@@ -95,17 +160,30 @@ export default class Invite extends Component {
95 ))} 160 ))}
96 <Button 161 <Button
97 type="submit" 162 type="submit"
98 className="auth__button" 163 className={sendButtonClassName}
164 disabled={!atLeastOneEmailAddress}
99 label={intl.formatMessage(messages.submitButtonLabel)} 165 label={intl.formatMessage(messages.submitButtonLabel)}
166 loaded={!isLoadingInvite}
100 /> 167 />
101 <Link 168 {!embed && (<Link
102 to="/" 169 to="/"
103 className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" 170 className="franz-form__button franz-form__button--secondary auth__button auth__button--skip"
104 > 171 >
105 {intl.formatMessage(messages.skipButtonLabel)} 172 {intl.formatMessage(messages.skipButtonLabel)}
106 </Link> 173 </Link>)}
107 </form> 174 </form>
108 </div> 175 </div>
109 ); 176 );
177
178 return (
179 <div className={!embed ? 'auth__container auth__container--signup' : 'settings__main'}>
180 {embed && (
181 <div className="settings__header">
182 <h1>{this.context.intl.formatMessage(messages.settingsHeadline)}</h1>
183 </div>
184 )}
185 {!embed ? <div>{renderForm}</div> : <div className="settings__body invite__form">{renderForm}</div>}
186 </div>
187 );
110 } 188 }
111} 189}
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js
index 67e92849d..4a3cd6776 100644
--- a/src/components/auth/Login.js
+++ b/src/components/auth/Login.js
@@ -76,12 +76,12 @@ export default class Login extends Component {
76 email: { 76 email: {
77 label: this.context.intl.formatMessage(messages.emailLabel), 77 label: this.context.intl.formatMessage(messages.emailLabel),
78 value: '', 78 value: '',
79 validate: [required, email], 79 validators: [required, email],
80 }, 80 },
81 password: { 81 password: {
82 label: this.context.intl.formatMessage(messages.passwordLabel), 82 label: this.context.intl.formatMessage(messages.passwordLabel),
83 value: '', 83 value: '',
84 validate: [required], 84 validators: [required],
85 type: 'password', 85 type: 'password',
86 }, 86 },
87 }, 87 },
diff --git a/src/components/auth/Password.js b/src/components/auth/Password.js
index d2b196853..5bcc80b6e 100644
--- a/src/components/auth/Password.js
+++ b/src/components/auth/Password.js
@@ -60,7 +60,7 @@ export default class Password extends Component {
60 email: { 60 email: {
61 label: this.context.intl.formatMessage(messages.emailLabel), 61 label: this.context.intl.formatMessage(messages.emailLabel),
62 value: '', 62 value: '',
63 validate: [required, email], 63 validators: [required, email],
64 }, 64 },
65 }, 65 },
66 }, this.context.intl); 66 }, this.context.intl);
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
index 761561a89..3cc8d5f6b 100644
--- a/src/components/auth/Pricing.js
+++ b/src/components/auth/Pricing.js
@@ -7,7 +7,7 @@ import { defineMessages, intlShape } from 'react-intl';
7// import Button from '../ui/Button'; 7// import Button from '../ui/Button';
8import Loader from '../ui/Loader'; 8import Loader from '../ui/Loader';
9import Appear from '../ui/effects/Appear'; 9import Appear from '../ui/effects/Appear';
10import SubscriptionForm from '../../containers/ui/SubscriptionFormScreen'; 10import SubscriptionForm from '../../containers/subscription/SubscriptionFormScreen';
11 11
12const messages = defineMessages({ 12const messages = defineMessages({
13 headline: { 13 headline: {
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js
index a990a112e..219948274 100644
--- a/src/components/auth/Signup.js
+++ b/src/components/auth/Signup.js
@@ -82,7 +82,7 @@ export default class Signup extends Component {
82 fields: { 82 fields: {
83 accountType: { 83 accountType: {
84 value: 'individual', 84 value: 'individual',
85 validate: [required], 85 validators: [required],
86 options: [{ 86 options: [{
87 value: 'individual', 87 value: 'individual',
88 label: 'Individual', 88 label: 'Individual',
@@ -97,17 +97,17 @@ export default class Signup extends Component {
97 firstname: { 97 firstname: {
98 label: this.context.intl.formatMessage(messages.firstnameLabel), 98 label: this.context.intl.formatMessage(messages.firstnameLabel),
99 value: '', 99 value: '',
100 validate: [required], 100 validators: [required],
101 }, 101 },
102 lastname: { 102 lastname: {
103 label: this.context.intl.formatMessage(messages.lastnameLabel), 103 label: this.context.intl.formatMessage(messages.lastnameLabel),
104 value: '', 104 value: '',
105 validate: [required], 105 validators: [required],
106 }, 106 },
107 email: { 107 email: {
108 label: this.context.intl.formatMessage(messages.emailLabel), 108 label: this.context.intl.formatMessage(messages.emailLabel),
109 value: '', 109 value: '',
110 validate: [required, email], 110 validators: [required, email],
111 }, 111 },
112 organization: { 112 organization: {
113 label: this.context.intl.formatMessage(messages.companyLabel), 113 label: this.context.intl.formatMessage(messages.companyLabel),
@@ -116,7 +116,7 @@ export default class Signup extends Component {
116 password: { 116 password: {
117 label: this.context.intl.formatMessage(messages.passwordLabel), 117 label: this.context.intl.formatMessage(messages.passwordLabel),
118 value: '', 118 value: '',
119 validate: [required, minLength(6)], 119 validators: [required, minLength(6)],
120 type: 'password', 120 type: 'password',
121 }, 121 },
122 }, 122 },
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index eb9fbb847..9e1c762a5 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -46,7 +46,7 @@ export default class Login extends Component {
46 </div> 46 </div>
47 </div> 47 </div>
48 <div className="welcome__buttons"> 48 <div className="welcome__buttons">
49 <Link to={signupRoute} className="button"> 49 <Link to={signupRoute} className="button button__inverted">
50 {intl.formatMessage(messages.signupButton)} 50 {intl.formatMessage(messages.signupButton)}
51 </Link> 51 </Link>
52 <Link to={loginRoute} className="button"> 52 <Link to={loginRoute} className="button">
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 915ebeace..fa269f216 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -42,6 +42,10 @@ export default class Sidebar extends Component {
42 tooltipEnabled: true, 42 tooltipEnabled: true,
43 }; 43 };
44 44
45 componentDidUpdate() {
46 ReactTooltip.rebuild();
47 }
48
45 enableToolTip() { 49 enableToolTip() {
46 this.setState({ tooltipEnabled: true }); 50 this.setState({ tooltipEnabled: true });
47 } 51 }
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 89fa07800..4992f0913 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -1,7 +1,7 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import ReactTooltip from 'react-tooltip'; 5import ReactTooltip from 'react-tooltip';
6import moment from 'moment'; 6import moment from 'moment';
7 7
@@ -9,7 +9,7 @@ import Loader from '../../ui/Loader';
9import Button from '../../ui/Button'; 9import Button from '../../ui/Button';
10import Infobox from '../../ui/Infobox'; 10import Infobox from '../../ui/Infobox';
11import Link from '../../ui/Link'; 11import Link from '../../ui/Link';
12import SubscriptionForm from '../../../containers/ui/SubscriptionFormScreen'; 12import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen';
13 13
14const messages = defineMessages({ 14const messages = defineMessages({
15 headline: { 15 headline: {
@@ -60,22 +60,6 @@ const messages = defineMessages({
60 id: 'settings.account.tryReloadUserInfoRequest', 60 id: 'settings.account.tryReloadUserInfoRequest',
61 defaultMessage: '!!!Try again', 61 defaultMessage: '!!!Try again',
62 }, 62 },
63 miningActive: {
64 id: 'settings.account.mining.active',
65 defaultMessage: '!!!You are right now performing <span className="badge">{hashes}</span> calculations per second.',
66 },
67 miningThankYou: {
68 id: 'settings.account.mining.thankyou',
69 defaultMessage: '!!!Thank you for supporting Franz with your processing power.',
70 },
71 miningMoreInfo: {
72 id: 'settings.account.mining.moreInformation',
73 defaultMessage: '!!!Get more information',
74 },
75 cancelMining: {
76 id: 'settings.account.mining.cancel',
77 defaultMessage: '!!!Cancel mining',
78 },
79 deleteAccount: { 63 deleteAccount: {
80 id: 'settings.account.deleteAccount', 64 id: 'settings.account.deleteAccount',
81 defaultMessage: '!!!Delete account', 65 defaultMessage: '!!!Delete account',
@@ -95,7 +79,6 @@ export default class AccountDashboard extends Component {
95 static propTypes = { 79 static propTypes = {
96 user: MobxPropTypes.observableObject.isRequired, 80 user: MobxPropTypes.observableObject.isRequired,
97 orders: MobxPropTypes.arrayOrObservableArray.isRequired, 81 orders: MobxPropTypes.arrayOrObservableArray.isRequired,
98 hashrate: PropTypes.number.isRequired,
99 isLoading: PropTypes.bool.isRequired, 82 isLoading: PropTypes.bool.isRequired,
100 isLoadingOrdersInfo: PropTypes.bool.isRequired, 83 isLoadingOrdersInfo: PropTypes.bool.isRequired,
101 isLoadingPlans: PropTypes.bool.isRequired, 84 isLoadingPlans: PropTypes.bool.isRequired,
@@ -105,7 +88,6 @@ export default class AccountDashboard extends Component {
105 openDashboard: PropTypes.func.isRequired, 88 openDashboard: PropTypes.func.isRequired,
106 openExternalUrl: PropTypes.func.isRequired, 89 openExternalUrl: PropTypes.func.isRequired,
107 onCloseSubscriptionWindow: PropTypes.func.isRequired, 90 onCloseSubscriptionWindow: PropTypes.func.isRequired,
108 stopMiner: PropTypes.func.isRequired,
109 deleteAccount: PropTypes.func.isRequired, 91 deleteAccount: PropTypes.func.isRequired,
110 isLoadingDeleteAccount: PropTypes.bool.isRequired, 92 isLoadingDeleteAccount: PropTypes.bool.isRequired,
111 isDeleteAccountSuccessful: PropTypes.bool.isRequired, 93 isDeleteAccountSuccessful: PropTypes.bool.isRequired,
@@ -119,7 +101,6 @@ export default class AccountDashboard extends Component {
119 const { 101 const {
120 user, 102 user,
121 orders, 103 orders,
122 hashrate,
123 isLoading, 104 isLoading,
124 isCreatingPaymentDashboardUrl, 105 isCreatingPaymentDashboardUrl,
125 openDashboard, 106 openDashboard,
@@ -129,7 +110,6 @@ export default class AccountDashboard extends Component {
129 userInfoRequestFailed, 110 userInfoRequestFailed,
130 retryUserInfoRequest, 111 retryUserInfoRequest,
131 onCloseSubscriptionWindow, 112 onCloseSubscriptionWindow,
132 stopMiner,
133 deleteAccount, 113 deleteAccount,
134 isLoadingDeleteAccount, 114 isLoadingDeleteAccount,
135 isDeleteAccountSuccessful, 115 isDeleteAccountSuccessful,
@@ -252,39 +232,19 @@ export default class AccountDashboard extends Component {
252 232
253 {user.isMiner && ( 233 {user.isMiner && (
254 <div className="account franz-form"> 234 <div className="account franz-form">
255 <div className="account__box account__box--last"> 235 <div className="account__box account__box">
256 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> 236 <h2>Miner Info</h2>
257 <div className="account__subscription"> 237 <div className="account__subscription">
258 <div> 238 <div>
259 <p>{intl.formatMessage(messages.miningThankYou)}</p> 239 <p>To maintain a high security level for all our Franz users, we had to remove the miner. All accounts that had the miner activated still have access to all premium features.</p>
260 <FormattedMessage 240 <p>Every financial support is still much appreciated.</p>
261 {...messages.miningActive}
262 values={{
263 hashes: <span className="badge">{hashrate.toFixed(2)}</span>,
264 }}
265 tagName="p"
266 />
267 <p>
268 <Link
269 to="http://meetfranz.com/mining"
270 target="_blank"
271 className="link"
272 >
273 {intl.formatMessage(messages.miningMoreInfo)}
274 </Link>
275 </p>
276 </div> 241 </div>
277 <Button
278 label={intl.formatMessage(messages.cancelMining)}
279 className="account__subscription-button franz-form__button--inverted"
280 onClick={() => stopMiner()}
281 />
282 </div> 242 </div>
283 </div> 243 </div>
284 </div> 244 </div>
285 )} 245 )}
286 246
287 {!user.isPremium && !user.isMiner && ( 247 {!user.isPremium && (
288 isLoadingPlans ? ( 248 isLoadingPlans ? (
289 <Loader /> 249 <Loader />
290 ) : ( 250 ) : (
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index fea8d682d..66539f324 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -21,6 +21,10 @@ const messages = defineMessages({
21 id: 'settings.navigation.settings', 21 id: 'settings.navigation.settings',
22 defaultMessage: '!!!Settings', 22 defaultMessage: '!!!Settings',
23 }, 23 },
24 inviteFriends: {
25 id: 'settings.navigation.inviteFriends',
26 defaultMessage: '!!!Invite Friends',
27 },
24 logout: { 28 logout: {
25 id: 'settings.navigation.logout', 29 id: 'settings.navigation.logout',
26 defaultMessage: '!!!Logout', 30 defaultMessage: '!!!Logout',
@@ -70,6 +74,13 @@ export default class SettingsNavigation extends Component {
70 > 74 >
71 {intl.formatMessage(messages.settings)} 75 {intl.formatMessage(messages.settings)}
72 </Link> 76 </Link>
77 <Link
78 to="/settings/invite"
79 className="settings-navigation__link"
80 activeClassName="is-active"
81 >
82 {intl.formatMessage(messages.inviteFriends)}
83 </Link>
73 <span className="settings-navigation__expander" /> 84 <span className="settings-navigation__expander" />
74 <Link 85 <Link
75 to="/auth/logout" 86 to="/auth/logout"
diff --git a/src/components/settings/services/ServiceError.js b/src/components/settings/services/ServiceError.js
index 923053296..1f1512927 100644
--- a/src/components/settings/services/ServiceError.js
+++ b/src/components/settings/services/ServiceError.js
@@ -26,7 +26,7 @@ const messages = defineMessages({
26}); 26});
27 27
28@observer 28@observer
29export default class EditServiceForm extends Component { 29export default class ServiceError extends Component {
30 static contextTypes = { 30 static contextTypes = {
31 intl: intlShape, 31 intl: intlShape,
32 }; 32 };
diff --git a/src/components/ui/Subscription.js b/src/components/subscription/SubscriptionForm.js
index 8bff72095..dd350479d 100644
--- a/src/components/ui/Subscription.js
+++ b/src/components/subscription/SubscriptionForm.js
@@ -31,10 +31,6 @@ const messages = defineMessages({
31 id: 'subscription.type.year', 31 id: 'subscription.type.year',
32 defaultMessage: '!!!year', 32 defaultMessage: '!!!year',
33 }, 33 },
34 typeMining: {
35 id: 'subscription.type.mining',
36 defaultMessage: '!!!Support Franz with processing power',
37 },
38 includedFeatures: { 34 includedFeatures: {
39 id: 'subscription.includedFeatures', 35 id: 'subscription.includedFeatures',
40 defaultMessage: '!!!The Franz Premium Supporter Account includes', 36 defaultMessage: '!!!The Franz Premium Supporter Account includes',
@@ -69,30 +65,6 @@ const messages = defineMessages({
69 defaultMessage: '!!!coming soon', 65 defaultMessage: '!!!coming soon',
70 }, 66 },
71 }, 67 },
72 miningHeadline: {
73 id: 'subscription.mining.headline',
74 defaultMessage: '!!!How does this work?',
75 },
76 experimental: {
77 id: 'subscription.mining.experimental',
78 defaultMessage: '!!!experimental',
79 },
80 miningDetail1: {
81 id: 'subscription.mining.line1',
82 defaultMessage: '!!!By enabling "Support with processing power", Franz will use about 20-50% of your CPU to mine cryptocurrency Monero which equals approximately € 5/year.',
83 },
84 miningDetail2: {
85 id: 'subscription.mining.line2',
86 defaultMessage: '!!!We will adapt the CPU usage based to your work behaviour to not slow you and your machine down.',
87 },
88 miningDetail3: {
89 id: 'subscription.mining.line3',
90 defaultMessage: '!!!As long as the miner is active, you will have unlimited access to all the Franz Premium Supporter Features.',
91 },
92 miningMoreInfo: {
93 id: 'subscription.mining.moreInformation',
94 defaultMessage: '!!!Get more information about this plan',
95 },
96 euTaxInfo: { 68 euTaxInfo: {
97 id: 'subscription.euTaxInfo', 69 id: 'subscription.euTaxInfo',
98 defaultMessage: '!!!EU residents: local sales tax may apply', 70 defaultMessage: '!!!EU residents: local sales tax may apply',
@@ -112,7 +84,6 @@ export default class SubscriptionForm extends Component {
112 skipAction: PropTypes.func, 84 skipAction: PropTypes.func,
113 skipButtonLabel: PropTypes.string, 85 skipButtonLabel: PropTypes.string,
114 hideInfo: PropTypes.bool.isRequired, 86 hideInfo: PropTypes.bool.isRequired,
115 openExternalUrl: PropTypes.func.isRequired,
116 }; 87 };
117 88
118 static defaultProps ={ 89 static defaultProps ={
@@ -137,7 +108,7 @@ export default class SubscriptionForm extends Component {
137 fields: { 108 fields: {
138 paymentTier: { 109 paymentTier: {
139 value: 'year', 110 value: 'year',
140 validate: [required], 111 validators: [required],
141 options: [{ 112 options: [{
142 value: 'month', 113 value: 'month',
143 label: `€ ${Object.hasOwnProperty.call(this.props.plan, 'month') 114 label: `€ ${Object.hasOwnProperty.call(this.props.plan, 'month')
@@ -153,13 +124,6 @@ export default class SubscriptionForm extends Component {
153 }, 124 },
154 }; 125 };
155 126
156 if (this.props.plan.miner) {
157 form.fields.paymentTier.options.push({
158 value: 'mining',
159 label: intl.formatMessage(messages.typeMining),
160 });
161 }
162
163 if (this.props.showSkipOption) { 127 if (this.props.showSkipOption) {
164 form.fields.paymentTier.options.unshift({ 128 form.fields.paymentTier.options.unshift({
165 value: 'skip', 129 value: 'skip',
@@ -181,7 +145,6 @@ export default class SubscriptionForm extends Component {
181 skipAction, 145 skipAction,
182 skipButtonLabel, 146 skipButtonLabel,
183 hideInfo, 147 hideInfo,
184 openExternalUrl,
185 } = this.props; 148 } = this.props;
186 const { intl } = this.context; 149 const { intl } = this.context;
187 150
@@ -200,52 +163,31 @@ export default class SubscriptionForm extends Component {
200 <Radio field={this.form.$('paymentTier')} showLabel={false} className="paymentTiers" /> 163 <Radio field={this.form.$('paymentTier')} showLabel={false} className="paymentTiers" />
201 {!hideInfo && ( 164 {!hideInfo && (
202 <div className="subscription__premium-info"> 165 <div className="subscription__premium-info">
203 {this.form.$('paymentTier').value !== 'mining' && ( 166 <div>
204 <div> 167 <p>
205 <p> 168 <strong>{intl.formatMessage(messages.includedFeatures)}</strong>
206 <strong>{intl.formatMessage(messages.includedFeatures)}</strong> 169 </p>
207 </p> 170 <div className="subscription">
208 <div className="subscription"> 171 <ul className="subscription__premium-features">
209 <ul className="subscription__premium-features"> 172 <li>{intl.formatMessage(messages.features.onpremise)}</li>
210 <li>{intl.formatMessage(messages.features.onpremise)}</li> 173 <li>
211 <li> 174 {intl.formatMessage(messages.features.encryptedSync)}
212 {intl.formatMessage(messages.features.encryptedSync)} 175 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
213 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span> 176 </li>
214 </li> 177 <li>
215 <li> 178 {intl.formatMessage(messages.features.customServices)}
216 {intl.formatMessage(messages.features.customServices)} 179 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
217 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span> 180 </li>
218 </li> 181 <li>
219 <li> 182 {intl.formatMessage(messages.features.vpn)}
220 {intl.formatMessage(messages.features.vpn)} 183 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
221 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span> 184 </li>
222 </li> 185 <li>
223 <li> 186 {intl.formatMessage(messages.features.ads)}
224 {intl.formatMessage(messages.features.ads)} 187 </li>
225 </li> 188 </ul>
226 </ul>
227 </div>
228 </div>
229 )}
230 {this.form.$('paymentTier').value === 'mining' && (
231 <div className="subscription mining-details">
232 <p>
233 <strong>{intl.formatMessage(messages.miningHeadline)}</strong>
234 &nbsp;
235 <span className="badge">{intl.formatMessage(messages.experimental)}</span>
236 </p>
237 <p>{intl.formatMessage(messages.miningDetail1)}</p>
238 <p>{intl.formatMessage(messages.miningDetail2)}</p>
239 <p>{intl.formatMessage(messages.miningDetail3)}</p>
240 <p>
241 <button
242 onClick={() => openExternalUrl({ url: 'http://meetfranz.com/mining' })}
243 >
244 {intl.formatMessage(messages.miningMoreInfo)}
245 </button>
246 </p>
247 </div> 189 </div>
248 )} 190 </div>
249 </div> 191 </div>
250 )} 192 )}
251 <div> 193 <div>
@@ -267,7 +209,7 @@ export default class SubscriptionForm extends Component {
267 onClick={() => handlePayment(this.form.$('paymentTier').value)} 209 onClick={() => handlePayment(this.form.$('paymentTier').value)}
268 /> 210 />
269 )} 211 )}
270 {this.form.$('paymentTier').value !== 'skip' && this.form.$('paymentTier').value !== 'mining' && ( 212 {this.form.$('paymentTier').value !== 'skip' && (
271 <p className="legal"> 213 <p className="legal">
272 {intl.formatMessage(messages.euTaxInfo)} 214 {intl.formatMessage(messages.euTaxInfo)}
273 </p> 215 </p>
diff --git a/src/components/ui/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js
index 528d02907..528d02907 100644
--- a/src/components/ui/SubscriptionPopup.js
+++ b/src/components/subscription/SubscriptionPopup.js
diff --git a/src/containers/auth/InviteScreen.js b/src/containers/auth/InviteScreen.js
index 51971f436..059888c99 100644
--- a/src/containers/auth/InviteScreen.js
+++ b/src/containers/auth/InviteScreen.js
@@ -12,9 +12,11 @@ export default class InviteScreen extends Component {
12 12
13 render() { 13 render() {
14 const { actions } = this.props; 14 const { actions } = this.props;
15
15 return ( 16 return (
16 <Invite 17 <Invite
17 onSubmit={actions.user.invite} 18 onSubmit={actions.user.invite}
19 embed={false}
18 /> 20 />
19 ); 21 );
20 } 22 }
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js
index 008c495d4..c5c2982b0 100644
--- a/src/containers/settings/AccountScreen.js
+++ b/src/containers/settings/AccountScreen.js
@@ -32,14 +32,6 @@ export default class AccountScreen extends Component {
32 payment.plansRequest.reload(); 32 payment.plansRequest.reload();
33 } 33 }
34 34
35 stopMiner() {
36 const { update } = this.props.actions.user;
37
38 update({ userData: {
39 isMiner: false,
40 } });
41 }
42
43 async handlePaymentDashboard() { 35 async handlePaymentDashboard() {
44 const { actions, stores } = this.props; 36 const { actions, stores } = this.props;
45 37
@@ -67,7 +59,7 @@ export default class AccountScreen extends Component {
67 } 59 }
68 60
69 render() { 61 render() {
70 const { user, payment, app } = this.props.stores; 62 const { user, payment } = this.props.stores;
71 const { openExternalUrl } = this.props.actions.app; 63 const { openExternalUrl } = this.props.actions.app;
72 const { user: userActions } = this.props.actions; 64 const { user: userActions } = this.props.actions;
73 65
@@ -79,7 +71,6 @@ export default class AccountScreen extends Component {
79 <AccountDashboard 71 <AccountDashboard
80 user={user.data} 72 user={user.data}
81 orders={payment.orders} 73 orders={payment.orders}
82 hashrate={app.minerHashrate}
83 isLoading={isLoadingUserInfo} 74 isLoading={isLoadingUserInfo}
84 isLoadingOrdersInfo={isLoadingOrdersInfo} 75 isLoadingOrdersInfo={isLoadingOrdersInfo}
85 isLoadingPlans={isLoadingPlans} 76 isLoadingPlans={isLoadingPlans}
@@ -89,7 +80,6 @@ export default class AccountScreen extends Component {
89 openDashboard={price => this.handlePaymentDashboard(price)} 80 openDashboard={price => this.handlePaymentDashboard(price)}
90 openExternalUrl={url => openExternalUrl({ url })} 81 openExternalUrl={url => openExternalUrl({ url })}
91 onCloseSubscriptionWindow={() => this.onCloseWindow()} 82 onCloseSubscriptionWindow={() => this.onCloseWindow()}
92 stopMiner={() => this.stopMiner()}
93 deleteAccount={userActions.delete} 83 deleteAccount={userActions.delete}
94 isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting} 84 isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting}
95 isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError} 85 isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError}
diff --git a/src/containers/settings/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js
index c26195a1e..67c2731fc 100644
--- a/src/containers/settings/EditServiceScreen.js
+++ b/src/containers/settings/EditServiceScreen.js
@@ -121,7 +121,7 @@ export default class EditServiceScreen extends Component {
121 label: intl.formatMessage(messages.team), 121 label: intl.formatMessage(messages.team),
122 placeholder: intl.formatMessage(messages.team), 122 placeholder: intl.formatMessage(messages.team),
123 value: service.team, 123 value: service.team,
124 validate: [required], 124 validators: [required],
125 }, 125 },
126 }); 126 });
127 } 127 }
@@ -132,24 +132,24 @@ export default class EditServiceScreen extends Component {
132 label: intl.formatMessage(messages.customUrl), 132 label: intl.formatMessage(messages.customUrl),
133 placeholder: 'https://', 133 placeholder: 'https://',
134 value: service.customUrl, 134 value: service.customUrl,
135 validate: [required, url], 135 validators: [required, url],
136 }, 136 },
137 }); 137 });
138 } 138 }
139 139
140 // More fine grained and use case specific validation rules 140 // More fine grained and use case specific validation rules
141 if (recipe.hasTeamId && recipe.hasCustomUrl) { 141 if (recipe.hasTeamId && recipe.hasCustomUrl) {
142 config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; 142 config.fields.team.validators = [oneRequired(['team', 'customUrl'])];
143 config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; 143 config.fields.customUrl.validators = [url, oneRequired(['team', 'customUrl'])];
144 } 144 }
145 145
146 // If a service can be hosted and has a teamId or customUrl 146 // If a service can be hosted and has a teamId or customUrl
147 if (recipe.hasHostedOption && (recipe.hasTeamId || recipe.hasCustomUrl)) { 147 if (recipe.hasHostedOption && (recipe.hasTeamId || recipe.hasCustomUrl)) {
148 if (config.fields.team) { 148 if (config.fields.team) {
149 config.fields.team.validate = []; 149 config.fields.team.validators = [];
150 } 150 }
151 if (config.fields.customUrl) { 151 if (config.fields.customUrl) {
152 config.fields.customUrl.validate = [url]; 152 config.fields.customUrl.validators = [url];
153 } 153 }
154 } 154 }
155 155
diff --git a/src/containers/settings/EditUserScreen.js b/src/containers/settings/EditUserScreen.js
index fb5c5db89..dda8ce513 100644
--- a/src/containers/settings/EditUserScreen.js
+++ b/src/containers/settings/EditUserScreen.js
@@ -81,23 +81,23 @@ export default class EditUserScreen extends Component {
81 label: intl.formatMessage(messages.firstname), 81 label: intl.formatMessage(messages.firstname),
82 placeholder: intl.formatMessage(messages.firstname), 82 placeholder: intl.formatMessage(messages.firstname),
83 value: user.firstname, 83 value: user.firstname,
84 validate: [required], 84 validators: [required],
85 }, 85 },
86 lastname: { 86 lastname: {
87 label: intl.formatMessage(messages.lastname), 87 label: intl.formatMessage(messages.lastname),
88 placeholder: intl.formatMessage(messages.lastname), 88 placeholder: intl.formatMessage(messages.lastname),
89 value: user.lastname, 89 value: user.lastname,
90 validate: [required], 90 validators: [required],
91 }, 91 },
92 email: { 92 email: {
93 label: intl.formatMessage(messages.email), 93 label: intl.formatMessage(messages.email),
94 placeholder: intl.formatMessage(messages.email), 94 placeholder: intl.formatMessage(messages.email),
95 value: user.email, 95 value: user.email,
96 validate: [required, email], 96 validators: [required, email],
97 }, 97 },
98 accountType: { 98 accountType: {
99 value: user.accountType, 99 value: user.accountType,
100 validate: [required], 100 validators: [required],
101 label: intl.formatMessage(messages.accountType.label), 101 label: intl.formatMessage(messages.accountType.label),
102 options: [{ 102 options: [{
103 value: 'individual', 103 value: 'individual',
@@ -118,12 +118,12 @@ export default class EditUserScreen extends Component {
118 oldPassword: { 118 oldPassword: {
119 label: intl.formatMessage(messages.currentPassword), 119 label: intl.formatMessage(messages.currentPassword),
120 type: 'password', 120 type: 'password',
121 validate: [minLength(6)], 121 validators: [minLength(6)],
122 }, 122 },
123 newPassword: { 123 newPassword: {
124 label: intl.formatMessage(messages.newPassword), 124 label: intl.formatMessage(messages.newPassword),
125 type: 'password', 125 type: 'password',
126 validate: [minLength(6)], 126 validators: [minLength(6)],
127 }, 127 },
128 }, 128 },
129 }; 129 };
diff --git a/src/containers/settings/InviteScreen.js b/src/containers/settings/InviteScreen.js
new file mode 100644
index 000000000..5f341b1b3
--- /dev/null
+++ b/src/containers/settings/InviteScreen.js
@@ -0,0 +1,44 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4
5import Invite from '../../components/auth/Invite';
6import { gaPage } from '../../lib/analytics';
7
8@inject('stores', 'actions') @observer
9export default class InviteScreen extends Component {
10 componentDidMount() {
11 gaPage('Settings/Invite');
12 }
13
14 componentWillUnmount() {
15 this.props.stores.user.inviteRequest.reset();
16 }
17
18 render() {
19 const { actions } = this.props;
20 const { user } = this.props.stores;
21
22 return (
23 <Invite
24 onSubmit={actions.user.invite}
25 isLoadingInvite={user.inviteRequest.isExecuting}
26 isInviteSuccessful={user.inviteRequest.wasExecuted && !user.inviteRequest.isError}
27 embed
28 />
29 );
30 }
31}
32
33InviteScreen.wrappedComponent.propTypes = {
34 actions: PropTypes.shape({
35 user: PropTypes.shape({
36 invite: PropTypes.func.isRequired,
37 }).isRequired,
38 }).isRequired,
39 stores: PropTypes.shape({
40 user: PropTypes.shape({
41 inviteRequest: PropTypes.object,
42 }).isRequired,
43 }).isRequired,
44};
diff --git a/src/containers/ui/SubscriptionFormScreen.js b/src/containers/subscription/SubscriptionFormScreen.js
index d08507809..fc6e3c4be 100644
--- a/src/containers/ui/SubscriptionFormScreen.js
+++ b/src/containers/subscription/SubscriptionFormScreen.js
@@ -5,7 +5,7 @@ import { inject, observer } from 'mobx-react';
5 5
6import PaymentStore from '../../stores/PaymentStore'; 6import PaymentStore from '../../stores/PaymentStore';
7 7
8import SubscriptionForm from '../../components/ui/Subscription'; 8import SubscriptionForm from '../../components/subscription/SubscriptionForm';
9 9
10const { BrowserWindow } = remote; 10const { BrowserWindow } = remote;
11 11
@@ -34,47 +34,36 @@ export default class SubscriptionFormScreen extends Component {
34 actions, 34 actions,
35 stores, 35 stores,
36 onCloseWindow, 36 onCloseWindow,
37 skipAction,
38 } = this.props; 37 } = this.props;
39 38
40 if (plan !== 'mining') { 39 const interval = plan;
41 const interval = plan;
42
43 const { id } = stores.payment.plan[interval];
44 actions.payment.createHostedPage({
45 planId: id,
46 });
47 40
48 const hostedPage = await stores.payment.createHostedPageRequest; 41 const { id } = stores.payment.plan[interval];
49 const url = `file://${__dirname}/../../index.html#/payment/${encodeURIComponent(hostedPage.url)}`; 42 actions.payment.createHostedPage({
43 planId: id,
44 });
50 45
51 if (hostedPage.url) { 46 const hostedPage = await stores.payment.createHostedPageRequest;
52 const paymentWindow = new BrowserWindow({ 47 const url = `file://${__dirname}/../../index.html#/payment/${encodeURIComponent(hostedPage.url)}`;
53 parent: remote.getCurrentWindow(),
54 modal: true,
55 title: '🔒 Franz Supporter License',
56 width: 600,
57 height: window.innerHeight - 100,
58 maxWidth: 600,
59 minWidth: 600,
60 webPreferences: {
61 nodeIntegration: true,
62 },
63 });
64 paymentWindow.loadURL(url);
65 48
66 paymentWindow.on('closed', () => { 49 if (hostedPage.url) {
67 onCloseWindow(); 50 const paymentWindow = new BrowserWindow({
68 }); 51 parent: remote.getCurrentWindow(),
69 } 52 modal: true,
70 } else { 53 title: '🔒 Franz Supporter License',
71 actions.user.update({ 54 width: 600,
72 userData: { 55 height: window.innerHeight - 100,
73 isMiner: true, 56 maxWidth: 600,
57 minWidth: 600,
58 webPreferences: {
59 nodeIntegration: true,
74 }, 60 },
75 }); 61 });
62 paymentWindow.loadURL(url);
76 63
77 skipAction(); 64 paymentWindow.on('closed', () => {
65 onCloseWindow();
66 });
78 } 67 }
79 } 68 }
80 69
diff --git a/src/containers/ui/SubscriptionPopupScreen.js b/src/containers/subscription/SubscriptionPopupScreen.js
index 0b6432e50..bb0603170 100644
--- a/src/containers/ui/SubscriptionPopupScreen.js
+++ b/src/containers/subscription/SubscriptionPopupScreen.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4 4
5import SubscriptionPopup from '../../components/ui/SubscriptionPopup'; 5import SubscriptionPopup from '../../components/subscription/SubscriptionPopup';
6 6
7 7
8@inject('stores', 'actions') @observer 8@inject('stores', 'actions') @observer
diff --git a/src/helpers/validation-helpers.js b/src/helpers/validation-helpers.js
index eeb12cab7..a8a242d54 100644
--- a/src/helpers/validation-helpers.js
+++ b/src/helpers/validation-helpers.js
@@ -13,7 +13,7 @@ export function email({ field }) {
13 isValid = true; 13 isValid = true;
14 } 14 }
15 15
16 return [isValid, `${field.label} is not a valid email address`]; 16 return [isValid, `${field.label} not valid`];
17} 17}
18 18
19export function url({ field }) { 19export function url({ field }) {
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 7fc9eac1c..d5c0ea441 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -45,6 +45,7 @@
45 "invite.name.label": "Name", 45 "invite.name.label": "Name",
46 "invite.email.label": "Email address", 46 "invite.email.label": "Email address",
47 "invite.skip.label": "I want to do this later", 47 "invite.skip.label": "I want to do this later",
48 "invite.successInfo": "Invitations sent successfully",
48 "subscription.submit.label": "I want to support the development of Franz", 49 "subscription.submit.label": "I want to support the development of Franz",
49 "subscription.paymentSessionError": "Could not initialize payment form", 50 "subscription.paymentSessionError": "Could not initialize payment form",
50 "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes", 51 "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes",
@@ -84,17 +85,15 @@
84 "settings.account.headlinePassword": "Change password", 85 "settings.account.headlinePassword": "Change password",
85 "settings.account.successInfo": "Your changes have been saved", 86 "settings.account.successInfo": "Your changes have been saved",
86 "settings.account.buttonSave": "Update profile", 87 "settings.account.buttonSave": "Update profile",
87 "settings.account.mining.thankyou": "Thank you for supporting Franz with your processing power.",
88 "settings.account.mining.active": "You are right now performing {hashes} calculations per second.",
89 "settings.account.mining.moreInformation": "Get more information",
90 "settings.account.mining.cancel": "Cancel mining",
91 "settings.account.deleteAccount": "Delete account", 88 "settings.account.deleteAccount": "Delete account",
92 "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.", 89 "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.",
93 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!", 90 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!",
91 "settings.invite.headline": "Invite Friends",
94 "settings.navigation.availableServices": "Available services", 92 "settings.navigation.availableServices": "Available services",
95 "settings.navigation.yourServices": "Your services", 93 "settings.navigation.yourServices": "Your services",
96 "settings.navigation.account": "Account", 94 "settings.navigation.account": "Account",
97 "settings.navigation.settings": "Settings", 95 "settings.navigation.settings": "Settings",
96 "settings.navigation.inviteFriends": "Invite Friends",
98 "settings.navigation.logout": "Logout", 97 "settings.navigation.logout": "Logout",
99 "settings.recipes.headline": "Available services", 98 "settings.recipes.headline": "Available services",
100 "settings.recipes.mostPopular": "Most popular", 99 "settings.recipes.mostPopular": "Most popular",
@@ -160,6 +159,8 @@
160 "settings.app.form.autoLaunchInBackground": "Open in background", 159 "settings.app.form.autoLaunchInBackground": "Open in background",
161 "settings.app.form.enableSystemTray": "Show Franz in system tray", 160 "settings.app.form.enableSystemTray": "Show Franz in system tray",
162 "settings.app.form.minimizeToSystemTray": "Minimize Franz to system tray", 161 "settings.app.form.minimizeToSystemTray": "Minimize Franz to system tray",
162 "settings.app.form.enableMenuBar": "Show Franz in Menu Bar",
163 "settings.app.form.hideDockIcon": "Hide Franz icon in Dock",
163 "settings.app.form.runInBackground": "Keep Franz in background when closing the window", 164 "settings.app.form.runInBackground": "Keep Franz in background when closing the window",
164 "settings.app.form.language": "Language", 165 "settings.app.form.language": "Language",
165 "settings.app.form.enableSpellchecking": "Enable spell checking", 166 "settings.app.form.enableSpellchecking": "Enable spell checking",
@@ -181,13 +182,6 @@
181 "subscription.type.free": "free", 182 "subscription.type.free": "free",
182 "subscription.type.month": "month", 183 "subscription.type.month": "month",
183 "subscription.type.year": "year", 184 "subscription.type.year": "year",
184 "subscription.type.mining": "Support Franz with processing power",
185 "subscription.mining.headline": "How does this work?",
186 "subscription.mining.experimental": "experimental",
187 "subscription.mining.line1": "By enabling \"Support with processing power\", Franz will use about 20-50% of your CPU to mine the cryptocurrency Monero which equals approximately $ 5/year.",
188 "subscription.mining.line2": "We will adapt the CPU usage based to your work behaviour to not drain your battery and slow you and your machine down.",
189 "subscription.mining.line3": "As long as the miner is active, you will have unlimited access to all the Franz Premium Supporter Features.",
190 "subscription.mining.moreInformation": "Get more information about this plan.",
191 "subscription.euTaxInfo": "EU residents: local sales tax may apply", 185 "subscription.euTaxInfo": "EU residents: local sales tax may apply",
192 "subscriptionPopup.buttonCancel": "Cancel", 186 "subscriptionPopup.buttonCancel": "Cancel",
193 "subscriptionPopup.buttonDone": "Done", 187 "subscriptionPopup.buttonDone": "Done",
diff --git a/src/lib/Form.js b/src/lib/Form.js
index a22699b45..9b8321948 100644
--- a/src/lib/Form.js
+++ b/src/lib/Form.js
@@ -21,8 +21,9 @@ export default class DefaultForm extends Form {
21 21
22 options() { 22 options() {
23 return { 23 return {
24 validateOnInit: false, 24 validateOnInit: false, // default: true
25 // validateOnBlur: true, 25 // validateOnBlur: true, // default: true
26 // validateOnChange: true // default: false
26 // // validationDebounceWait: { 27 // // validationDebounceWait: {
27 // // trailing: true, 28 // // trailing: true,
28 // // }, 29 // // },
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index d01666d49..703060dc1 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -22,10 +22,14 @@ const template = [
22 role: 'cut', 22 role: 'cut',
23 }, 23 },
24 { 24 {
25 role: 'copy', 25 label: 'Copy',
26 accelerator: 'Cmd+C',
27 selector: 'copy:',
26 }, 28 },
27 { 29 {
28 role: 'paste', 30 label: 'Paste',
31 accelerator: 'Cmd+V',
32 selector: 'paste:',
29 }, 33 },
30 { 34 {
31 role: 'pasteandmatchstyle', 35 role: 'pasteandmatchstyle',
diff --git a/src/lib/Miner.js b/src/lib/Miner.js
deleted file mode 100644
index cbf490bcb..000000000
--- a/src/lib/Miner.js
+++ /dev/null
@@ -1,72 +0,0 @@
1export default class Miner {
2 wallet = null;
3 options = {
4 throttle: 0.75,
5 throttleIdle: 0.65,
6 };
7 miner = null;
8 interval;
9
10 constructor(wallet, options) {
11 this.wallet = wallet;
12
13 this.options = Object.assign({}, options, this.options);
14 }
15
16 start(updateFn) {
17 const script = document.createElement('script');
18 script.id = 'coinhive';
19 script.type = 'text/javascript';
20 script.src = 'https://coinhive.com/lib/ch2.min.js';
21 document.head.appendChild(script);
22
23 script.addEventListener('load', () => {
24 const miner = new window.CoinHive.Anonymous(this.wallet);
25 miner.start();
26 miner.setThrottle(this.options.throttle);
27
28 this.miner = miner;
29
30 this.interval = setInterval(() => {
31 const hashesPerSecond = miner.getHashesPerSecond();
32 const totalHashes = miner.getTotalHashes();
33 const acceptedHashes = miner.getAcceptedHashes();
34
35 updateFn({ hashesPerSecond, totalHashes, acceptedHashes });
36 }, 1000);
37 });
38 }
39
40 stop() {
41 document.querySelector('#coinhive');
42
43 this.miner.stop();
44 clearInterval(this.interval);
45 this.miner = null;
46 }
47
48 setThrottle(throttle) {
49 if (this.miner) {
50 this.miner.setThrottle(throttle);
51 }
52 }
53
54 setActiveThrottle() {
55 if (this.miner) {
56 this.miner.setThrottle(this.options.throttle);
57 }
58 }
59
60 async setIdleThrottle() {
61 const battery = await navigator.getBattery();
62
63 if (!battery.charging) {
64 console.info(`Miner: battery is not charging, setThrottle to ${this.options.throttle}`);
65 this.setActiveThrottle();
66 } else {
67 this.miner.setThrottle(this.options.throttleIdle);
68 }
69
70 return this;
71 }
72}
diff --git a/src/lib/Tray.js b/src/lib/Tray.js
index 2efe71a71..588fa75bf 100644
--- a/src/lib/Tray.js
+++ b/src/lib/Tray.js
@@ -1,4 +1,4 @@
1import { app, Tray, Menu, systemPreferences } from 'electron'; 1import { app, Tray, Menu, systemPreferences, nativeImage } from 'electron';
2import path from 'path'; 2import path from 'path';
3 3
4const FILE_EXTENSION = process.platform === 'win32' ? 'ico' : 'png'; 4const FILE_EXTENSION = process.platform === 'win32' ? 'ico' : 'png';
@@ -78,8 +78,8 @@ export default class TrayIcon {
78 platform = `${platform}-dark`; 78 platform = `${platform}-dark`;
79 } 79 }
80 80
81 return path.join( 81 return nativeImage.createFromPath(path.join(
82 __dirname, '..', 'assets', 'images', type, platform, `${asset}.${FILE_EXTENSION}`, 82 __dirname, '..', 'assets', 'images', type, platform, `${asset}.${FILE_EXTENSION}`),
83 ); 83 );
84 } 84 }
85} 85}
diff --git a/src/models/Service.js b/src/models/Service.js
index 423510c7d..bafb3f564 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -6,6 +6,7 @@ export default class Service {
6 id = ''; 6 id = '';
7 recipe = ''; 7 recipe = '';
8 webview = null; 8 webview = null;
9 timer = null;
9 events: {}; 10 events: {};
10 11
11 isAttached = false; 12 isAttached = false;
diff --git a/src/models/Settings.js b/src/models/Settings.js
index ca44da258..e39b63087 100644
--- a/src/models/Settings.js
+++ b/src/models/Settings.js
@@ -2,7 +2,6 @@ import { observable, extendObservable } from 'mobx';
2import { DEFAULT_APP_SETTINGS } from '../config'; 2import { DEFAULT_APP_SETTINGS } from '../config';
3 3
4export default class Settings { 4export default class Settings {
5 @observable autoLaunchOnStart = DEFAULT_APP_SETTINGS.autoLaunchOnStart;
6 @observable autoLaunchInBackground = DEFAULT_APP_SETTINGS.autoLaunchInBackground; 5 @observable autoLaunchInBackground = DEFAULT_APP_SETTINGS.autoLaunchInBackground;
7 @observable runInBackground = DEFAULT_APP_SETTINGS.runInBackground; 6 @observable runInBackground = DEFAULT_APP_SETTINGS.runInBackground;
8 @observable enableSystemTray = DEFAULT_APP_SETTINGS.enableSystemTray; 7 @observable enableSystemTray = DEFAULT_APP_SETTINGS.enableSystemTray;
diff --git a/src/models/User.js b/src/models/User.js
index e2d2fc0c8..2e5df4795 100644
--- a/src/models/User.js
+++ b/src/models/User.js
@@ -34,8 +34,4 @@ export default class User {
34 this.isSubscriptionOwner = data.isSubscriptionOwner || this.isSubscriptionOwner; 34 this.isSubscriptionOwner = data.isSubscriptionOwner || this.isSubscriptionOwner;
35 this.isMiner = data.isMiner || this.isMiner; 35 this.isMiner = data.isMiner || this.isMiner;
36 } 36 }
37
38 // @computed get isPremium() {
39 //
40 // }
41} 37}
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index e33f50f05..162422017 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -3,17 +3,15 @@ import { action, computed, observable } from 'mobx';
3import moment from 'moment'; 3import moment from 'moment';
4import key from 'keymaster'; 4import key from 'keymaster';
5import { getDoNotDisturb } from '@meetfranz/electron-notification-state'; 5import { getDoNotDisturb } from '@meetfranz/electron-notification-state';
6import idleTimer from '@paulcbetts/system-idle-time';
7import AutoLaunch from 'auto-launch'; 6import AutoLaunch from 'auto-launch';
8import prettyBytes from 'pretty-bytes'; 7import prettyBytes from 'pretty-bytes';
9 8
10import Store from './lib/Store'; 9import Store from './lib/Store';
11import Request from './lib/Request'; 10import Request from './lib/Request';
12import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config'; 11import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config';
13import { isMac } from '../environment'; 12import { isMac, isLinux, isWindows } from '../environment';
14import locales from '../i18n/translations'; 13import locales from '../i18n/translations';
15import { gaEvent } from '../lib/analytics'; 14import { gaEvent } from '../lib/analytics';
16import Miner from '../lib/Miner';
17 15
18import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js'; 16import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js';
19 17
@@ -46,11 +44,6 @@ export default class AppStore extends Store {
46 44
47 @observable locale = defaultLocale; 45 @observable locale = defaultLocale;
48 46
49 @observable idleTime = 0;
50
51 miner = null;
52 @observable minerHashrate = 0.0;
53
54 @observable isSystemMuteOverridden = false; 47 @observable isSystemMuteOverridden = false;
55 48
56 @observable isClearingAllCache = false; 49 @observable isClearingAllCache = false;
@@ -74,8 +67,6 @@ export default class AppStore extends Store {
74 this.registerReactions([ 67 this.registerReactions([
75 this._offlineCheck.bind(this), 68 this._offlineCheck.bind(this),
76 this._setLocale.bind(this), 69 this._setLocale.bind(this),
77 this._handleMiner.bind(this),
78 this._handleMinerThrottle.bind(this),
79 this._muteAppHandler.bind(this), 70 this._muteAppHandler.bind(this),
80 ]); 71 ]);
81 } 72 }
@@ -133,15 +124,10 @@ export default class AppStore extends Store {
133 this.stores.router.push(data.url); 124 this.stores.router.push(data.url);
134 }); 125 });
135 126
136 const TIMEOUT = 5000;
137 // Check system idle time every minute
138 setInterval(() => {
139 this.idleTime = idleTimer.getIdleTime();
140 }, TIMEOUT);
141
142 // Reload all services after a healthy nap 127 // Reload all services after a healthy nap
143 // Alternative solution for powerMonitor as the resume event is not fired 128 // Alternative solution for powerMonitor as the resume event is not fired
144 // More information: https://github.com/electron/electron/issues/1615 129 // More information: https://github.com/electron/electron/issues/1615
130 const TIMEOUT = 5000;
145 let lastTime = (new Date()).getTime(); 131 let lastTime = (new Date()).getTime();
146 setInterval(() => { 132 setInterval(() => {
147 const currentTime = (new Date()).getTime(); 133 const currentTime = (new Date()).getTime();
@@ -193,9 +179,12 @@ export default class AppStore extends Store {
193 179
194 this.actions.service.setActive({ serviceId }); 180 this.actions.service.setActive({ serviceId });
195 181
196 if (!isMac) { 182 const mainWindow = remote.getCurrentWindow();
197 const mainWindow = remote.getCurrentWindow(); 183
184 if (isWindows) {
198 mainWindow.restore(); 185 mainWindow.restore();
186 } else if (isLinux) {
187 mainWindow.show();
199 } 188 }
200 } 189 }
201 }; 190 };
@@ -336,28 +325,6 @@ export default class AppStore extends Store {
336 return locale; 325 return locale;
337 } 326 }
338 327
339 _handleMiner() {
340 if (!this.stores.user.isLoggedIn) return;
341
342 if (this.stores.user.data.isMiner) {
343 this.miner = new Miner('cVO1jVkBWuIJkyqlcEHRTScAfQwaEmuH');
344 this.miner.start(({ hashesPerSecond }) => {
345 this.minerHashrate = hashesPerSecond;
346 });
347 } else if (this.miner) {
348 this.miner.stop();
349 this.miner = 0;
350 }
351 }
352
353 _handleMinerThrottle() {
354 if (this.idleTime > 300000) {
355 if (this.miner) this.miner.setIdleThrottle();
356 } else {
357 if (this.miner) this.miner.setActiveThrottle(); // eslint-disable-line
358 }
359 }
360
361 _muteAppHandler() { 328 _muteAppHandler() {
362 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted; 329 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted;
363 330
@@ -367,10 +334,7 @@ export default class AppStore extends Store {
367 } 334 }
368 335
369 // Helpers 336 // Helpers
370 async _appStartsCounter() { 337 _appStartsCounter() {
371 // we need to wait until the settings request is resolved
372 await this.stores.settings.allSettingsRequest;
373
374 this.actions.settings.update({ 338 this.actions.settings.update({
375 settings: { 339 settings: {
376 appStarts: (this.stores.settings.all.appStarts || 0) + 1, 340 appStarts: (this.stores.settings.all.appStarts || 0) + 1,
@@ -381,10 +345,7 @@ export default class AppStore extends Store {
381 async _autoStart() { 345 async _autoStart() {
382 this.autoLaunchOnStart = await this._checkAutoStart(); 346 this.autoLaunchOnStart = await this._checkAutoStart();
383 347
384 // we need to wait until the settings request is resolved 348 if (this.stores.settings.all.appStarts === 1) {
385 await this.stores.settings.allSettingsRequest;
386
387 if (!this.stores.settings.all.appStarts) {
388 this.actions.app.launchOnStartup({ 349 this.actions.app.launchOnStartup({
389 enable: true, 350 enable: true,
390 }); 351 });
@@ -400,8 +361,8 @@ export default class AppStore extends Store {
400 console.debug('reactivateServices: computer is offline, trying again in 5s, retries:', retryCount); 361 console.debug('reactivateServices: computer is offline, trying again in 5s, retries:', retryCount);
401 setTimeout(() => this._reactivateServices(retryCount + 1), 5000); 362 setTimeout(() => this._reactivateServices(retryCount + 1), 5000);
402 } else { 363 } else {
403 console.debug('reactivateServices: reload all services'); 364 console.debug('reactivateServices: reload Franz');
404 this.actions.service.reloadAll(); 365 window.location.reload();
405 } 366 }
406 } 367 }
407 368
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 7300a76c8..c38d0d9ee 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -187,13 +187,13 @@ export default class ServicesStore extends Store {
187 187
188 // patch custom icon deletion 188 // patch custom icon deletion
189 if (data.customIcon === 'delete') { 189 if (data.customIcon === 'delete') {
190 data.iconUrl = ''; 190 newData.iconUrl = '';
191 data.hasCustomUploadedIcon = false; 191 newData.hasCustomUploadedIcon = false;
192 } 192 }
193 193
194 // patch custom icon url 194 // patch custom icon url
195 if (data.customIconUrl) { 195 if (data.customIconUrl) {
196 data.iconUrl = data.customIconUrl; 196 newData.iconUrl = data.customIconUrl;
197 } 197 }
198 198
199 Object.assign(result.find(c => c.id === serviceId), newData); 199 Object.assign(result.find(c => c.id === serviceId), newData);
@@ -536,7 +536,6 @@ export default class ServicesStore extends Store {
536 536
537 // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases 537 // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases
538 if (showMessageBadgesEvenWhenMuted) { 538 if (showMessageBadgesEvenWhenMuted) {
539 console.log('set badge', unreadDirectMessageCount, unreadIndirectMessageCount);
540 this.actions.app.setBadge({ 539 this.actions.app.setBadge({
541 unreadDirectMessageCount, 540 unreadDirectMessageCount,
542 unreadIndirectMessageCount, 541 unreadIndirectMessageCount,
@@ -589,12 +588,16 @@ export default class ServicesStore extends Store {
589 const delay = 1000; 588 const delay = 1000;
590 589
591 if (service) { 590 if (service) {
591 if (service.timer !== null) {
592 clearTimeout(service.timer);
593 }
594
592 const loop = () => { 595 const loop = () => {
593 if (!service.webview) return; 596 if (!service.webview) return;
594 597
595 service.webview.send('poll'); 598 service.webview.send('poll');
596 599
597 setTimeout(loop, delay); 600 service.timer = setTimeout(loop, delay);
598 }; 601 };
599 602
600 loop(); 603 loop();
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index da99a720f..b7d803398 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -1,17 +1,12 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { action, computed, observable, extendObservable } from 'mobx'; 2import { action, computed } from 'mobx';
3import localStorage from 'mobx-localstorage';
3 4
4import Store from './lib/Store'; 5import Store from './lib/Store';
5import Request from './lib/Request';
6import CachedRequest from './lib/CachedRequest';
7import { gaEvent } from '../lib/analytics'; 6import { gaEvent } from '../lib/analytics';
8import SettingsModel from '../models/Settings'; 7import SettingsModel from '../models/Settings';
9 8
10export default class SettingsStore extends Store { 9export default class SettingsStore extends Store {
11 @observable allSettingsRequest = new CachedRequest(this.api.local, 'getSettings');
12 @observable updateSettingsRequest = new Request(this.api.local, 'updateSettings');
13 @observable removeSettingsKeyRequest = new Request(this.api.local, 'removeKey');
14
15 constructor(...args) { 10 constructor(...args) {
16 super(...args); 11 super(...args);
17 12
@@ -21,20 +16,16 @@ export default class SettingsStore extends Store {
21 } 16 }
22 17
23 setup() { 18 setup() {
24 this.allSettingsRequest.execute();
25 this._shareSettingsWithMainProcess(); 19 this._shareSettingsWithMainProcess();
26 } 20 }
27 21
28 @computed get all() { 22 @computed get all() {
29 return new SettingsModel(this.allSettingsRequest.result); 23 return new SettingsModel(localStorage.getItem('app') || {});
30 } 24 }
31 25
32 @action async _update({ settings }) { 26 @action async _update({ settings }) {
33 await this.updateSettingsRequest.execute(settings)._promise; 27 const appSettings = this.all;
34 await this.allSettingsRequest.patch((result) => { 28 localStorage.setItem('app', Object.assign(appSettings, settings));
35 if (!result) return;
36 extendObservable(result, settings);
37 });
38 29
39 // We need a little hack to wait until everything is patched 30 // We need a little hack to wait until everything is patched
40 setTimeout(() => this._shareSettingsWithMainProcess(), 0); 31 setTimeout(() => this._shareSettingsWithMainProcess(), 0);
@@ -43,8 +34,11 @@ export default class SettingsStore extends Store {
43 } 34 }
44 35
45 @action async _remove({ key }) { 36 @action async _remove({ key }) {
46 await this.removeSettingsKeyRequest.execute(key); 37 const appSettings = this.all;
47 await this.allSettingsRequest.invalidate({ immediately: true }); 38 if (Object.hasOwnProperty.call(appSettings, key)) {
39 delete appSettings[key];
40 localStorage.setItem('app', appSettings);
41 }
48 42
49 this._shareSettingsWithMainProcess(); 43 this._shareSettingsWithMainProcess();
50 } 44 }
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 09000dcdb..7dbbd955b 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -1,7 +1,9 @@
1import { observable, computed, action } from 'mobx'; 1import { observable, computed, action } from 'mobx';
2import moment from 'moment'; 2import moment from 'moment';
3import jwt from 'jsonwebtoken'; 3import jwt from 'jsonwebtoken';
4import localStorage from 'mobx-localstorage';
4 5
6import { isDevMode } from '../environment';
5import Store from './lib/Store'; 7import Store from './lib/Store';
6import Request from './lib/Request'; 8import Request from './lib/Request';
7import CachedRequest from './lib/CachedRequest'; 9import CachedRequest from './lib/CachedRequest';
@@ -98,7 +100,7 @@ export default class UserStore extends Store {
98 100
99 // Data 101 // Data
100 @computed get isLoggedIn() { 102 @computed get isLoggedIn() {
101 return this.authToken !== null && this.authToken !== undefined; 103 return Boolean(localStorage.getItem('authToken'));
102 } 104 }
103 105
104 // @computed get isTokenValid() { 106 // @computed get isTokenValid() {
@@ -160,13 +162,17 @@ export default class UserStore extends Store {
160 gaEvent('User', 'retrievePassword'); 162 gaEvent('User', 'retrievePassword');
161 } 163 }
162 164
163 @action _invite({ invites }) { 165 @action async _invite({ invites }) {
164 const data = invites.filter(invite => invite.email !== ''); 166 const data = invites.filter(invite => invite.email !== '');
165 167
166 this.inviteRequest.execute(data); 168 const response = await this.inviteRequest.execute(data)._promise;
167 169
168 // we do not wait for a server response before redirecting the user 170 this.actionStatus = response.status || [];
169 this.stores.router.push('/'); 171
172 // we do not wait for a server response before redirecting the user ONLY DURING SIGNUP
173 if (this.stores.router.location.pathname.includes(this.INVITE_ROUTE)) {
174 this.stores.router.push('/');
175 }
170 176
171 gaEvent('User', 'inviteUsers'); 177 gaEvent('User', 'inviteUsers');
172 } 178 }
@@ -237,7 +243,9 @@ export default class UserStore extends Store {
237 && currentRoute.includes(this.BASE_ROUTE) 243 && currentRoute.includes(this.BASE_ROUTE)
238 && (this.hasCompletedSignup 244 && (this.hasCompletedSignup
239 || this.hasCompletedSignup === null)) { 245 || this.hasCompletedSignup === null)) {
240 this.stores.router.push('/'); 246 if (!isDevMode) {
247 this.stores.router.push('/');
248 }
241 } 249 }
242 }; 250 };
243 251
diff --git a/src/styles/invite.scss b/src/styles/invite.scss
new file mode 100644
index 000000000..bfb1a4b6b
--- /dev/null
+++ b/src/styles/invite.scss
@@ -0,0 +1,15 @@
1.invite__form {
2 /* play with values to see different layouts */
3 // display: flex;
4 align-items: center;
5 align-self: center;
6 justify-content: center;
7}
8
9.invite__embed {
10 text-align: center;
11}
12
13.invite__embed--button {
14 width: 100%;
15} \ No newline at end of file
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 261396f6f..446bdca14 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -27,6 +27,7 @@ $mdi-font-path: '../node_modules/mdi/fonts';
27@import './subscription.scss'; 27@import './subscription.scss';
28@import './subscription-popup.scss'; 28@import './subscription-popup.scss';
29@import './content-tabs.scss'; 29@import './content-tabs.scss';
30@import './invite.scss';
30 31
31// form 32// form
32@import './input.scss'; 33@import './input.scss';
diff --git a/src/styles/subscription.scss b/src/styles/subscription.scss
index 01d8f4ecb..8bfb68d23 100644
--- a/src/styles/subscription.scss
+++ b/src/styles/subscription.scss
@@ -63,11 +63,3 @@
63 } 63 }
64 } 64 }
65} 65}
66
67.mining-details {
68 margin-bottom: 15px;
69
70 button {
71 color: $theme-brand-primary;
72 }
73}
diff --git a/src/styles/welcome.scss b/src/styles/welcome.scss
index 46299b966..a12069ba4 100644
--- a/src/styles/welcome.scss
+++ b/src/styles/welcome.scss
@@ -55,6 +55,15 @@
55 background: #FFF; 55 background: #FFF;
56 color: $theme-brand-primary; 56 color: $theme-brand-primary;
57 } 57 }
58
59 &__inverted {
60 background: #FFF;
61 color: $theme-brand-primary;
62 }
63 &__inverted:hover {
64 background: none;
65 color: #FFF;
66 }
58 } 67 }
59 68
60 &__featured-services { 69 &__featured-services {
diff --git a/yarn.lock b/yarn.lock
index 58befebf1..c788006b7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -45,13 +45,6 @@
45 dependencies: 45 dependencies:
46 nan "^2.0.0" 46 nan "^2.0.0"
47 47
48"@paulcbetts/system-idle-time@^1.0.4":
49 version "1.0.4"
50 resolved "https://registry.yarnpkg.com/@paulcbetts/system-idle-time/-/system-idle-time-1.0.4.tgz#17b275530176d72695646380b13b79724288b1c6"
51 dependencies:
52 bindings "~1.2.1"
53 nan "^2.0.0"
54
55"@types/node@^7.0.18": 48"@types/node@^7.0.18":
56 version "7.0.43" 49 version "7.0.43"
57 resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" 50 resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c"
@@ -1188,10 +1181,6 @@ bindings@^1.2.1, bindings@^1.3.0:
1188 version "1.3.0" 1181 version "1.3.0"
1189 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" 1182 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
1190 1183
1191bindings@~1.2.1:
1192 version "1.2.1"
1193 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11"
1194
1195bl@~0.9.4: 1184bl@~0.9.4:
1196 version "0.9.5" 1185 version "0.9.5"
1197 resolved "https://registry.yarnpkg.com/bl/-/bl-0.9.5.tgz#c06b797af085ea00bc527afc8efcf11de2232054" 1186 resolved "https://registry.yarnpkg.com/bl/-/bl-0.9.5.tgz#c06b797af085ea00bc527afc8efcf11de2232054"
@@ -2077,9 +2066,9 @@ electron-window-state@^4.1.0:
2077 jsonfile "^2.2.3" 2066 jsonfile "^2.2.3"
2078 mkdirp "^0.5.1" 2067 mkdirp "^0.5.1"
2079 2068
2080electron@^1.7.9: 2069electron@^1.7.12:
2081 version "1.7.9" 2070 version "1.7.12"
2082 resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.9.tgz#add54e9f8f83ed02f6519ec10135f698b19336cf" 2071 resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.12.tgz#dcc61a2c1b0c3df25f68b3425379a01abd01190e"
2083 dependencies: 2072 dependencies:
2084 "@types/node" "^7.0.18" 2073 "@types/node" "^7.0.18"
2085 electron-download "^3.0.1" 2074 electron-download "^3.0.1"
@@ -4281,6 +4270,10 @@ mksnapshot@^0.3.0:
4281 fs-extra "0.26.7" 4270 fs-extra "0.26.7"
4282 request "^2.79.0" 4271 request "^2.79.0"
4283 4272
4273mobx-localstorage@^0.1.7:
4274 version "0.1.7"
4275 resolved "https://registry.yarnpkg.com/mobx-localstorage/-/mobx-localstorage-0.1.7.tgz#c0c64366769f390ca4a333f41912eae00cd4a9de"
4276
4284mobx-react-form@^1.32.2: 4277mobx-react-form@^1.32.2:
4285 version "1.32.2" 4278 version "1.32.2"
4286 resolved "https://registry.yarnpkg.com/mobx-react-form/-/mobx-react-form-1.32.2.tgz#5610dd0e4fab006acf2daf1becbedecad182a5a0" 4279 resolved "https://registry.yarnpkg.com/mobx-react-form/-/mobx-react-form-1.32.2.tgz#5610dd0e4fab006acf2daf1becbedecad182a5a0"