aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--docs/example-feature/actions.js10
-rw-r--r--docs/example-feature/api.js5
-rw-r--r--docs/example-feature/index.js36
-rw-r--r--docs/example-feature/state.js14
-rw-r--r--docs/example-feature/store.js32
-rw-r--r--package-lock.json60
-rw-r--r--package.json2
-rw-r--r--src/api/server/ServerApi.js185
-rw-r--r--src/api/utils/auth.js6
-rw-r--r--src/index.js5
-rw-r--r--src/lib/Tray.js8
-rw-r--r--src/stores/AppStore.js19
-rw-r--r--src/styles/auth.scss2
-rw-r--r--src/webview/contextMenu.js21
15 files changed, 252 insertions, 155 deletions
diff --git a/README.md b/README.md
index c7e1d8034..d44cfaa6c 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many mor
16 16
17`$ brew cask install franz` 17`$ brew cask install franz`
18 18
19(Don't know homebrew? [brew.sh](https://brew.sh/) 19(Don't know homebrew? [brew.sh](https://brew.sh/))
20 20
21## Development 21## Development
22 22
diff --git a/docs/example-feature/actions.js b/docs/example-feature/actions.js
new file mode 100644
index 000000000..c4d49b708
--- /dev/null
+++ b/docs/example-feature/actions.js
@@ -0,0 +1,10 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../src/actions/lib/actions';
3
4export const exampleFeatureActions = createActionsFromDefinitions({
5 greet: {
6 name: PropTypes.string.isRequired,
7 },
8}, PropTypes.checkPropTypes);
9
10export default exampleFeatureActions;
diff --git a/docs/example-feature/api.js b/docs/example-feature/api.js
new file mode 100644
index 000000000..65207e877
--- /dev/null
+++ b/docs/example-feature/api.js
@@ -0,0 +1,5 @@
1export default {
2 async getName() {
3 return Promise.resolve('Franz');
4 },
5};
diff --git a/docs/example-feature/index.js b/docs/example-feature/index.js
new file mode 100644
index 000000000..af859af26
--- /dev/null
+++ b/docs/example-feature/index.js
@@ -0,0 +1,36 @@
1import { reaction, runInAction } from 'mobx';
2import { ExampleFeatureStore } from './store';
3import state, { resetState } from './state';
4import api from './api';
5
6const debug = require('debug')('Franz:feature:EXAMPLE_FEATURE');
7
8let store = null;
9
10export default function initAnnouncements(stores, actions) {
11 const { features } = stores;
12
13 // Toggle workspace feature
14 reaction(
15 () => (
16 features.features.isExampleFeatureEnabled
17 ),
18 (isEnabled) => {
19 if (isEnabled) {
20 debug('Initializing `EXAMPLE_FEATURE` feature');
21 store = new ExampleFeatureStore(stores, api, actions, state);
22 store.initialize();
23 runInAction(() => { state.isFeatureActive = true; });
24 } else if (store) {
25 debug('Disabling `EXAMPLE_FEATURE` feature');
26 runInAction(() => { state.isFeatureActive = false; });
27 store.teardown();
28 store = null;
29 resetState(); // Reset state to default
30 }
31 },
32 {
33 fireImmediately: true,
34 },
35 );
36}
diff --git a/docs/example-feature/state.js b/docs/example-feature/state.js
new file mode 100644
index 000000000..676717da7
--- /dev/null
+++ b/docs/example-feature/state.js
@@ -0,0 +1,14 @@
1import { observable } from 'mobx';
2
3const defaultState = {
4 name: null,
5 isFeatureActive: false,
6};
7
8export const exampleFeatureState = observable(defaultState);
9
10export function resetState() {
11 Object.assign(exampleFeatureState, defaultState);
12}
13
14export default exampleFeatureState;
diff --git a/docs/example-feature/store.js b/docs/example-feature/store.js
new file mode 100644
index 000000000..d8acfdca3
--- /dev/null
+++ b/docs/example-feature/store.js
@@ -0,0 +1,32 @@
1import { action, observable, reaction } from 'mobx';
2import Store from '../../src/stores/lib/Store';
3import Request from '../../src/stores/lib/Request';
4
5const debug = require('debug')('Franz:feature:EXAMPLE_FEATURE:store');
6
7export class ExampleFeatureStore extends Store {
8 @observable getNameRequest = new Request(this.api, 'getName');
9
10 constructor(stores, api, actions, state) {
11 super(stores, api, actions);
12 this.state = state;
13 }
14
15 setup() {
16 debug('fetching name from api');
17 this.getNameRequest.execute();
18
19 // Update the name on the state when the request resolved
20 reaction(
21 () => this.getNameRequest.result,
22 name => this._setName(name),
23 );
24 }
25
26 @action _setName = (name) => {
27 debug('setting name', name);
28 this.state.name = name;
29 };
30}
31
32export default ExampleFeatureStore;
diff --git a/package-lock.json b/package-lock.json
index 1f1ac3c00..c3b291acf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2340,7 +2340,8 @@
2340 "abbrev": { 2340 "abbrev": {
2341 "version": "1.1.1", 2341 "version": "1.1.1",
2342 "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 2342 "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
2343 "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 2343 "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
2344 "dev": true
2344 }, 2345 },
2345 "accepts": { 2346 "accepts": {
2346 "version": "1.0.7", 2347 "version": "1.0.7",
@@ -2610,7 +2611,8 @@
2610 "aproba": { 2611 "aproba": {
2611 "version": "1.2.0", 2612 "version": "1.2.0",
2612 "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 2613 "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
2613 "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 2614 "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
2615 "dev": true
2614 }, 2616 },
2615 "archy": { 2617 "archy": {
2616 "version": "1.0.0", 2618 "version": "1.0.0",
@@ -2622,6 +2624,7 @@
2622 "version": "1.1.5", 2624 "version": "1.1.5",
2623 "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 2625 "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
2624 "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 2626 "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
2627 "dev": true,
2625 "requires": { 2628 "requires": {
2626 "delegates": "^1.0.0", 2629 "delegates": "^1.0.0",
2627 "readable-stream": "^2.0.6" 2630 "readable-stream": "^2.0.6"
@@ -2631,6 +2634,7 @@
2631 "version": "2.3.6", 2634 "version": "2.3.6",
2632 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 2635 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
2633 "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 2636 "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
2637 "dev": true,
2634 "requires": { 2638 "requires": {
2635 "core-util-is": "~1.0.0", 2639 "core-util-is": "~1.0.0",
2636 "inherits": "~2.0.3", 2640 "inherits": "~2.0.3",
@@ -2645,6 +2649,7 @@
2645 "version": "1.1.1", 2649 "version": "1.1.1",
2646 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 2650 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
2647 "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 2651 "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
2652 "dev": true,
2648 "requires": { 2653 "requires": {
2649 "safe-buffer": "~5.1.0" 2654 "safe-buffer": "~5.1.0"
2650 } 2655 }
@@ -5410,7 +5415,8 @@
5410 "delegates": { 5415 "delegates": {
5411 "version": "1.0.0", 5416 "version": "1.0.0",
5412 "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 5417 "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
5413 "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 5418 "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
5419 "dev": true
5414 }, 5420 },
5415 "depd": { 5421 "depd": {
5416 "version": "0.4.4", 5422 "version": "0.4.4",
@@ -5650,6 +5656,29 @@
5650 "readable-stream": "~1.1.9" 5656 "readable-stream": "~1.1.9"
5651 }, 5657 },
5652 "dependencies": { 5658 "dependencies": {
5659 "abbrev": {
5660 "version": "1.1.1",
5661 "resolved": false,
5662 "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
5663 },
5664 "ansi-regex": {
5665 "version": "2.1.1",
5666 "resolved": false,
5667 "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
5668 },
5669 "aproba": {
5670 "version": "1.2.0",
5671 "resolved": false,
5672 "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
5673 },
5674 "are-we-there-yet": {
5675 "version": "1.1.4",
5676 "resolved": false,
5677 "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
5678 "requires": {
5679 "delegates": "^1.0.0"
5680 }
5681 },
5653 "balanced-match": { 5682 "balanced-match": {
5654 "version": "1.0.0", 5683 "version": "1.0.0",
5655 "resolved": false, 5684 "resolved": false,
@@ -5966,6 +5995,11 @@
5966 "glob": "^7.0.5" 5995 "glob": "^7.0.5"
5967 } 5996 }
5968 }, 5997 },
5998 "safe-buffer": {
5999 "version": "5.1.1",
6000 "resolved": false,
6001 "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
6002 },
5969 "safer-buffer": { 6003 "safer-buffer": {
5970 "version": "2.1.2", 6004 "version": "2.1.2",
5971 "resolved": false, 6005 "resolved": false,
@@ -6010,7 +6044,10 @@
6010 "strip-ansi": { 6044 "strip-ansi": {
6011 "version": "3.0.1", 6045 "version": "3.0.1",
6012 "resolved": false, 6046 "resolved": false,
6013 "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" 6047 "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
6048 "requires": {
6049 "ansi-regex": "^2.0.0"
6050 }
6014 }, 6051 },
6015 "strip-json-comments": { 6052 "strip-json-comments": {
6016 "version": "2.0.1", 6053 "version": "2.0.1",
@@ -6136,9 +6173,9 @@
6136 "dev": true 6173 "dev": true
6137 }, 6174 },
6138 "electron": { 6175 "electron": {
6139 "version": "4.0.7", 6176 "version": "4.0.8",
6140 "resolved": "https://registry.npmjs.org/electron/-/electron-4.0.7.tgz", 6177 "resolved": "https://registry.npmjs.org/electron/-/electron-4.0.8.tgz",
6141 "integrity": "sha512-KYQ9SJZFWNKqoq6XjKW1bLFHjmAGeSC3XNuhHK/Sd2MK5H5sO3iKjvZU/YhiBUtkB/cBSkOdQTVEaLcMwU8l3A==", 6178 "integrity": "sha512-FOBJIHkuv8wc15N+ZyqwDzPavYVu5CHMBEf14jHDWv7QW2vkEIpJjVK+PIT31kfZfvjsIP0j2wvA/FBsiqB7pw==",
6142 "dev": true, 6179 "dev": true,
6143 "requires": { 6180 "requires": {
6144 "@types/node": "^10.12.18", 6181 "@types/node": "^10.12.18",
@@ -6147,9 +6184,9 @@
6147 }, 6184 },
6148 "dependencies": { 6185 "dependencies": {
6149 "@types/node": { 6186 "@types/node": {
6150 "version": "10.12.29", 6187 "version": "10.12.30",
6151 "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.29.tgz", 6188 "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.30.tgz",
6152 "integrity": "sha512-J/tnbnj8HcsBgCe2apZbdUpQ7hs4d7oZNTYA5bekWdP0sr2NGsOpI/HRdDroEi209tEvTcTtxhD0FfED3DhEcw==", 6189 "integrity": "sha512-nsqTN6zUcm9xtdJiM9OvOJ5EF0kOI8f1Zuug27O/rgtxCRJHGqncSWfCMZUP852dCKPsDsYXGvBhxfRjDBkF5Q==",
6153 "dev": true 6190 "dev": true
6154 } 6191 }
6155 } 6192 }
@@ -14574,7 +14611,8 @@
14574 "process-nextick-args": { 14611 "process-nextick-args": {
14575 "version": "2.0.0", 14612 "version": "2.0.0",
14576 "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 14613 "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
14577 "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 14614 "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
14615 "dev": true
14578 }, 14616 },
14579 "progress": { 14617 "progress": {
14580 "version": "2.0.3", 14618 "version": "2.0.3",
diff --git a/package.json b/package.json
index 14e0df7ca..df7fa4191 100644
--- a/package.json
+++ b/package.json
@@ -113,7 +113,7 @@
113 "cross-env": "^5.0.5", 113 "cross-env": "^5.0.5",
114 "cz-conventional-changelog": "2.1.0", 114 "cz-conventional-changelog": "2.1.0",
115 "dotenv": "^4.0.0", 115 "dotenv": "^4.0.0",
116 "electron": "4.0.7", 116 "electron": "4.0.8",
117 "electron-builder": "20.38.4", 117 "electron-builder": "20.38.4",
118 "electron-rebuild": "1.8.4", 118 "electron-rebuild": "1.8.4",
119 "eslint": "5.10.0", 119 "eslint": "5.10.0",
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index 2871769a9..bafeef005 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -3,7 +3,6 @@ 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';
7 6
8import ServiceModel from '../../models/Service'; 7import ServiceModel from '../../models/Service';
9import RecipePreviewModel from '../../models/RecipePreview'; 8import RecipePreviewModel from '../../models/RecipePreview';
@@ -16,6 +15,7 @@ import OrderModel from '../../models/Order';
16import { sleep } from '../../helpers/async-helpers'; 15import { sleep } from '../../helpers/async-helpers';
17 16
18import { API } from '../../environment'; 17import { API } from '../../environment';
18import { prepareAuthRequest, sendAuthRequest } from '../utils/auth';
19 19
20import { 20import {
21 getRecipeDirectory, 21 getRecipeDirectory,
@@ -39,6 +39,7 @@ const { default: fetch } = remote.require('electron-fetch');
39 39
40const SERVER_URL = API; 40const SERVER_URL = API;
41const API_VERSION = 'v1'; 41const API_VERSION = 'v1';
42const API_URL = `${SERVER_URL}/${API_VERSION}`;
42 43
43export default class ServerApi { 44export default class ServerApi {
44 recipePreviews = []; 45 recipePreviews = [];
@@ -47,12 +48,12 @@ export default class ServerApi {
47 48
48 // User 49 // User
49 async login(email, passwordHash) { 50 async login(email, passwordHash) {
50 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/auth/login`, this._prepareAuthRequest({ 51 const request = await sendAuthRequest(`${API_URL}/auth/login`, {
51 method: 'POST', 52 method: 'POST',
52 headers: { 53 headers: {
53 Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`, 54 Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`,
54 }, 55 },
55 }, false)); 56 }, false);
56 if (!request.ok) { 57 if (!request.ok) {
57 throw request; 58 throw request;
58 } 59 }
@@ -63,10 +64,10 @@ export default class ServerApi {
63 } 64 }
64 65
65 async signup(data) { 66 async signup(data) {
66 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/auth/signup`, this._prepareAuthRequest({ 67 const request = await sendAuthRequest(`${API_URL}/auth/signup`, {
67 method: 'POST', 68 method: 'POST',
68 body: JSON.stringify(data), 69 body: JSON.stringify(data),
69 }, false)); 70 }, false);
70 if (!request.ok) { 71 if (!request.ok) {
71 throw request; 72 throw request;
72 } 73 }
@@ -77,10 +78,10 @@ export default class ServerApi {
77 } 78 }
78 79
79 async inviteUser(data) { 80 async inviteUser(data) {
80 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/invite`, this._prepareAuthRequest({ 81 const request = await sendAuthRequest(`${API_URL}/invite`, {
81 method: 'POST', 82 method: 'POST',
82 body: JSON.stringify(data), 83 body: JSON.stringify(data),
83 })); 84 });
84 if (!request.ok) { 85 if (!request.ok) {
85 throw request; 86 throw request;
86 } 87 }
@@ -90,12 +91,12 @@ export default class ServerApi {
90 } 91 }
91 92
92 async retrievePassword(email) { 93 async retrievePassword(email) {
93 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/auth/password`, this._prepareAuthRequest({ 94 const request = await sendAuthRequest(`${API_URL}/auth/password`, {
94 method: 'POST', 95 method: 'POST',
95 body: JSON.stringify({ 96 body: JSON.stringify({
96 email, 97 email,
97 }), 98 }),
98 }, false)); 99 }, false);
99 if (!request.ok) { 100 if (!request.ok) {
100 throw request; 101 throw request;
101 } 102 }
@@ -106,9 +107,7 @@ export default class ServerApi {
106 } 107 }
107 108
108 async userInfo() { 109 async userInfo() {
109 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({ 110 const request = await sendAuthRequest(`${API_URL}/me`);
110 method: 'GET',
111 }));
112 if (!request.ok) { 111 if (!request.ok) {
113 throw request; 112 throw request;
114 } 113 }
@@ -121,10 +120,10 @@ export default class ServerApi {
121 } 120 }
122 121
123 async updateUserInfo(data) { 122 async updateUserInfo(data) {
124 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({ 123 const request = await sendAuthRequest(`${API_URL}/me`, {
125 method: 'PUT', 124 method: 'PUT',
126 body: JSON.stringify(data), 125 body: JSON.stringify(data),
127 })); 126 });
128 if (!request.ok) { 127 if (!request.ok) {
129 throw request; 128 throw request;
130 } 129 }
@@ -136,9 +135,9 @@ export default class ServerApi {
136 } 135 }
137 136
138 async deleteAccount() { 137 async deleteAccount() {
139 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({ 138 const request = await sendAuthRequest(`${API_URL}/me`, {
140 method: 'DELETE', 139 method: 'DELETE',
141 })); 140 });
142 if (!request.ok) { 141 if (!request.ok) {
143 throw request; 142 throw request;
144 } 143 }
@@ -150,9 +149,7 @@ export default class ServerApi {
150 149
151 // Services 150 // Services
152 async getServices() { 151 async getServices() {
153 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({ 152 const request = await sendAuthRequest(`${API_URL}/me/services`);
154 method: 'GET',
155 }));
156 if (!request.ok) { 153 if (!request.ok) {
157 throw request; 154 throw request;
158 } 155 }
@@ -165,12 +162,12 @@ export default class ServerApi {
165 } 162 }
166 163
167 async createService(recipeId, data) { 164 async createService(recipeId, data) {
168 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service`, this._prepareAuthRequest({ 165 const request = await sendAuthRequest(`${API_URL}/service`, {
169 method: 'POST', 166 method: 'POST',
170 body: JSON.stringify(Object.assign({ 167 body: JSON.stringify(Object.assign({
171 recipeId, 168 recipeId,
172 }, data)), 169 }, data)),
173 })); 170 });
174 if (!request.ok) { 171 if (!request.ok) {
175 throw request; 172 throw request;
176 } 173 }
@@ -195,10 +192,10 @@ export default class ServerApi {
195 await this.uploadServiceIcon(serviceId, data.iconFile); 192 await this.uploadServiceIcon(serviceId, data.iconFile);
196 } 193 }
197 194
198 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${serviceId}`, this._prepareAuthRequest({ 195 const request = await sendAuthRequest(`${API_URL}/service/${serviceId}`, {
199 method: 'PUT', 196 method: 'PUT',
200 body: JSON.stringify(data), 197 body: JSON.stringify(data),
201 })); 198 });
202 199
203 if (!request.ok) { 200 if (!request.ok) {
204 throw request; 201 throw request;
@@ -216,14 +213,14 @@ export default class ServerApi {
216 const formData = new FormData(); 213 const formData = new FormData();
217 formData.append('icon', icon); 214 formData.append('icon', icon);
218 215
219 const requestData = this._prepareAuthRequest({ 216 const requestData = prepareAuthRequest({
220 method: 'PUT', 217 method: 'PUT',
221 body: formData, 218 body: formData,
222 }); 219 });
223 220
224 delete requestData.headers['Content-Type']; 221 delete requestData.headers['Content-Type'];
225 222
226 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${serviceId}`, requestData); 223 const request = await window.fetch(`${API_URL}/service/${serviceId}`, requestData);
227 224
228 if (!request.ok) { 225 if (!request.ok) {
229 throw request; 226 throw request;
@@ -235,10 +232,10 @@ export default class ServerApi {
235 } 232 }
236 233
237 async reorderService(data) { 234 async reorderService(data) {
238 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/reorder`, this._prepareAuthRequest({ 235 const request = await sendAuthRequest(`${API_URL}/service/reorder`, {
239 method: 'PUT', 236 method: 'PUT',
240 body: JSON.stringify(data), 237 body: JSON.stringify(data),
241 })); 238 });
242 if (!request.ok) { 239 if (!request.ok) {
243 throw request; 240 throw request;
244 } 241 }
@@ -248,9 +245,9 @@ export default class ServerApi {
248 } 245 }
249 246
250 async deleteService(id) { 247 async deleteService(id) {
251 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${id}`, this._prepareAuthRequest({ 248 const request = await sendAuthRequest(`${API_URL}/service/${id}`, {
252 method: 'DELETE', 249 method: 'DELETE',
253 })); 250 });
254 if (!request.ok) { 251 if (!request.ok) {
255 throw request; 252 throw request;
256 } 253 }
@@ -264,9 +261,7 @@ export default class ServerApi {
264 261
265 // Features 262 // Features
266 async getDefaultFeatures() { 263 async getDefaultFeatures() {
267 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/features/default`, this._prepareAuthRequest({ 264 const request = await sendAuthRequest(`${API_URL}/features/default`);
268 method: 'GET',
269 }));
270 if (!request.ok) { 265 if (!request.ok) {
271 throw request; 266 throw request;
272 } 267 }
@@ -278,9 +273,7 @@ export default class ServerApi {
278 } 273 }
279 274
280 async getFeatures() { 275 async getFeatures() {
281 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/features`, this._prepareAuthRequest({ 276 const request = await sendAuthRequest(`${API_URL}/features`);
282 method: 'GET',
283 }));
284 if (!request.ok) { 277 if (!request.ok) {
285 throw request; 278 throw request;
286 } 279 }
@@ -314,10 +307,10 @@ export default class ServerApi {
314 } 307 }
315 308
316 async getRecipeUpdates(recipeVersions) { 309 async getRecipeUpdates(recipeVersions) {
317 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/update`, this._prepareAuthRequest({ 310 const request = await sendAuthRequest(`${API_URL}/recipes/update`, {
318 method: 'POST', 311 method: 'POST',
319 body: JSON.stringify(recipeVersions), 312 body: JSON.stringify(recipeVersions),
320 })); 313 });
321 if (!request.ok) { 314 if (!request.ok) {
322 throw request; 315 throw request;
323 } 316 }
@@ -328,29 +321,19 @@ export default class ServerApi {
328 321
329 // Recipes Previews 322 // Recipes Previews
330 async getRecipePreviews() { 323 async getRecipePreviews() {
331 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes`, this._prepareAuthRequest({ 324 const request = await sendAuthRequest(`${API_URL}/recipes`);
332 method: 'GET', 325 if (!request.ok) throw request;
333 }));
334 if (!request.ok) {
335 throw request;
336 }
337 const data = await request.json(); 326 const data = await request.json();
338
339 const recipePreviews = this._mapRecipePreviewModel(data); 327 const recipePreviews = this._mapRecipePreviewModel(data);
340 debug('ServerApi::getRecipes resolves', recipePreviews); 328 debug('ServerApi::getRecipes resolves', recipePreviews);
341
342 return recipePreviews; 329 return recipePreviews;
343 } 330 }
344 331
345 async getFeaturedRecipePreviews() { 332 async getFeaturedRecipePreviews() {
346 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/popular`, this._prepareAuthRequest({ 333 const request = await sendAuthRequest(`${API_URL}/recipes/popular`);
347 method: 'GET', 334 if (!request.ok) throw request;
348 }));
349 if (!request.ok) {
350 throw request;
351 }
352 const data = await request.json();
353 335
336 const data = await request.json();
354 // data = this._addLocalRecipesToPreviews(data); 337 // data = this._addLocalRecipesToPreviews(data);
355 338
356 const recipePreviews = this._mapRecipePreviewModel(data); 339 const recipePreviews = this._mapRecipePreviewModel(data);
@@ -359,14 +342,11 @@ export default class ServerApi {
359 } 342 }
360 343
361 async searchRecipePreviews(needle) { 344 async searchRecipePreviews(needle) {
362 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/search?needle=${needle}`, this._prepareAuthRequest({ 345 const url = `${API_URL}/recipes/search?needle=${needle}`;
363 method: 'GET', 346 const request = await sendAuthRequest(url);
364 })); 347 if (!request.ok) throw request;
365 if (!request.ok) {
366 throw request;
367 }
368 const data = await request.json();
369 348
349 const data = await request.json();
370 const recipePreviews = this._mapRecipePreviewModel(data); 350 const recipePreviews = this._mapRecipePreviewModel(data);
371 debug('ServerApi::searchRecipePreviews resolves', recipePreviews); 351 debug('ServerApi::searchRecipePreviews resolves', recipePreviews);
372 return recipePreviews; 352 return recipePreviews;
@@ -375,10 +355,9 @@ export default class ServerApi {
375 async getRecipePackage(recipeId) { 355 async getRecipePackage(recipeId) {
376 try { 356 try {
377 const recipesDirectory = path.join(app.getPath('userData'), 'recipes'); 357 const recipesDirectory = path.join(app.getPath('userData'), 'recipes');
378
379 const recipeTempDirectory = path.join(recipesDirectory, 'temp', recipeId); 358 const recipeTempDirectory = path.join(recipesDirectory, 'temp', recipeId);
380 const archivePath = path.join(recipeTempDirectory, 'recipe.tar.gz'); 359 const archivePath = path.join(recipeTempDirectory, 'recipe.tar.gz');
381 const packageUrl = `${SERVER_URL}/${API_VERSION}/recipes/download/${recipeId}`; 360 const packageUrl = `${API_URL}/recipes/download/${recipeId}`;
382 361
383 fs.ensureDirSync(recipeTempDirectory); 362 fs.ensureDirSync(recipeTempDirectory);
384 const res = await fetch(packageUrl); 363 const res = await fetch(packageUrl);
@@ -415,26 +394,21 @@ export default class ServerApi {
415 394
416 // Payment 395 // Payment
417 async getPlans() { 396 async getPlans() {
418 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/payment/plans`, this._prepareAuthRequest({ 397 const request = await sendAuthRequest(`${API_URL}/payment/plans`);
419 method: 'GET', 398 if (!request.ok) throw request;
420 }));
421 if (!request.ok) {
422 throw request;
423 }
424 const data = await request.json(); 399 const data = await request.json();
425
426 const plan = new PlanModel(data); 400 const plan = new PlanModel(data);
427 debug('ServerApi::getPlans resolves', plan); 401 debug('ServerApi::getPlans resolves', plan);
428 return plan; 402 return plan;
429 } 403 }
430 404
431 async getHostedPage(planId) { 405 async getHostedPage(planId) {
432 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/payment/init`, this._prepareAuthRequest({ 406 const request = await sendAuthRequest(`${API_URL}/payment/init`, {
433 method: 'POST', 407 method: 'POST',
434 body: JSON.stringify({ 408 body: JSON.stringify({
435 planId, 409 planId,
436 }), 410 }),
437 })); 411 });
438 if (!request.ok) { 412 if (!request.ok) {
439 throw request; 413 throw request;
440 } 414 }
@@ -445,25 +419,16 @@ export default class ServerApi {
445 } 419 }
446 420
447 async getPaymentDashboardUrl() { 421 async getPaymentDashboardUrl() {
448 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/billing`, this._prepareAuthRequest({ 422 const request = await sendAuthRequest(`${API_URL}/me/billing`);
449 method: 'GET', 423 if (!request.ok) throw request;
450 }));
451 if (!request.ok) {
452 throw request;
453 }
454 const data = await request.json(); 424 const data = await request.json();
455
456 debug('ServerApi::getPaymentDashboardUrl resolves', data); 425 debug('ServerApi::getPaymentDashboardUrl resolves', data);
457 return data; 426 return data;
458 } 427 }
459 428
460 async getSubscriptionOrders() { 429 async getSubscriptionOrders() {
461 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/subscription`, this._prepareAuthRequest({ 430 const request = await sendAuthRequest(`${API_URL}/me/subscription`);
462 method: 'GET', 431 if (!request.ok) throw request;
463 }));
464 if (!request.ok) {
465 throw request;
466 }
467 const data = await request.json(); 432 const data = await request.json();
468 const orders = this._mapOrderModels(data); 433 const orders = this._mapOrderModels(data);
469 debug('ServerApi::getSubscriptionOrders resolves', orders); 434 debug('ServerApi::getSubscriptionOrders resolves', orders);
@@ -472,15 +437,9 @@ export default class ServerApi {
472 437
473 // News 438 // News
474 async getLatestNews() { 439 async getLatestNews() {
475 // eslint-disable-next-line 440 const url = `${API_URL}/news?platform=${os.platform()}&arch=${os.arch()}&version=${app.getVersion()}`;
476 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/news?platform=${os.platform()}&arch=${os.arch()}&version=${app.getVersion()}`, 441 const request = await sendAuthRequest(url);
477 this._prepareAuthRequest({ 442 if (!request.ok) throw request;
478 method: 'GET',
479 }));
480
481 if (!request.ok) {
482 throw request;
483 }
484 const data = await request.json(); 443 const data = await request.json();
485 const news = this._mapNewsModels(data); 444 const news = this._mapNewsModels(data);
486 debug('ServerApi::getLatestNews resolves', news); 445 debug('ServerApi::getLatestNews resolves', news);
@@ -488,23 +447,16 @@ export default class ServerApi {
488 } 447 }
489 448
490 async hideNews(id) { 449 async hideNews(id) {
491 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/news/${id}/read`, 450 const request = await sendAuthRequest(`${API_URL}/news/${id}/read`);
492 this._prepareAuthRequest({ 451 if (!request.ok) throw request;
493 method: 'GET',
494 }));
495
496 if (!request.ok) {
497 throw request;
498 }
499
500 debug('ServerApi::hideNews resolves', id); 452 debug('ServerApi::hideNews resolves', id);
501 } 453 }
502 454
503 // Health Check 455 // Health Check
504 async healthCheck() { 456 async healthCheck() {
505 const request = await window.fetch(`${SERVER_URL}/health`, this._prepareAuthRequest({ 457 const request = await sendAuthRequest(`${SERVER_URL}/health`, {
506 method: 'GET', 458 method: 'GET',
507 }, false)); 459 }, false);
508 if (!request.ok) { 460 if (!request.ok) {
509 throw request; 461 throw request;
510 } 462 }
@@ -520,10 +472,7 @@ export default class ServerApi {
520 if (Object.prototype.hasOwnProperty.call(config, 'services')) { 472 if (Object.prototype.hasOwnProperty.call(config, 'services')) {
521 const services = await Promise.all(config.services.map(async (s) => { 473 const services = await Promise.all(config.services.map(async (s) => {
522 const service = s; 474 const service = s;
523 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/${s.service}`, 475 const request = await sendAuthRequest(`${API_URL}/recipes/${s.service}`);
524 this._prepareAuthRequest({
525 method: 'GET',
526 }));
527 476
528 if (request.status === 200) { 477 if (request.status === 200) {
529 const data = await request.json(); 478 const data = await request.json();
@@ -546,9 +495,7 @@ export default class ServerApi {
546 // Helper 495 // Helper
547 async _mapServiceModels(services) { 496 async _mapServiceModels(services) {
548 const recipes = services.map(s => s.recipeId); 497 const recipes = services.map(s => s.recipeId);
549
550 await this._bulkRecipeCheck(recipes); 498 await this._bulkRecipeCheck(recipes);
551
552 /* eslint-disable no-return-await */ 499 /* eslint-disable no-return-await */
553 return Promise.all(services.map(async service => await this._prepareServiceModel(service))); 500 return Promise.all(services.map(async service => await this._prepareServiceModel(service)));
554 /* eslint-enable no-return-await */ 501 /* eslint-enable no-return-await */
@@ -632,26 +579,6 @@ export default class ServerApi {
632 }).filter(orderItem => orderItem !== null); 579 }).filter(orderItem => orderItem !== null);
633 } 580 }
634 581
635 _prepareAuthRequest(options, auth = true) {
636 const request = Object.assign(options, {
637 mode: 'cors',
638 headers: Object.assign({
639 'Content-Type': 'application/json',
640 'X-Franz-Source': 'desktop',
641 'X-Franz-Version': app.getVersion(),
642 'X-Franz-platform': process.platform,
643 'X-Franz-Timezone-Offset': new Date().getTimezoneOffset(),
644 'X-Franz-System-Locale': app.getLocale(),
645 }, options.headers),
646 });
647
648 if (auth) {
649 request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`;
650 }
651
652 return request;
653 }
654
655 _getDevRecipes() { 582 _getDevRecipes() {
656 const recipesDirectory = getDevRecipeDirectory(); 583 const recipesDirectory = getDevRecipeDirectory();
657 try { 584 try {
diff --git a/src/api/utils/auth.js b/src/api/utils/auth.js
index d469853a5..6dbdeaa7f 100644
--- a/src/api/utils/auth.js
+++ b/src/api/utils/auth.js
@@ -3,7 +3,7 @@ import localStorage from 'mobx-localstorage';
3 3
4const { app } = remote; 4const { app } = remote;
5 5
6export const prepareAuthRequest = (options, auth = true) => { 6export const prepareAuthRequest = (options = { method: 'GET' }, auth = true) => {
7 const request = Object.assign(options, { 7 const request = Object.assign(options, {
8 mode: 'cors', 8 mode: 'cors',
9 headers: Object.assign({ 9 headers: Object.assign({
@@ -23,6 +23,6 @@ export const prepareAuthRequest = (options, auth = true) => {
23 return request; 23 return request;
24}; 24};
25 25
26export const sendAuthRequest = (url, options) => ( 26export const sendAuthRequest = (url, options, auth) => (
27 window.fetch(url, prepareAuthRequest(options)) 27 window.fetch(url, prepareAuthRequest(options, auth))
28); 28);
diff --git a/src/index.js b/src/index.js
index 0e222c3d6..05c793d98 100644
--- a/src/index.js
+++ b/src/index.js
@@ -72,7 +72,10 @@ if (!gotTheLock) {
72 app.on('second-instance', (event, argv) => { 72 app.on('second-instance', (event, argv) => {
73 // Someone tried to run a second instance, we should focus our window. 73 // Someone tried to run a second instance, we should focus our window.
74 if (mainWindow) { 74 if (mainWindow) {
75 if (mainWindow.isMinimized()) mainWindow.restore(); 75 mainWindow.show();
76 if (mainWindow.isMinimized()) {
77 mainWindow.restore();
78 }
76 mainWindow.focus(); 79 mainWindow.focus();
77 80
78 if (isWindows) { 81 if (isWindows) {
diff --git a/src/lib/Tray.js b/src/lib/Tray.js
index 669b02709..192e24796 100644
--- a/src/lib/Tray.js
+++ b/src/lib/Tray.js
@@ -22,7 +22,11 @@ export default class TrayIcon {
22 { 22 {
23 label: 'Show Franz', 23 label: 'Show Franz',
24 click() { 24 click() {
25 if (app.mainWindow.isMinimized()) {
26 app.mainWindow.restore();
27 }
25 app.mainWindow.show(); 28 app.mainWindow.show();
29 app.mainWindow.focus();
26 }, 30 },
27 }, { 31 }, {
28 label: 'Quit Franz', 32 label: 'Quit Franz',
@@ -36,7 +40,11 @@ export default class TrayIcon {
36 this.trayIcon.setContextMenu(trayMenu); 40 this.trayIcon.setContextMenu(trayMenu);
37 41
38 this.trayIcon.on('click', () => { 42 this.trayIcon.on('click', () => {
43 if (app.mainWindow.isMinimized()) {
44 app.mainWindow.restore();
45 }
39 app.mainWindow.show(); 46 app.mainWindow.show();
47 app.mainWindow.focus();
40 }); 48 });
41 49
42 if (process.platform === 'darwin') { 50 if (process.platform === 'darwin') {
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 7784ff1f9..351ad6422 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -12,7 +12,7 @@ import { URL } from 'url';
12import Store from './lib/Store'; 12import Store from './lib/Store';
13import Request from './lib/Request'; 13import Request from './lib/Request';
14import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config'; 14import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config';
15import { isMac, isLinux, isWindows } from '../environment'; 15import { isMac } from '../environment';
16import locales from '../i18n/translations'; 16import locales from '../i18n/translations';
17import { gaEvent, gaPage } from '../lib/analytics'; 17import { gaEvent, gaPage } from '../lib/analytics';
18import { onVisibilityChange } from '../helpers/visibility-helper'; 18import { onVisibilityChange } from '../helpers/visibility-helper';
@@ -185,7 +185,15 @@ export default class AppStore extends Store {
185 }) { 185 }) {
186 if (this.stores.settings.all.app.isAppMuted) return; 186 if (this.stores.settings.all.app.isAppMuted) return;
187 187
188 // TODO: is there a simple way to use blobs for notifications without storing them on disk?
189 if (options.icon.startsWith('blob:')) {
190 delete options.icon;
191 }
192
188 const notification = new window.Notification(title, options); 193 const notification = new window.Notification(title, options);
194
195 debug('New notification', title, options);
196
189 notification.onclick = (e) => { 197 notification.onclick = (e) => {
190 if (serviceId) { 198 if (serviceId) {
191 this.actions.service.sendIPCMessage({ 199 this.actions.service.sendIPCMessage({
@@ -195,12 +203,13 @@ export default class AppStore extends Store {
195 }); 203 });
196 204
197 this.actions.service.setActive({ serviceId }); 205 this.actions.service.setActive({ serviceId });
198 206 mainWindow.show();
199 if (isWindows) { 207 if (app.mainWindow.isMinimized()) {
200 mainWindow.restore(); 208 mainWindow.restore();
201 } else if (isLinux) {
202 mainWindow.show();
203 } 209 }
210 mainWindow.focus();
211
212 debug('Notification click handler');
204 } 213 }
205 }; 214 };
206 } 215 }
diff --git a/src/styles/auth.scss b/src/styles/auth.scss
index 817801982..0a075036a 100644
--- a/src/styles/auth.scss
+++ b/src/styles/auth.scss
@@ -107,7 +107,7 @@
107 &__scroll-container { 107 &__scroll-container {
108 max-height: 100vh; 108 max-height: 100vh;
109 padding: 80px 0; 109 padding: 80px 0;
110 overflow: scroll; 110 overflow: auto;
111 width: 100%; 111 width: 100%;
112 } 112 }
113 113
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js
index afb1d8912..a4a6ab899 100644
--- a/src/webview/contextMenu.js
+++ b/src/webview/contextMenu.js
@@ -33,6 +33,8 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck
33 const canGoBack = webContents.canGoBack(); 33 const canGoBack = webContents.canGoBack();
34 const canGoForward = webContents.canGoForward(); 34 const canGoForward = webContents.canGoForward();
35 35
36 // @adlk: we can't use roles here due to a bug with electron where electron.remote.webContents.getFocusedWebContents() returns the first webview in DOM instead of the focused one
37 // Github issue creation is pending
36 let menuTpl = [ 38 let menuTpl = [
37 { 39 {
38 type: 'separator', 40 type: 'separator',
@@ -48,19 +50,32 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck
48 type: 'separator', 50 type: 'separator',
49 }, { 51 }, {
50 id: 'cut', 52 id: 'cut',
51 role: can('Cut') ? 'cut' : '', 53 label: 'Cut',
54 click() {
55 if (can('Cut')) {
56 webContents.cut();
57 }
58 },
52 enabled: can('Cut'), 59 enabled: can('Cut'),
53 visible: hasText && props.isEditable, 60 visible: hasText && props.isEditable,
54 }, { 61 }, {
55 id: 'copy', 62 id: 'copy',
56 label: 'Copy', 63 label: 'Copy',
57 role: can('Copy') ? 'copy' : '', 64 click() {
65 if (can('Copy')) {
66 webContents.copy();
67 }
68 },
58 enabled: can('Copy'), 69 enabled: can('Copy'),
59 visible: props.isEditable || hasText, 70 visible: props.isEditable || hasText,
60 }, { 71 }, {
61 id: 'paste', 72 id: 'paste',
62 label: 'Paste', 73 label: 'Paste',
63 role: editFlags.canPaste ? 'paste' : '', 74 click() {
75 if (editFlags.canPaste) {
76 webContents.paste();
77 }
78 },
64 enabled: editFlags.canPaste, 79 enabled: editFlags.canPaste,
65 visible: props.isEditable, 80 visible: props.isEditable,
66 }, { 81 }, {