aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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--packages/forms/package.json6
-rw-r--r--packages/forms/src/button/index.tsx1
-rw-r--r--packages/forms/src/input/index.tsx6
-rw-r--r--packages/forms/src/label/index.tsx2
-rw-r--r--packages/theme/package.json4
-rw-r--r--packages/theme/src/themes/default/index.ts4
-rw-r--r--packages/typings/package.json4
-rw-r--r--packages/ui/package.json6
-rw-r--r--packages/ui/src/infobox/index.tsx7
-rw-r--r--src/actions/lib/actions.js29
-rw-r--r--src/api/server/ServerApi.js185
-rw-r--r--src/api/utils/auth.js28
-rw-r--r--src/styles/auth.scss2
-rw-r--r--uidev/src/stories/button.stories.tsx5
-rw-r--r--uidev/src/stories/infobox.stories.tsx9
-rw-r--r--uidev/src/stories/input.stories.tsx8
21 files changed, 249 insertions, 154 deletions
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/packages/forms/package.json b/packages/forms/package.json
index e78929777..fe161a282 100644
--- a/packages/forms/package.json
+++ b/packages/forms/package.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "@meetfranz/forms", 2 "name": "@meetfranz/forms",
3 "version": "1.0.9", 3 "version": "1.0.13",
4 "description": "React form components for Franz", 4 "description": "React form components for Franz",
5 "main": "lib/index.js", 5 "main": "lib/index.js",
6 "scripts": { 6 "scripts": {
@@ -25,7 +25,7 @@
25 "dependencies": { 25 "dependencies": {
26 "@mdi/js": "^3.3.92", 26 "@mdi/js": "^3.3.92",
27 "@mdi/react": "^1.1.0", 27 "@mdi/react": "^1.1.0",
28 "@meetfranz/theme": "^1.0.7", 28 "@meetfranz/theme": "^1.0.11",
29 "react-html-attributes": "^1.4.3", 29 "react-html-attributes": "^1.4.3",
30 "react-loader": "^2.4.5" 30 "react-loader": "^2.4.5"
31 }, 31 },
@@ -35,5 +35,5 @@
35 "react-dom": "16.7.0", 35 "react-dom": "16.7.0",
36 "react-jss": "^8.6.1" 36 "react-jss": "^8.6.1"
37 }, 37 },
38 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b" 38 "gitHead": "27778954921365e4957eae964e28f68690f3825f"
39} 39}
diff --git a/packages/forms/src/button/index.tsx b/packages/forms/src/button/index.tsx
index 7a7f83dab..6959cde73 100644
--- a/packages/forms/src/button/index.tsx
+++ b/packages/forms/src/button/index.tsx
@@ -44,6 +44,7 @@ const styles = (theme: Theme) => ({
44 width: (props: IProps) => (props.stretch ? '100%' : 'auto') as CSS.WidthProperty<string>, 44 width: (props: IProps) => (props.stretch ? '100%' : 'auto') as CSS.WidthProperty<string>,
45 fontSize: theme.uiFontSize, 45 fontSize: theme.uiFontSize,
46 textDecoration: 'none', 46 textDecoration: 'none',
47 height: theme.buttonHeight,
47 48
48 '&:hover': { 49 '&:hover': {
49 opacity: 0.8, 50 opacity: 0.8,
diff --git a/packages/forms/src/input/index.tsx b/packages/forms/src/input/index.tsx
index 478738cad..ab1c33315 100644
--- a/packages/forms/src/input/index.tsx
+++ b/packages/forms/src/input/index.tsx
@@ -102,6 +102,9 @@ class InputComponent extends Component<IProps, IState> {
102 spellCheck, 102 spellCheck,
103 onBlur, 103 onBlur,
104 onFocus, 104 onFocus,
105 min,
106 max,
107 step,
105 } = this.props; 108 } = this.props;
106 109
107 const { 110 const {
@@ -147,6 +150,9 @@ class InputComponent extends Component<IProps, IState> {
147 onFocus={onFocus} 150 onFocus={onFocus}
148 onBlur={onBlur} 151 onBlur={onBlur}
149 disabled={disabled} 152 disabled={disabled}
153 min={min}
154 max={max}
155 step={step}
150 /> 156 />
151 {suffix && ( 157 {suffix && (
152 <span className={classes.suffix}> 158 <span className={classes.suffix}>
diff --git a/packages/forms/src/label/index.tsx b/packages/forms/src/label/index.tsx
index 36fcfbedf..590270a06 100644
--- a/packages/forms/src/label/index.tsx
+++ b/packages/forms/src/label/index.tsx
@@ -26,6 +26,8 @@ class LabelComponent extends Component<ILabel> {
26 htmlFor, 26 htmlFor,
27 } = this.props; 27 } = this.props;
28 28
29 if (!showLabel) return children;
30
29 return ( 31 return (
30 <label 32 <label
31 className={classnames({ 33 className={classnames({
diff --git a/packages/theme/package.json b/packages/theme/package.json
index 5c21a787f..3034d6347 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "@meetfranz/theme", 2 "name": "@meetfranz/theme",
3 "version": "1.0.9", 3 "version": "1.0.11",
4 "description": "Theme configuration for Franz", 4 "description": "Theme configuration for Franz",
5 "author": "Stefan Malzner <stefan@adlk.io>", 5 "author": "Stefan Malzner <stefan@adlk.io>",
6 "homepage": "https://github.com/meetfranz/franz", 6 "homepage": "https://github.com/meetfranz/franz",
@@ -25,5 +25,5 @@
25 "dependencies": { 25 "dependencies": {
26 "color": "^3.1.0" 26 "color": "^3.1.0"
27 }, 27 },
28 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b" 28 "gitHead": "27778954921365e4957eae964e28f68690f3825f"
29} 29}
diff --git a/packages/theme/src/themes/default/index.ts b/packages/theme/src/themes/default/index.ts
index 8a71e61cf..273792d46 100644
--- a/packages/theme/src/themes/default/index.ts
+++ b/packages/theme/src/themes/default/index.ts
@@ -42,7 +42,7 @@ export const colorWebviewLoaderBackground = color(legacyStyles.themeGrayLighter)
42// Input 42// Input
43export const labelColor = legacyStyles.themeGrayLight; 43export const labelColor = legacyStyles.themeGrayLight;
44export const inputColor = legacyStyles.themeGray; 44export const inputColor = legacyStyles.themeGray;
45export const inputHeight = '35px'; 45export const inputHeight = 40;
46export const inputBackground = legacyStyles.themeGrayLightest; 46export const inputBackground = legacyStyles.themeGrayLightest;
47export const inputBorder = `1px solid ${legacyStyles.themeGrayLighter}`; 47export const inputBorder = `1px solid ${legacyStyles.themeGrayLighter}`;
48export const inputModifierColor = legacyStyles.themeGrayLight; 48export const inputModifierColor = legacyStyles.themeGrayLight;
@@ -108,6 +108,8 @@ export const buttonInvertedBackground = 'none';
108export const buttonInvertedTextColor = brandPrimary; 108export const buttonInvertedTextColor = brandPrimary;
109export const buttonInvertedBorder = `1px solid ${brandPrimary}`; 109export const buttonInvertedBorder = `1px solid ${brandPrimary}`;
110 110
111export const buttonHeight = inputHeight;
112
111export const buttonLoaderColor = { 113export const buttonLoaderColor = {
112 primary: '#FFF', 114 primary: '#FFF',
113 secondary: buttonSecondaryTextColor, 115 secondary: buttonSecondaryTextColor,
diff --git a/packages/typings/package.json b/packages/typings/package.json
index 54257c54c..5207cf3c6 100644
--- a/packages/typings/package.json
+++ b/packages/typings/package.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "@meetfranz/typings", 2 "name": "@meetfranz/typings",
3 "version": "0.0.7", 3 "version": "0.0.9",
4 "description": "TypeScript typings for internal and external projects", 4 "description": "TypeScript typings for internal and external projects",
5 "author": "Stefan Malzner <stefan@adlk.io>", 5 "author": "Stefan Malzner <stefan@adlk.io>",
6 "homepage": "https://github.com/meetfranz/franz", 6 "homepage": "https://github.com/meetfranz/franz",
@@ -18,5 +18,5 @@
18 "bugs": { 18 "bugs": {
19 "url": "https://github.com/meetfranz/franz/issues" 19 "url": "https://github.com/meetfranz/franz/issues"
20 }, 20 },
21 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b" 21 "gitHead": "27778954921365e4957eae964e28f68690f3825f"
22} 22}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 62a1e5a6e..fb36555c4 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "@meetfranz/ui", 2 "name": "@meetfranz/ui",
3 "version": "0.0.4", 3 "version": "0.0.6",
4 "description": "React UI components for Franz", 4 "description": "React UI components for Franz",
5 "main": "lib/index.js", 5 "main": "lib/index.js",
6 "scripts": { 6 "scripts": {
@@ -25,7 +25,7 @@
25 "dependencies": { 25 "dependencies": {
26 "@mdi/js": "^3.3.92", 26 "@mdi/js": "^3.3.92",
27 "@mdi/react": "^1.1.0", 27 "@mdi/react": "^1.1.0",
28 "@meetfranz/theme": "^1.0.7", 28 "@meetfranz/theme": "^1.0.11",
29 "react-loader": "^2.4.5" 29 "react-loader": "^2.4.5"
30 }, 30 },
31 "peerDependencies": { 31 "peerDependencies": {
@@ -34,5 +34,5 @@
34 "react-dom": "16.7.0", 34 "react-dom": "16.7.0",
35 "react-jss": "^8.6.1" 35 "react-jss": "^8.6.1"
36 }, 36 },
37 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b" 37 "gitHead": "27778954921365e4957eae964e28f68690f3825f"
38} 38}
diff --git a/packages/ui/src/infobox/index.tsx b/packages/ui/src/infobox/index.tsx
index 53ed16341..1a442a733 100644
--- a/packages/ui/src/infobox/index.tsx
+++ b/packages/ui/src/infobox/index.tsx
@@ -15,6 +15,7 @@ interface IProps extends IWithStyle {
15 ctaLabel?: string; 15 ctaLabel?: string;
16 ctaLoading?: boolean; 16 ctaLoading?: boolean;
17 children: React.ReactNode; 17 children: React.ReactNode;
18 className: string;
18} 19}
19 20
20interface IState { 21interface IState {
@@ -138,6 +139,7 @@ class InfoboxComponent extends Component<IProps, IState> {
138 ctaLoading, 139 ctaLoading,
139 ctaOnClick, 140 ctaOnClick,
140 dismissable, 141 dismissable,
142 className,
141 } = this.props; 143 } = this.props;
142 144
143 const { 145 const {
@@ -150,7 +152,10 @@ class InfoboxComponent extends Component<IProps, IState> {
150 } 152 }
151 153
152 return ( 154 return (
153 <div className={classes.wrapper}> 155 <div className={classnames({
156 [classes.wrapper]: true,
157 [`${className}`]: className,
158 })}>
154 <div 159 <div
155 className={classnames({ 160 className={classnames({
156 [classes.infobox]: true, 161 [classes.infobox]: true,
diff --git a/src/actions/lib/actions.js b/src/actions/lib/actions.js
index 499018d70..6571e9441 100644
--- a/src/actions/lib/actions.js
+++ b/src/actions/lib/actions.js
@@ -1,18 +1,23 @@
1export const createActionsFromDefinitions = (actionDefinitions, validate) => {
2 const actions = {};
3 Object.keys(actionDefinitions).forEach((actionName) => {
4 const action = (params) => {
5 const schema = actionDefinitions[actionName];
6 validate(schema, params, actionName);
7 action.notify(params);
8 };
9 actions[actionName] = action;
10 action.listeners = [];
11 action.listen = listener => action.listeners.push(listener);
12 action.notify = params => action.listeners.forEach(listener => listener(params));
13 });
14 return actions;
15};
16
1export default (definitions, validate) => { 17export default (definitions, validate) => {
2 const newActions = {}; 18 const newActions = {};
3 Object.keys(definitions).forEach((scopeName) => { 19 Object.keys(definitions).forEach((scopeName) => {
4 newActions[scopeName] = {}; 20 newActions[scopeName] = createActionsFromDefinitions(definitions[scopeName], validate);
5 Object.keys(definitions[scopeName]).forEach((actionName) => {
6 const action = (params) => {
7 const schema = definitions[scopeName][actionName];
8 validate(schema, params, actionName);
9 action.notify(params);
10 };
11 newActions[scopeName][actionName] = action;
12 action.listeners = [];
13 action.listen = listener => action.listeners.push(listener);
14 action.notify = params => action.listeners.forEach(listener => listener(params));
15 });
16 }); 21 });
17 return newActions; 22 return newActions;
18}; 23};
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
new file mode 100644
index 000000000..6dbdeaa7f
--- /dev/null
+++ b/src/api/utils/auth.js
@@ -0,0 +1,28 @@
1import { remote } from 'electron';
2import localStorage from 'mobx-localstorage';
3
4const { app } = remote;
5
6export const prepareAuthRequest = (options = { method: 'GET' }, auth = true) => {
7 const request = Object.assign(options, {
8 mode: 'cors',
9 headers: Object.assign({
10 'Content-Type': 'application/json',
11 'X-Franz-Source': 'desktop',
12 'X-Franz-Version': app.getVersion(),
13 'X-Franz-platform': process.platform,
14 'X-Franz-Timezone-Offset': new Date().getTimezoneOffset(),
15 'X-Franz-System-Locale': app.getLocale(),
16 }, options.headers),
17 });
18
19 if (auth) {
20 request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`;
21 }
22
23 return request;
24};
25
26export const sendAuthRequest = (url, options, auth) => (
27 window.fetch(url, prepareAuthRequest(options, auth))
28);
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/uidev/src/stories/button.stories.tsx b/uidev/src/stories/button.stories.tsx
index d81808530..c8e9bcbf3 100644
--- a/uidev/src/stories/button.stories.tsx
+++ b/uidev/src/stories/button.stories.tsx
@@ -76,6 +76,11 @@ storiesOf('Button')
76 busy: true, 76 busy: true,
77 })} /> 77 })} />
78 )) 78 ))
79 .add('With icon', () => (
80 <WithStoreButton store={createStore({
81 icon: 'mdiInformation',
82 })} />
83 ))
79 .add('As link', () => ( 84 .add('As link', () => (
80 <WithStoreButton store={createStore({ 85 <WithStoreButton store={createStore({
81 href: 'https://meetfranz.com', 86 href: 'https://meetfranz.com',
diff --git a/uidev/src/stories/infobox.stories.tsx b/uidev/src/stories/infobox.stories.tsx
index 2a5e8b0d5..144855376 100644
--- a/uidev/src/stories/infobox.stories.tsx
+++ b/uidev/src/stories/infobox.stories.tsx
@@ -10,6 +10,7 @@ interface IStoreArgs {
10 ctaLabel?: string; 10 ctaLabel?: string;
11 type?: string; 11 type?: string;
12 dismissable?: boolean; 12 dismissable?: boolean;
13 className?: string;
13} 14}
14 15
15const createStore = (args?: IStoreArgs) => { 16const createStore = (args?: IStoreArgs) => {
@@ -29,6 +30,7 @@ const WithStoreInfobox = observer(({ store, children }: { store: any, children:
29 type={store.type} 30 type={store.type}
30 ctaOnClick={store.ctaOnClick} 31 ctaOnClick={store.ctaOnClick}
31 dismissable={store.dismissable} 32 dismissable={store.dismissable}
33 className={store.className}
32 > 34 >
33 {children} 35 {children}
34 </Infobox> 36 </Infobox>
@@ -123,4 +125,11 @@ storiesOf('Infobox')
123 > 125 >
124 Welcome to the world of tomorrow 126 Welcome to the world of tomorrow
125 </WithStoreInfobox> 127 </WithStoreInfobox>
128 ))
129 .add('With className', () => (
130 <WithStoreInfobox store={createStore({
131 className: 'franz-is-awesome',
132 })}>
133 Welcome to the world of tomorrow
134 </WithStoreInfobox>
126 )); 135 ));
diff --git a/uidev/src/stories/input.stories.tsx b/uidev/src/stories/input.stories.tsx
index c522a10c7..af5e791d0 100644
--- a/uidev/src/stories/input.stories.tsx
+++ b/uidev/src/stories/input.stories.tsx
@@ -66,6 +66,14 @@ storiesOf('Input')
66 value="faulty input" 66 value="faulty input"
67 error="This is a generic error message." 67 error="This is a generic error message."
68 /> 68 />
69 ))
70 .add('Type number with min & max', () => (
71 <Input
72 {...defaultProps()}
73 type="number"
74 min={1}
75 max={10}
76 />
69 )); 77 ));
70 78
71storiesOf('Password') 79storiesOf('Password')