diff options
Diffstat (limited to 'src/containers')
-rw-r--r-- | src/containers/auth/AuthLayoutContainer.js | 47 | ||||
-rw-r--r-- | src/containers/auth/ImportScreen.js | 41 | ||||
-rw-r--r-- | src/containers/auth/InviteScreen.js | 29 | ||||
-rw-r--r-- | src/containers/auth/LoginScreen.js | 45 | ||||
-rw-r--r-- | src/containers/auth/PasswordScreen.js | 38 | ||||
-rw-r--r-- | src/containers/auth/PricingScreen.js | 53 | ||||
-rw-r--r-- | src/containers/auth/SignupScreen.js | 43 | ||||
-rw-r--r-- | src/containers/auth/WelcomeScreen.js | 34 | ||||
-rw-r--r-- | src/containers/layout/AppLayoutContainer.js | 166 | ||||
-rw-r--r-- | src/containers/settings/AccountScreen.js | 114 | ||||
-rw-r--r-- | src/containers/settings/EditServiceScreen.js | 208 | ||||
-rw-r--r-- | src/containers/settings/EditSettingsScreen.js | 167 | ||||
-rw-r--r-- | src/containers/settings/EditUserScreen.js | 165 | ||||
-rw-r--r-- | src/containers/settings/RecipesScreen.js | 126 | ||||
-rw-r--r-- | src/containers/settings/ServicesScreen.js | 75 | ||||
-rw-r--r-- | src/containers/settings/SettingsWindow.js | 43 | ||||
-rw-r--r-- | src/containers/ui/SubscriptionFormScreen.js | 126 | ||||
-rw-r--r-- | src/containers/ui/SubscriptionPopupScreen.js | 43 |
18 files changed, 1563 insertions, 0 deletions
diff --git a/src/containers/auth/AuthLayoutContainer.js b/src/containers/auth/AuthLayoutContainer.js new file mode 100644 index 000000000..004054fdd --- /dev/null +++ b/src/containers/auth/AuthLayoutContainer.js | |||
@@ -0,0 +1,47 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | |||
5 | import AuthLayout from '../../components/auth/AuthLayout'; | ||
6 | import AppStore from '../../stores/AppStore'; | ||
7 | import GlobalErrorStore from '../../stores/GlobalErrorStore'; | ||
8 | |||
9 | import { oneOrManyChildElements } from '../../prop-types'; | ||
10 | |||
11 | @inject('stores', 'actions') @observer | ||
12 | export default class AuthLayoutContainer extends Component { | ||
13 | static propTypes = { | ||
14 | children: oneOrManyChildElements.isRequired, | ||
15 | location: PropTypes.shape({ | ||
16 | pathname: PropTypes.string.isRequired, | ||
17 | }).isRequired, | ||
18 | }; | ||
19 | |||
20 | render() { | ||
21 | const { stores, actions, children, location } = this.props; | ||
22 | return ( | ||
23 | <AuthLayout | ||
24 | error={stores.globalError.response} | ||
25 | pathname={location.pathname} | ||
26 | isOnline={stores.app.isOnline} | ||
27 | isAPIHealthy={!stores.app.healthCheckRequest.isError} | ||
28 | retryHealthCheck={actions.app.healthCheck} | ||
29 | isHealthCheckLoading={stores.app.healthCheckRequest.isExecuting} | ||
30 | > | ||
31 | {children} | ||
32 | </AuthLayout> | ||
33 | ); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | AuthLayoutContainer.wrappedComponent.propTypes = { | ||
38 | stores: PropTypes.shape({ | ||
39 | app: PropTypes.instanceOf(AppStore).isRequired, | ||
40 | globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired, | ||
41 | }).isRequired, | ||
42 | actions: PropTypes.shape({ | ||
43 | app: PropTypes.shape({ | ||
44 | healthCheck: PropTypes.func.isRequired, | ||
45 | }).isRequired, | ||
46 | }).isRequired, | ||
47 | }; | ||
diff --git a/src/containers/auth/ImportScreen.js b/src/containers/auth/ImportScreen.js new file mode 100644 index 000000000..ddd56ffb6 --- /dev/null +++ b/src/containers/auth/ImportScreen.js | |||
@@ -0,0 +1,41 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import Import from '../../components/auth/Import'; | ||
5 | import UserStore from '../../stores/UserStore'; | ||
6 | import { gaPage } from '../../lib/analytics'; | ||
7 | |||
8 | @inject('stores', 'actions') @observer | ||
9 | export default class ImportScreen extends Component { | ||
10 | componentDidMount() { | ||
11 | gaPage('Auth/Import'); | ||
12 | } | ||
13 | |||
14 | render() { | ||
15 | const { actions, stores } = this.props; | ||
16 | |||
17 | if (stores.user.isImportLegacyServicesCompleted) { | ||
18 | stores.router.push(stores.user.inviteRoute); | ||
19 | } | ||
20 | |||
21 | return ( | ||
22 | <Import | ||
23 | services={stores.user.legacyServices} | ||
24 | onSubmit={actions.user.importLegacyServices} | ||
25 | isSubmitting={stores.user.isImportLegacyServicesExecuting} | ||
26 | inviteRoute={stores.user.inviteRoute} | ||
27 | /> | ||
28 | ); | ||
29 | } | ||
30 | } | ||
31 | |||
32 | ImportScreen.wrappedComponent.propTypes = { | ||
33 | actions: PropTypes.shape({ | ||
34 | user: PropTypes.shape({ | ||
35 | importLegacyServices: PropTypes.func.isRequired, | ||
36 | }).isRequired, | ||
37 | }).isRequired, | ||
38 | stores: PropTypes.shape({ | ||
39 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
40 | }).isRequired, | ||
41 | }; | ||
diff --git a/src/containers/auth/InviteScreen.js b/src/containers/auth/InviteScreen.js new file mode 100644 index 000000000..51971f436 --- /dev/null +++ b/src/containers/auth/InviteScreen.js | |||
@@ -0,0 +1,29 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import Invite from '../../components/auth/Invite'; | ||
5 | import { gaPage } from '../../lib/analytics'; | ||
6 | |||
7 | @inject('stores', 'actions') @observer | ||
8 | export default class InviteScreen extends Component { | ||
9 | componentDidMount() { | ||
10 | gaPage('Auth/Invite'); | ||
11 | } | ||
12 | |||
13 | render() { | ||
14 | const { actions } = this.props; | ||
15 | return ( | ||
16 | <Invite | ||
17 | onSubmit={actions.user.invite} | ||
18 | /> | ||
19 | ); | ||
20 | } | ||
21 | } | ||
22 | |||
23 | InviteScreen.wrappedComponent.propTypes = { | ||
24 | actions: PropTypes.shape({ | ||
25 | user: PropTypes.shape({ | ||
26 | invite: PropTypes.func.isRequired, | ||
27 | }).isRequired, | ||
28 | }).isRequired, | ||
29 | }; | ||
diff --git a/src/containers/auth/LoginScreen.js b/src/containers/auth/LoginScreen.js new file mode 100644 index 000000000..9e22c5141 --- /dev/null +++ b/src/containers/auth/LoginScreen.js | |||
@@ -0,0 +1,45 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import Login from '../../components/auth/Login'; | ||
5 | import UserStore from '../../stores/UserStore'; | ||
6 | import { gaPage } from '../../lib/analytics'; | ||
7 | |||
8 | import { globalError as globalErrorPropType } from '../../prop-types'; | ||
9 | |||
10 | @inject('stores', 'actions') @observer | ||
11 | export default class LoginScreen extends Component { | ||
12 | static propTypes = { | ||
13 | error: globalErrorPropType.isRequired, | ||
14 | }; | ||
15 | |||
16 | componentDidMount() { | ||
17 | gaPage('Auth/Login'); | ||
18 | } | ||
19 | |||
20 | render() { | ||
21 | const { actions, stores, error } = this.props; | ||
22 | return ( | ||
23 | <Login | ||
24 | onSubmit={actions.user.login} | ||
25 | isSubmitting={stores.user.loginRequest.isExecuting} | ||
26 | isTokenExpired={stores.user.isTokenExpired} | ||
27 | isServerLogout={stores.user.logoutReason === stores.user.logoutReasonTypes.SERVER} | ||
28 | signupRoute={stores.user.signupRoute} | ||
29 | passwordRoute={stores.user.passwordRoute} | ||
30 | error={error} | ||
31 | /> | ||
32 | ); | ||
33 | } | ||
34 | } | ||
35 | |||
36 | LoginScreen.wrappedComponent.propTypes = { | ||
37 | actions: PropTypes.shape({ | ||
38 | user: PropTypes.shape({ | ||
39 | login: PropTypes.func.isRequired, | ||
40 | }).isRequired, | ||
41 | }).isRequired, | ||
42 | stores: PropTypes.shape({ | ||
43 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
44 | }).isRequired, | ||
45 | }; | ||
diff --git a/src/containers/auth/PasswordScreen.js b/src/containers/auth/PasswordScreen.js new file mode 100644 index 000000000..d88cb08e6 --- /dev/null +++ b/src/containers/auth/PasswordScreen.js | |||
@@ -0,0 +1,38 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import Password from '../../components/auth/Password'; | ||
5 | import UserStore from '../../stores/UserStore'; | ||
6 | import { gaPage } from '../../lib/analytics'; | ||
7 | |||
8 | @inject('stores', 'actions') @observer | ||
9 | export default class PasswordScreen extends Component { | ||
10 | componentDidMount() { | ||
11 | gaPage('Auth/Password Retrieve'); | ||
12 | } | ||
13 | |||
14 | render() { | ||
15 | const { actions, stores } = this.props; | ||
16 | |||
17 | return ( | ||
18 | <Password | ||
19 | onSubmit={actions.user.retrievePassword} | ||
20 | isSubmitting={stores.user.passwordRequest.isExecuting} | ||
21 | signupRoute={stores.user.signupRoute} | ||
22 | loginRoute={stores.user.loginRoute} | ||
23 | status={stores.user.actionStatus} | ||
24 | /> | ||
25 | ); | ||
26 | } | ||
27 | } | ||
28 | |||
29 | PasswordScreen.wrappedComponent.propTypes = { | ||
30 | actions: PropTypes.shape({ | ||
31 | user: PropTypes.shape({ | ||
32 | retrievePassword: PropTypes.func.isRequired, | ||
33 | }).isRequired, | ||
34 | }).isRequired, | ||
35 | stores: PropTypes.shape({ | ||
36 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
37 | }).isRequired, | ||
38 | }; | ||
diff --git a/src/containers/auth/PricingScreen.js b/src/containers/auth/PricingScreen.js new file mode 100644 index 000000000..7e1586535 --- /dev/null +++ b/src/containers/auth/PricingScreen.js | |||
@@ -0,0 +1,53 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import { RouterStore } from 'mobx-react-router'; | ||
5 | |||
6 | import Pricing from '../../components/auth/Pricing'; | ||
7 | import UserStore from '../../stores/UserStore'; | ||
8 | import PaymentStore from '../../stores/PaymentStore'; | ||
9 | import { gaPage } from '../../lib/analytics'; | ||
10 | |||
11 | import { globalError as globalErrorPropType } from '../../prop-types'; | ||
12 | |||
13 | @inject('stores', 'actions') @observer | ||
14 | export default class PricingScreen extends Component { | ||
15 | static propTypes = { | ||
16 | error: globalErrorPropType.isRequired, | ||
17 | }; | ||
18 | |||
19 | componentDidMount() { | ||
20 | gaPage('Auth/Pricing'); | ||
21 | } | ||
22 | |||
23 | render() { | ||
24 | const { actions, stores, error } = this.props; | ||
25 | |||
26 | const nextStepRoute = stores.user.legacyServices.length ? stores.user.importRoute : stores.user.inviteRoute; | ||
27 | |||
28 | return ( | ||
29 | <Pricing | ||
30 | donor={stores.user.data.donor || {}} | ||
31 | onSubmit={actions.user.signup} | ||
32 | onCloseSubscriptionWindow={() => this.props.stores.router.push(nextStepRoute)} | ||
33 | isLoading={stores.payment.plansRequest.isExecuting} | ||
34 | isLoadingUser={stores.user.getUserInfoRequest.isExecuting} | ||
35 | error={error} | ||
36 | skipAction={() => this.props.stores.router.push(nextStepRoute)} | ||
37 | /> | ||
38 | ); | ||
39 | } | ||
40 | } | ||
41 | |||
42 | PricingScreen.wrappedComponent.propTypes = { | ||
43 | actions: PropTypes.shape({ | ||
44 | user: PropTypes.shape({ | ||
45 | signup: PropTypes.func.isRequired, | ||
46 | }).isRequired, | ||
47 | }).isRequired, | ||
48 | stores: PropTypes.shape({ | ||
49 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
50 | payment: PropTypes.instanceOf(PaymentStore).isRequired, | ||
51 | router: PropTypes.instanceOf(RouterStore).isRequired, | ||
52 | }).isRequired, | ||
53 | }; | ||
diff --git a/src/containers/auth/SignupScreen.js b/src/containers/auth/SignupScreen.js new file mode 100644 index 000000000..3b86ab138 --- /dev/null +++ b/src/containers/auth/SignupScreen.js | |||
@@ -0,0 +1,43 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | |||
5 | import Signup from '../../components/auth/Signup'; | ||
6 | import UserStore from '../../stores/UserStore'; | ||
7 | import { gaPage } from '../../lib/analytics'; | ||
8 | |||
9 | import { globalError as globalErrorPropType } from '../../prop-types'; | ||
10 | |||
11 | @inject('stores', 'actions') @observer | ||
12 | export default class SignupScreen extends Component { | ||
13 | static propTypes = { | ||
14 | error: globalErrorPropType.isRequired, | ||
15 | }; | ||
16 | |||
17 | componentDidMount() { | ||
18 | gaPage('Auth/Signup'); | ||
19 | } | ||
20 | |||
21 | render() { | ||
22 | const { actions, stores, error } = this.props; | ||
23 | return ( | ||
24 | <Signup | ||
25 | onSubmit={actions.user.signup} | ||
26 | isSubmitting={stores.user.signupRequest.isExecuting} | ||
27 | loginRoute={stores.user.loginRoute} | ||
28 | error={error} | ||
29 | /> | ||
30 | ); | ||
31 | } | ||
32 | } | ||
33 | |||
34 | SignupScreen.wrappedComponent.propTypes = { | ||
35 | actions: PropTypes.shape({ | ||
36 | user: PropTypes.shape({ | ||
37 | signup: PropTypes.func.isRequired, | ||
38 | }).isRequired, | ||
39 | }).isRequired, | ||
40 | stores: PropTypes.shape({ | ||
41 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
42 | }).isRequired, | ||
43 | }; | ||
diff --git a/src/containers/auth/WelcomeScreen.js b/src/containers/auth/WelcomeScreen.js new file mode 100644 index 000000000..e413264a6 --- /dev/null +++ b/src/containers/auth/WelcomeScreen.js | |||
@@ -0,0 +1,34 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | |||
5 | import Welcome from '../../components/auth/Welcome'; | ||
6 | import UserStore from '../../stores/UserStore'; | ||
7 | import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; | ||
8 | import { gaPage } from '../../lib/analytics'; | ||
9 | |||
10 | @inject('stores', 'actions') @observer | ||
11 | export default class LoginScreen extends Component { | ||
12 | componentDidMount() { | ||
13 | gaPage('Auth/Welcome'); | ||
14 | } | ||
15 | |||
16 | render() { | ||
17 | const { user, recipePreviews } = this.props.stores; | ||
18 | |||
19 | return ( | ||
20 | <Welcome | ||
21 | loginRoute={user.loginRoute} | ||
22 | signupRoute={user.signupRoute} | ||
23 | recipes={recipePreviews.featured} | ||
24 | /> | ||
25 | ); | ||
26 | } | ||
27 | } | ||
28 | |||
29 | LoginScreen.wrappedComponent.propTypes = { | ||
30 | stores: PropTypes.shape({ | ||
31 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
32 | recipePreviews: PropTypes.instanceOf(RecipePreviewsStore).isRequired, | ||
33 | }).isRequired, | ||
34 | }; | ||
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js new file mode 100644 index 000000000..aa7f7952a --- /dev/null +++ b/src/containers/layout/AppLayoutContainer.js | |||
@@ -0,0 +1,166 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | |||
5 | import AppStore from '../../stores/AppStore'; | ||
6 | import RecipesStore from '../../stores/RecipesStore'; | ||
7 | import ServicesStore from '../../stores/ServicesStore'; | ||
8 | import UIStore from '../../stores/UIStore'; | ||
9 | import NewsStore from '../../stores/NewsStore'; | ||
10 | import UserStore from '../../stores/UserStore'; | ||
11 | import RequestStore from '../../stores/RequestStore'; | ||
12 | import GlobalErrorStore from '../../stores/GlobalErrorStore'; | ||
13 | |||
14 | import { oneOrManyChildElements } from '../../prop-types'; | ||
15 | import AppLayout from '../../components/layout/AppLayout'; | ||
16 | import Sidebar from '../../components/layout/Sidebar'; | ||
17 | import Services from '../../components/services/content/Services'; | ||
18 | import AppLoader from '../../components/ui/AppLoader'; | ||
19 | |||
20 | @inject('stores', 'actions') @observer | ||
21 | export default class AppLayoutContainer extends Component { | ||
22 | static defaultProps = { | ||
23 | children: null, | ||
24 | }; | ||
25 | |||
26 | render() { | ||
27 | const { | ||
28 | app, | ||
29 | services, | ||
30 | ui, | ||
31 | news, | ||
32 | globalError, | ||
33 | user, | ||
34 | requests, | ||
35 | } = this.props.stores; | ||
36 | |||
37 | const { | ||
38 | setActive, | ||
39 | handleIPCMessage, | ||
40 | setWebviewReference, | ||
41 | openWindow, | ||
42 | reloadUpdatedServices, | ||
43 | reorder, | ||
44 | reload, | ||
45 | toggleNotifications, | ||
46 | deleteService, | ||
47 | updateService, | ||
48 | } = this.props.actions.service; | ||
49 | |||
50 | const { hide } = this.props.actions.news; | ||
51 | |||
52 | const { retryRequiredRequests } = this.props.actions.requests; | ||
53 | |||
54 | const { | ||
55 | installUpdate, | ||
56 | } = this.props.actions.app; | ||
57 | |||
58 | const { | ||
59 | openSettings, | ||
60 | closeSettings, | ||
61 | } = this.props.actions.ui; | ||
62 | |||
63 | const { children } = this.props; | ||
64 | const allServices = services.enabled; | ||
65 | |||
66 | const isLoadingServices = services.allServicesRequest.isExecuting | ||
67 | && services.allServicesRequest.isExecutingFirstTime; | ||
68 | |||
69 | // const isLoadingRecipes = recipes.allRecipesRequest.isExecuting | ||
70 | // && recipes.allRecipesRequest.isExecutingFirstTime; | ||
71 | |||
72 | if (isLoadingServices) { | ||
73 | return ( | ||
74 | <AppLoader /> | ||
75 | ); | ||
76 | } | ||
77 | |||
78 | const sidebar = ( | ||
79 | <Sidebar | ||
80 | services={allServices} | ||
81 | setActive={setActive} | ||
82 | openSettings={openSettings} | ||
83 | closeSettings={closeSettings} | ||
84 | reorder={reorder} | ||
85 | reload={reload} | ||
86 | toggleNotifications={toggleNotifications} | ||
87 | deleteService={deleteService} | ||
88 | updateService={updateService} | ||
89 | isPremiumUser={user.data.isPremium} | ||
90 | /> | ||
91 | ); | ||
92 | |||
93 | const servicesContainer = ( | ||
94 | <Services | ||
95 | // settings={allSettings} | ||
96 | services={allServices} | ||
97 | handleIPCMessage={handleIPCMessage} | ||
98 | setWebviewReference={setWebviewReference} | ||
99 | openWindow={openWindow} | ||
100 | /> | ||
101 | ); | ||
102 | |||
103 | return ( | ||
104 | <AppLayout | ||
105 | isOnline={app.isOnline} | ||
106 | showServicesUpdatedInfoBar={ui.showServicesUpdatedInfoBar} | ||
107 | appUpdateIsDownloaded={app.updateStatus === app.updateStatusTypes.DOWNLOADED} | ||
108 | sidebar={sidebar} | ||
109 | services={servicesContainer} | ||
110 | news={news.latest} | ||
111 | removeNewsItem={hide} | ||
112 | reloadServicesAfterUpdate={reloadUpdatedServices} | ||
113 | installAppUpdate={installUpdate} | ||
114 | globalError={globalError.error} | ||
115 | showRequiredRequestsError={requests.showRequiredRequestsError} | ||
116 | areRequiredRequestsSuccessful={requests.areRequiredRequestsSuccessful} | ||
117 | retryRequiredRequests={retryRequiredRequests} | ||
118 | areRequiredRequestsLoading={requests.areRequiredRequestsLoading} | ||
119 | > | ||
120 | {React.Children.count(children) > 0 ? children : null} | ||
121 | </AppLayout> | ||
122 | ); | ||
123 | } | ||
124 | } | ||
125 | |||
126 | AppLayoutContainer.wrappedComponent.propTypes = { | ||
127 | stores: PropTypes.shape({ | ||
128 | services: PropTypes.instanceOf(ServicesStore).isRequired, | ||
129 | recipes: PropTypes.instanceOf(RecipesStore).isRequired, | ||
130 | app: PropTypes.instanceOf(AppStore).isRequired, | ||
131 | ui: PropTypes.instanceOf(UIStore).isRequired, | ||
132 | news: PropTypes.instanceOf(NewsStore).isRequired, | ||
133 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
134 | requests: PropTypes.instanceOf(RequestStore).isRequired, | ||
135 | globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired, | ||
136 | }).isRequired, | ||
137 | actions: PropTypes.shape({ | ||
138 | service: PropTypes.shape({ | ||
139 | setActive: PropTypes.func.isRequired, | ||
140 | reload: PropTypes.func.isRequired, | ||
141 | toggleNotifications: PropTypes.func.isRequired, | ||
142 | handleIPCMessage: PropTypes.func.isRequired, | ||
143 | setWebviewReference: PropTypes.func.isRequired, | ||
144 | openWindow: PropTypes.func.isRequired, | ||
145 | reloadUpdatedServices: PropTypes.func.isRequired, | ||
146 | updateService: PropTypes.func.isRequired, | ||
147 | deleteService: PropTypes.func.isRequired, | ||
148 | reorder: PropTypes.func.isRequired, | ||
149 | }).isRequired, | ||
150 | news: PropTypes.shape({ | ||
151 | hide: PropTypes.func.isRequired, | ||
152 | }).isRequired, | ||
153 | ui: PropTypes.shape({ | ||
154 | openSettings: PropTypes.func.isRequired, | ||
155 | closeSettings: PropTypes.func.isRequired, | ||
156 | }).isRequired, | ||
157 | app: PropTypes.shape({ | ||
158 | installUpdate: PropTypes.func.isRequired, | ||
159 | healthCheck: PropTypes.func.isRequired, | ||
160 | }).isRequired, | ||
161 | requests: PropTypes.shape({ | ||
162 | retryRequiredRequests: PropTypes.func.isRequired, | ||
163 | }).isRequired, | ||
164 | }).isRequired, | ||
165 | children: oneOrManyChildElements, | ||
166 | }; | ||
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js new file mode 100644 index 000000000..a1ac8bda3 --- /dev/null +++ b/src/containers/settings/AccountScreen.js | |||
@@ -0,0 +1,114 @@ | |||
1 | import { remote } from 'electron'; | ||
2 | import React, { Component } from 'react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { inject, observer } from 'mobx-react'; | ||
5 | |||
6 | import PaymentStore from '../../stores/PaymentStore'; | ||
7 | import UserStore from '../../stores/UserStore'; | ||
8 | import AppStore from '../../stores/AppStore'; | ||
9 | import { gaPage } from '../../lib/analytics'; | ||
10 | |||
11 | import AccountDashboard from '../../components/settings/account/AccountDashboard'; | ||
12 | |||
13 | const { BrowserWindow } = remote; | ||
14 | |||
15 | @inject('stores', 'actions') @observer | ||
16 | export default class AccountScreen extends Component { | ||
17 | componentDidMount() { | ||
18 | gaPage('Settings/Account Dashboard'); | ||
19 | } | ||
20 | |||
21 | onCloseWindow() { | ||
22 | const { user, payment } = this.props.stores; | ||
23 | user.getUserInfoRequest.invalidate({ immediately: true }); | ||
24 | payment.ordersDataRequest.invalidate({ immediately: true }); | ||
25 | } | ||
26 | |||
27 | reloadData() { | ||
28 | const { user, payment } = this.props.stores; | ||
29 | |||
30 | user.getUserInfoRequest.reload(); | ||
31 | payment.ordersDataRequest.reload(); | ||
32 | payment.plansRequest.reload(); | ||
33 | } | ||
34 | |||
35 | stopMiner() { | ||
36 | const { update } = this.props.actions.user; | ||
37 | |||
38 | update({ userData: { | ||
39 | isMiner: false, | ||
40 | } }); | ||
41 | } | ||
42 | |||
43 | async handlePaymentDashboard() { | ||
44 | const { actions, stores } = this.props; | ||
45 | |||
46 | actions.payment.createDashboardUrl(); | ||
47 | |||
48 | const dashboard = await stores.payment.createDashboardUrlRequest; | ||
49 | |||
50 | if (dashboard.url) { | ||
51 | const paymentWindow = new BrowserWindow({ | ||
52 | title: '🔒 Franz Subscription Dashboard', | ||
53 | parent: remote.getCurrentWindow(), | ||
54 | modal: false, | ||
55 | width: 900, | ||
56 | minWidth: 600, | ||
57 | webPreferences: { | ||
58 | nodeIntegration: false, | ||
59 | }, | ||
60 | }); | ||
61 | paymentWindow.loadURL(dashboard.url); | ||
62 | |||
63 | paymentWindow.on('closed', () => { | ||
64 | this.onCloseWindow(); | ||
65 | }); | ||
66 | } | ||
67 | } | ||
68 | |||
69 | render() { | ||
70 | const { user, payment, app } = this.props.stores; | ||
71 | const { openExternalUrl } = this.props.actions.app; | ||
72 | |||
73 | const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; | ||
74 | const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting; | ||
75 | const isLoadingPlans = payment.plansRequest.isExecuting; | ||
76 | |||
77 | return ( | ||
78 | <AccountDashboard | ||
79 | user={user.data} | ||
80 | orders={payment.orders} | ||
81 | hashrate={app.minerHashrate} | ||
82 | isLoading={isLoadingUserInfo} | ||
83 | isLoadingOrdersInfo={isLoadingOrdersInfo} | ||
84 | isLoadingPlans={isLoadingPlans} | ||
85 | userInfoRequestFailed={user.getUserInfoRequest.wasExecuted && user.getUserInfoRequest.isError} | ||
86 | retryUserInfoRequest={() => this.reloadData()} | ||
87 | isCreatingPaymentDashboardUrl={payment.createDashboardUrlRequest.isExecuting} | ||
88 | openDashboard={price => this.handlePaymentDashboard(price)} | ||
89 | openExternalUrl={url => openExternalUrl({ url })} | ||
90 | onCloseSubscriptionWindow={() => this.onCloseWindow()} | ||
91 | stopMiner={() => this.stopMiner()} | ||
92 | /> | ||
93 | ); | ||
94 | } | ||
95 | } | ||
96 | |||
97 | AccountScreen.wrappedComponent.propTypes = { | ||
98 | stores: PropTypes.shape({ | ||
99 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
100 | payment: PropTypes.instanceOf(PaymentStore).isRequired, | ||
101 | app: PropTypes.instanceOf(AppStore).isRequired, | ||
102 | }).isRequired, | ||
103 | actions: PropTypes.shape({ | ||
104 | payment: PropTypes.shape({ | ||
105 | createDashboardUrl: PropTypes.func.isRequired, | ||
106 | }).isRequired, | ||
107 | app: PropTypes.shape({ | ||
108 | openExternalUrl: PropTypes.func.isRequired, | ||
109 | }).isRequired, | ||
110 | user: PropTypes.shape({ | ||
111 | update: PropTypes.func.isRequired, | ||
112 | }).isRequired, | ||
113 | }).isRequired, | ||
114 | }; | ||
diff --git a/src/containers/settings/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js new file mode 100644 index 000000000..6c614b941 --- /dev/null +++ b/src/containers/settings/EditServiceScreen.js | |||
@@ -0,0 +1,208 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import UserStore from '../../stores/UserStore'; | ||
7 | import RecipesStore from '../../stores/RecipesStore'; | ||
8 | import ServicesStore from '../../stores/ServicesStore'; | ||
9 | import Form from '../../lib/Form'; | ||
10 | import { gaPage } from '../../lib/analytics'; | ||
11 | |||
12 | |||
13 | import ServiceError from '../../components/settings/services/ServiceError'; | ||
14 | import EditServiceForm from '../../components/settings/services/EditServiceForm'; | ||
15 | import { required, url, oneRequired } from '../../helpers/validation-helpers'; | ||
16 | |||
17 | const messages = defineMessages({ | ||
18 | name: { | ||
19 | id: 'settings.service.form.name', | ||
20 | defaultMessage: '!!!Name', | ||
21 | }, | ||
22 | enableService: { | ||
23 | id: 'settings.service.form.enableService', | ||
24 | defaultMessage: '!!!Enable service', | ||
25 | }, | ||
26 | enableNotification: { | ||
27 | id: 'settings.service.form.enableNotification', | ||
28 | defaultMessage: '!!!Enable Notifications', | ||
29 | }, | ||
30 | team: { | ||
31 | id: 'settings.service.form.team', | ||
32 | defaultMessage: '!!!Team', | ||
33 | }, | ||
34 | customUrl: { | ||
35 | id: 'settings.service.form.customUrl', | ||
36 | defaultMessage: '!!!Custom server', | ||
37 | }, | ||
38 | indirectMessages: { | ||
39 | id: 'settings.service.form.indirectMessages', | ||
40 | defaultMessage: '!!!Show message badge for all new messages', | ||
41 | }, | ||
42 | }); | ||
43 | |||
44 | @inject('stores', 'actions') @observer | ||
45 | export default class EditServiceScreen extends Component { | ||
46 | static contextTypes = { | ||
47 | intl: intlShape, | ||
48 | }; | ||
49 | |||
50 | componentDidMount() { | ||
51 | gaPage('Settings/Service/Edit'); | ||
52 | } | ||
53 | |||
54 | onSubmit(serviceData) { | ||
55 | const { action } = this.props.router.params; | ||
56 | const { recipes, services } = this.props.stores; | ||
57 | const { createService, updateService } = this.props.actions.service; | ||
58 | |||
59 | if (action === 'edit') { | ||
60 | updateService({ serviceId: services.activeSettings.id, serviceData }); | ||
61 | } else { | ||
62 | createService({ recipeId: recipes.active.id, serviceData }); | ||
63 | } | ||
64 | } | ||
65 | |||
66 | prepareForm(recipe, service) { | ||
67 | const { intl } = this.context; | ||
68 | const config = { | ||
69 | fields: { | ||
70 | name: { | ||
71 | label: intl.formatMessage(messages.name), | ||
72 | placeholder: intl.formatMessage(messages.name), | ||
73 | value: service.id ? service.name : recipe.name, | ||
74 | }, | ||
75 | isEnabled: { | ||
76 | label: intl.formatMessage(messages.enableService), | ||
77 | value: service.isEnabled, | ||
78 | default: true, | ||
79 | }, | ||
80 | isNotificationEnabled: { | ||
81 | label: intl.formatMessage(messages.enableNotification), | ||
82 | value: service.isNotificationEnabled, | ||
83 | default: true, | ||
84 | }, | ||
85 | }, | ||
86 | }; | ||
87 | |||
88 | if (recipe.hasTeamId) { | ||
89 | Object.assign(config.fields, { | ||
90 | team: { | ||
91 | label: intl.formatMessage(messages.team), | ||
92 | placeholder: intl.formatMessage(messages.team), | ||
93 | value: service.team, | ||
94 | validate: [required], | ||
95 | }, | ||
96 | }); | ||
97 | } | ||
98 | |||
99 | if (recipe.hasCustomUrl) { | ||
100 | Object.assign(config.fields, { | ||
101 | customUrl: { | ||
102 | label: intl.formatMessage(messages.customUrl), | ||
103 | placeholder: 'https://', | ||
104 | value: service.customUrl, | ||
105 | validate: [required, url], | ||
106 | }, | ||
107 | }); | ||
108 | } | ||
109 | |||
110 | if (recipe.hasTeamId && recipe.hasCustomUrl) { | ||
111 | config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; | ||
112 | config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; | ||
113 | } | ||
114 | |||
115 | if (recipe.hasIndirectMessages) { | ||
116 | Object.assign(config.fields, { | ||
117 | isIndirectMessageBadgeEnabled: { | ||
118 | label: intl.formatMessage(messages.indirectMessages), | ||
119 | value: service.isIndirectMessageBadgeEnabled, | ||
120 | default: true, | ||
121 | }, | ||
122 | }); | ||
123 | } | ||
124 | |||
125 | return new Form(config); | ||
126 | } | ||
127 | |||
128 | deleteService() { | ||
129 | const { deleteService } = this.props.actions.service; | ||
130 | const { action } = this.props.router.params; | ||
131 | |||
132 | if (action === 'edit') { | ||
133 | const { activeSettings: service } = this.props.stores.services; | ||
134 | deleteService({ | ||
135 | serviceId: service.id, | ||
136 | redirect: '/settings/services', | ||
137 | }); | ||
138 | } | ||
139 | } | ||
140 | |||
141 | render() { | ||
142 | const { recipes, services, user } = this.props.stores; | ||
143 | const { action } = this.props.router.params; | ||
144 | |||
145 | let recipe; | ||
146 | let service = {}; | ||
147 | let isLoading = false; | ||
148 | |||
149 | if (action === 'add') { | ||
150 | recipe = recipes.active; | ||
151 | |||
152 | // TODO: render error message when recipe is `null` | ||
153 | if (!recipe) { | ||
154 | return ( | ||
155 | <ServiceError /> | ||
156 | ); | ||
157 | } | ||
158 | } else { | ||
159 | service = services.activeSettings; | ||
160 | isLoading = services.allServicesRequest.isExecuting; | ||
161 | |||
162 | if (!isLoading && service) { | ||
163 | recipe = service.recipe; | ||
164 | } | ||
165 | } | ||
166 | |||
167 | if (isLoading) { | ||
168 | return (<div>Loading...</div>); | ||
169 | } | ||
170 | |||
171 | const form = this.prepareForm(recipe, service); | ||
172 | |||
173 | return ( | ||
174 | <EditServiceForm | ||
175 | action={action} | ||
176 | recipe={recipe} | ||
177 | service={service} | ||
178 | user={user.data} | ||
179 | form={form} | ||
180 | status={services.actionStatus} | ||
181 | isSaving={services.updateServiceRequest.isExecuting || services.createServiceRequest.isExecuting} | ||
182 | isDeleting={services.deleteServiceRequest.isExecuting} | ||
183 | onSubmit={d => this.onSubmit(d)} | ||
184 | onDelete={() => this.deleteService()} | ||
185 | /> | ||
186 | ); | ||
187 | } | ||
188 | } | ||
189 | |||
190 | EditServiceScreen.wrappedComponent.propTypes = { | ||
191 | stores: PropTypes.shape({ | ||
192 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
193 | recipes: PropTypes.instanceOf(RecipesStore).isRequired, | ||
194 | services: PropTypes.instanceOf(ServicesStore).isRequired, | ||
195 | }).isRequired, | ||
196 | router: PropTypes.shape({ | ||
197 | params: PropTypes.shape({ | ||
198 | action: PropTypes.string.isRequired, | ||
199 | }).isRequired, | ||
200 | }).isRequired, | ||
201 | actions: PropTypes.shape({ | ||
202 | service: PropTypes.shape({ | ||
203 | createService: PropTypes.func.isRequired, | ||
204 | updateService: PropTypes.func.isRequired, | ||
205 | deleteService: PropTypes.func.isRequired, | ||
206 | }).isRequired, | ||
207 | }).isRequired, | ||
208 | }; | ||
diff --git a/src/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js new file mode 100644 index 000000000..0e17cafce --- /dev/null +++ b/src/containers/settings/EditSettingsScreen.js | |||
@@ -0,0 +1,167 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import AppStore from '../../stores/AppStore'; | ||
7 | import SettingsStore from '../../stores/SettingsStore'; | ||
8 | import UserStore from '../../stores/UserStore'; | ||
9 | import Form from '../../lib/Form'; | ||
10 | import languages from '../../i18n/languages'; | ||
11 | import { gaPage } from '../../lib/analytics'; | ||
12 | |||
13 | |||
14 | import EditSettingsForm from '../../components/settings/settings/EditSettingsForm'; | ||
15 | |||
16 | const messages = defineMessages({ | ||
17 | autoLaunchOnStart: { | ||
18 | id: 'settings.app.form.autoLaunchOnStart', | ||
19 | defaultMessage: '!!!Launch Franz on start', | ||
20 | }, | ||
21 | autoLaunchInBackground: { | ||
22 | id: 'settings.app.form.autoLaunchInBackground', | ||
23 | defaultMessage: '!!!Open in background', | ||
24 | }, | ||
25 | runInBackground: { | ||
26 | id: 'settings.app.form.runInBackground', | ||
27 | defaultMessage: '!!!Keep Franz in background when closing the window', | ||
28 | }, | ||
29 | minimizeToSystemTray: { | ||
30 | id: 'settings.app.form.minimizeToSystemTray', | ||
31 | defaultMessage: '!!!Minimize Franz to system tray', | ||
32 | }, | ||
33 | language: { | ||
34 | id: 'settings.app.form.language', | ||
35 | defaultMessage: '!!!Language', | ||
36 | }, | ||
37 | beta: { | ||
38 | id: 'settings.app.form.beta', | ||
39 | defaultMessage: '!!!Include beta versions', | ||
40 | }, | ||
41 | }); | ||
42 | |||
43 | @inject('stores', 'actions') @observer | ||
44 | export default class EditSettingsScreen extends Component { | ||
45 | static contextTypes = { | ||
46 | intl: intlShape, | ||
47 | }; | ||
48 | |||
49 | componentDidMount() { | ||
50 | gaPage('Settings/App'); | ||
51 | } | ||
52 | |||
53 | onSubmit(settingsData) { | ||
54 | const { app, settings, user } = this.props.actions; | ||
55 | |||
56 | app.launchOnStartup({ | ||
57 | enable: settingsData.autoLaunchOnStart, | ||
58 | openInBackground: settingsData.autoLaunchInBackground, | ||
59 | }); | ||
60 | |||
61 | settings.update({ | ||
62 | settings: { | ||
63 | runInBackground: settingsData.runInBackground, | ||
64 | minimizeToSystemTray: settingsData.minimizeToSystemTray, | ||
65 | locale: settingsData.locale, | ||
66 | beta: settingsData.beta, | ||
67 | }, | ||
68 | }); | ||
69 | |||
70 | user.update({ | ||
71 | userData: { | ||
72 | beta: settingsData.beta, | ||
73 | }, | ||
74 | }); | ||
75 | } | ||
76 | |||
77 | prepareForm() { | ||
78 | const { app, settings, user } = this.props.stores; | ||
79 | const { intl } = this.context; | ||
80 | |||
81 | const options = []; | ||
82 | Object.keys(languages).forEach((key) => { | ||
83 | options.push({ | ||
84 | value: key, | ||
85 | label: languages[key], | ||
86 | }); | ||
87 | }); | ||
88 | |||
89 | const config = { | ||
90 | fields: { | ||
91 | autoLaunchOnStart: { | ||
92 | label: intl.formatMessage(messages.autoLaunchOnStart), | ||
93 | value: app.autoLaunchOnStart, | ||
94 | default: true, | ||
95 | }, | ||
96 | autoLaunchInBackground: { | ||
97 | label: intl.formatMessage(messages.autoLaunchInBackground), | ||
98 | value: app.launchInBackground, | ||
99 | default: false, | ||
100 | }, | ||
101 | runInBackground: { | ||
102 | label: intl.formatMessage(messages.runInBackground), | ||
103 | value: settings.all.runInBackground, | ||
104 | default: true, | ||
105 | }, | ||
106 | minimizeToSystemTray: { | ||
107 | label: intl.formatMessage(messages.minimizeToSystemTray), | ||
108 | value: settings.all.minimizeToSystemTray, | ||
109 | default: false, | ||
110 | }, | ||
111 | locale: { | ||
112 | label: intl.formatMessage(messages.language), | ||
113 | value: app.locale, | ||
114 | options, | ||
115 | default: 'en-US', | ||
116 | }, | ||
117 | beta: { | ||
118 | label: intl.formatMessage(messages.beta), | ||
119 | value: user.data.beta, | ||
120 | default: false, | ||
121 | }, | ||
122 | }, | ||
123 | }; | ||
124 | |||
125 | return new Form(config); | ||
126 | } | ||
127 | |||
128 | render() { | ||
129 | const { updateStatus, updateStatusTypes } = this.props.stores.app; | ||
130 | const { checkForUpdates, installUpdate } = this.props.actions.app; | ||
131 | const form = this.prepareForm(); | ||
132 | |||
133 | return ( | ||
134 | <EditSettingsForm | ||
135 | form={form} | ||
136 | checkForUpdates={checkForUpdates} | ||
137 | installUpdate={installUpdate} | ||
138 | isCheckingForUpdates={updateStatus === updateStatusTypes.CHECKING} | ||
139 | isUpdateAvailable={updateStatus === updateStatusTypes.AVAILABLE} | ||
140 | noUpdateAvailable={updateStatus === updateStatusTypes.NOT_AVAILABLE} | ||
141 | updateIsReadyToInstall={updateStatus === updateStatusTypes.DOWNLOADED} | ||
142 | onSubmit={d => this.onSubmit(d)} | ||
143 | /> | ||
144 | ); | ||
145 | } | ||
146 | } | ||
147 | |||
148 | EditSettingsScreen.wrappedComponent.propTypes = { | ||
149 | stores: PropTypes.shape({ | ||
150 | app: PropTypes.instanceOf(AppStore).isRequired, | ||
151 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
152 | settings: PropTypes.instanceOf(SettingsStore).isRequired, | ||
153 | }).isRequired, | ||
154 | actions: PropTypes.shape({ | ||
155 | app: PropTypes.shape({ | ||
156 | launchOnStartup: PropTypes.func.isRequired, | ||
157 | checkForUpdates: PropTypes.func.isRequired, | ||
158 | installUpdate: PropTypes.func.isRequired, | ||
159 | }).isRequired, | ||
160 | settings: PropTypes.shape({ | ||
161 | update: PropTypes.func.isRequired, | ||
162 | }).isRequired, | ||
163 | user: PropTypes.shape({ | ||
164 | update: PropTypes.func.isRequired, | ||
165 | }).isRequired, | ||
166 | }).isRequired, | ||
167 | }; | ||
diff --git a/src/containers/settings/EditUserScreen.js b/src/containers/settings/EditUserScreen.js new file mode 100644 index 000000000..fb5c5db89 --- /dev/null +++ b/src/containers/settings/EditUserScreen.js | |||
@@ -0,0 +1,165 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import UserStore from '../../stores/UserStore'; | ||
7 | import Form from '../../lib/Form'; | ||
8 | import EditUserForm from '../../components/settings/user/EditUserForm'; | ||
9 | import { required, email, minLength } from '../../helpers/validation-helpers'; | ||
10 | import { gaPage } from '../../lib/analytics'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | firstname: { | ||
14 | id: 'settings.user.form.firstname', | ||
15 | defaultMessage: '!!!Firstname', | ||
16 | }, | ||
17 | lastname: { | ||
18 | id: 'settings.user.form.lastname', | ||
19 | defaultMessage: '!!!Lastname', | ||
20 | }, | ||
21 | email: { | ||
22 | id: 'settings.user.form.email', | ||
23 | defaultMessage: '!!!Email', | ||
24 | }, | ||
25 | accountType: { | ||
26 | label: { | ||
27 | id: 'settings.user.form.accountType.label', | ||
28 | defaultMessage: '!!!Account type', | ||
29 | }, | ||
30 | individual: { | ||
31 | id: 'settings.user.form.accountType.individual', | ||
32 | defaultMessage: '!!!Individual', | ||
33 | }, | ||
34 | nonProfit: { | ||
35 | id: 'settings.user.form.accountType.non-profit', | ||
36 | defaultMessage: '!!!Non-Profit', | ||
37 | }, | ||
38 | company: { | ||
39 | id: 'settings.user.form.accountType.company', | ||
40 | defaultMessage: '!!!Company', | ||
41 | }, | ||
42 | }, | ||
43 | currentPassword: { | ||
44 | id: 'settings.user.form.currentPassword', | ||
45 | defaultMessage: '!!!Current password', | ||
46 | }, | ||
47 | newPassword: { | ||
48 | id: 'settings.user.form.newPassword', | ||
49 | defaultMessage: '!!!New password', | ||
50 | }, | ||
51 | }); | ||
52 | |||
53 | @inject('stores', 'actions') @observer | ||
54 | export default class EditUserScreen extends Component { | ||
55 | static contextTypes = { | ||
56 | intl: intlShape, | ||
57 | }; | ||
58 | |||
59 | componentDidMount() { | ||
60 | gaPage('Settings/Account/Edit'); | ||
61 | } | ||
62 | |||
63 | componentWillUnmount() { | ||
64 | this.props.actions.user.resetStatus(); | ||
65 | } | ||
66 | |||
67 | onSubmit(userData) { | ||
68 | const { update } = this.props.actions.user; | ||
69 | |||
70 | update({ userData }); | ||
71 | |||
72 | document.querySelector('#form').scrollIntoView({ behavior: 'smooth' }); | ||
73 | } | ||
74 | |||
75 | prepareForm(user) { | ||
76 | const { intl } = this.context; | ||
77 | |||
78 | const config = { | ||
79 | fields: { | ||
80 | firstname: { | ||
81 | label: intl.formatMessage(messages.firstname), | ||
82 | placeholder: intl.formatMessage(messages.firstname), | ||
83 | value: user.firstname, | ||
84 | validate: [required], | ||
85 | }, | ||
86 | lastname: { | ||
87 | label: intl.formatMessage(messages.lastname), | ||
88 | placeholder: intl.formatMessage(messages.lastname), | ||
89 | value: user.lastname, | ||
90 | validate: [required], | ||
91 | }, | ||
92 | email: { | ||
93 | label: intl.formatMessage(messages.email), | ||
94 | placeholder: intl.formatMessage(messages.email), | ||
95 | value: user.email, | ||
96 | validate: [required, email], | ||
97 | }, | ||
98 | accountType: { | ||
99 | value: user.accountType, | ||
100 | validate: [required], | ||
101 | label: intl.formatMessage(messages.accountType.label), | ||
102 | options: [{ | ||
103 | value: 'individual', | ||
104 | label: intl.formatMessage(messages.accountType.individual), | ||
105 | }, { | ||
106 | value: 'non-profit', | ||
107 | label: intl.formatMessage(messages.accountType.nonProfit), | ||
108 | }, { | ||
109 | value: 'company', | ||
110 | label: intl.formatMessage(messages.accountType.company), | ||
111 | }], | ||
112 | }, | ||
113 | organization: { | ||
114 | label: intl.formatMessage(messages.accountType.company), | ||
115 | placeholder: intl.formatMessage(messages.accountType.company), | ||
116 | value: user.organization, | ||
117 | }, | ||
118 | oldPassword: { | ||
119 | label: intl.formatMessage(messages.currentPassword), | ||
120 | type: 'password', | ||
121 | validate: [minLength(6)], | ||
122 | }, | ||
123 | newPassword: { | ||
124 | label: intl.formatMessage(messages.newPassword), | ||
125 | type: 'password', | ||
126 | validate: [minLength(6)], | ||
127 | }, | ||
128 | }, | ||
129 | }; | ||
130 | |||
131 | return new Form(config); | ||
132 | } | ||
133 | |||
134 | render() { | ||
135 | const { user } = this.props.stores; | ||
136 | |||
137 | if (user.getUserInfoRequest.isExecuting) { | ||
138 | return (<div>Loading...</div>); | ||
139 | } | ||
140 | |||
141 | const form = this.prepareForm(user.data); | ||
142 | |||
143 | return ( | ||
144 | <EditUserForm | ||
145 | // user={user.data} | ||
146 | status={user.actionStatus} | ||
147 | form={form} | ||
148 | isSaving={user.updateUserInfoRequest.isExecuting} | ||
149 | onSubmit={d => this.onSubmit(d)} | ||
150 | /> | ||
151 | ); | ||
152 | } | ||
153 | } | ||
154 | |||
155 | EditUserScreen.wrappedComponent.propTypes = { | ||
156 | stores: PropTypes.shape({ | ||
157 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
158 | }).isRequired, | ||
159 | actions: PropTypes.shape({ | ||
160 | user: PropTypes.shape({ | ||
161 | update: PropTypes.func.isRequired, | ||
162 | resetStatus: PropTypes.func.isRequired, | ||
163 | }).isRequired, | ||
164 | }).isRequired, | ||
165 | }; | ||
diff --git a/src/containers/settings/RecipesScreen.js b/src/containers/settings/RecipesScreen.js new file mode 100644 index 000000000..65341e9e3 --- /dev/null +++ b/src/containers/settings/RecipesScreen.js | |||
@@ -0,0 +1,126 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { autorun } from 'mobx'; | ||
4 | import { inject, observer } from 'mobx-react'; | ||
5 | |||
6 | import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; | ||
7 | import RecipeStore from '../../stores/RecipesStore'; | ||
8 | import ServiceStore from '../../stores/ServicesStore'; | ||
9 | import UserStore from '../../stores/UserStore'; | ||
10 | import { gaPage } from '../../lib/analytics'; | ||
11 | |||
12 | import RecipesDashboard from '../../components/settings/recipes/RecipesDashboard'; | ||
13 | |||
14 | @inject('stores', 'actions') @observer | ||
15 | export default class RecipesScreen extends Component { | ||
16 | static propTypes = { | ||
17 | params: PropTypes.shape({ | ||
18 | filter: PropTypes.string, | ||
19 | }).isRequired, | ||
20 | }; | ||
21 | |||
22 | static defaultProps = { | ||
23 | params: { | ||
24 | filter: null, | ||
25 | }, | ||
26 | }; | ||
27 | |||
28 | state = { | ||
29 | needle: null, | ||
30 | currentFilter: 'featured', | ||
31 | }; | ||
32 | |||
33 | componentDidMount() { | ||
34 | gaPage('Settings/Recipe Dashboard/Featured'); | ||
35 | |||
36 | autorun(() => { | ||
37 | const { filter } = this.props.params; | ||
38 | const { currentFilter } = this.state; | ||
39 | |||
40 | if (filter === 'all' && currentFilter !== 'all') { | ||
41 | gaPage('Settings/Recipe Dashboard/All'); | ||
42 | this.setState({ currentFilter: 'all' }); | ||
43 | } else if (filter === 'featured' && currentFilter !== 'featured') { | ||
44 | gaPage('Settings/Recipe Dashboard/Featured'); | ||
45 | this.setState({ currentFilter: 'featured' }); | ||
46 | } else if (filter === 'dev' && currentFilter !== 'dev') { | ||
47 | gaPage('Settings/Recipe Dashboard/Dev'); | ||
48 | this.setState({ currentFilter: 'dev' }); | ||
49 | } | ||
50 | }); | ||
51 | } | ||
52 | |||
53 | componentWillUnmount() { | ||
54 | this.props.stores.services.resetStatus(); | ||
55 | } | ||
56 | |||
57 | searchRecipes(needle) { | ||
58 | if (needle === '') { | ||
59 | this.resetSearch(); | ||
60 | } else { | ||
61 | const { search } = this.props.actions.recipePreview; | ||
62 | this.setState({ needle }); | ||
63 | search({ needle }); | ||
64 | } | ||
65 | } | ||
66 | |||
67 | resetSearch() { | ||
68 | this.setState({ needle: null }); | ||
69 | } | ||
70 | |||
71 | render() { | ||
72 | const { recipePreviews, recipes, services, user } = this.props.stores; | ||
73 | const { showAddServiceInterface } = this.props.actions.service; | ||
74 | |||
75 | const { filter } = this.props.params; | ||
76 | let recipeFilter; | ||
77 | |||
78 | if (filter === 'all') { | ||
79 | recipeFilter = recipePreviews.all; | ||
80 | } else if (filter === 'dev') { | ||
81 | recipeFilter = recipePreviews.dev; | ||
82 | } else { | ||
83 | recipeFilter = recipePreviews.featured; | ||
84 | } | ||
85 | |||
86 | const allRecipes = this.state.needle ? recipePreviews.searchResults : recipeFilter; | ||
87 | |||
88 | const isLoading = recipePreviews.featuredRecipePreviewsRequest.isExecuting | ||
89 | || recipePreviews.allRecipePreviewsRequest.isExecuting | ||
90 | || recipes.installRecipeRequest.isExecuting | ||
91 | || recipePreviews.searchRecipePreviewsRequest.isExecuting; | ||
92 | |||
93 | return ( | ||
94 | <RecipesDashboard | ||
95 | recipes={allRecipes} | ||
96 | isLoading={isLoading} | ||
97 | addedServiceCount={services.all.length} | ||
98 | isPremium={user.data.isPremium} | ||
99 | hasLoadedRecipes={recipePreviews.featuredRecipePreviewsRequest.wasExecuted} | ||
100 | showAddServiceInterface={showAddServiceInterface} | ||
101 | searchRecipes={e => this.searchRecipes(e)} | ||
102 | resetSearch={() => this.resetSearch()} | ||
103 | searchNeedle={this.state.needle} | ||
104 | serviceStatus={services.actionStatus} | ||
105 | devRecipesCount={recipePreviews.dev.length} | ||
106 | /> | ||
107 | ); | ||
108 | } | ||
109 | } | ||
110 | |||
111 | RecipesScreen.wrappedComponent.propTypes = { | ||
112 | stores: PropTypes.shape({ | ||
113 | recipePreviews: PropTypes.instanceOf(RecipePreviewsStore).isRequired, | ||
114 | recipes: PropTypes.instanceOf(RecipeStore).isRequired, | ||
115 | services: PropTypes.instanceOf(ServiceStore).isRequired, | ||
116 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
117 | }).isRequired, | ||
118 | actions: PropTypes.shape({ | ||
119 | service: PropTypes.shape({ | ||
120 | showAddServiceInterface: PropTypes.func.isRequired, | ||
121 | }).isRequired, | ||
122 | recipePreview: PropTypes.shape({ | ||
123 | search: PropTypes.func.isRequired, | ||
124 | }).isRequired, | ||
125 | }).isRequired, | ||
126 | }; | ||
diff --git a/src/containers/settings/ServicesScreen.js b/src/containers/settings/ServicesScreen.js new file mode 100644 index 000000000..d0580041f --- /dev/null +++ b/src/containers/settings/ServicesScreen.js | |||
@@ -0,0 +1,75 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import { RouterStore } from 'mobx-react-router'; | ||
5 | |||
6 | // import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; | ||
7 | import UserStore from '../../stores/UserStore'; | ||
8 | import ServiceStore from '../../stores/ServicesStore'; | ||
9 | import { gaPage } from '../../lib/analytics'; | ||
10 | |||
11 | import ServicesDashboard from '../../components/settings/services/ServicesDashboard'; | ||
12 | |||
13 | @inject('stores', 'actions') @observer | ||
14 | export default class ServicesScreen extends Component { | ||
15 | componentDidMount() { | ||
16 | gaPage('Settings/Service Dashboard'); | ||
17 | } | ||
18 | |||
19 | componentWillUnmount() { | ||
20 | this.props.actions.service.resetFilter(); | ||
21 | } | ||
22 | |||
23 | deleteService() { | ||
24 | this.props.actions.service.deleteService(); | ||
25 | this.props.stores.services.resetFilter(); | ||
26 | } | ||
27 | |||
28 | render() { | ||
29 | const { user, services, router } = this.props.stores; | ||
30 | const { | ||
31 | toggleService, | ||
32 | filter, | ||
33 | resetFilter, | ||
34 | } = this.props.actions.service; | ||
35 | const isLoading = services.allServicesRequest.isExecuting; | ||
36 | |||
37 | let allServices = services.all; | ||
38 | if (services.filterNeedle !== null) { | ||
39 | allServices = services.filtered; | ||
40 | } | ||
41 | |||
42 | return ( | ||
43 | <ServicesDashboard | ||
44 | user={user.data} | ||
45 | services={allServices} | ||
46 | status={services.actionStatus} | ||
47 | deleteService={() => this.deleteService()} | ||
48 | toggleService={toggleService} | ||
49 | isLoading={isLoading} | ||
50 | filterServices={filter} | ||
51 | resetFilter={resetFilter} | ||
52 | goTo={router.push} | ||
53 | servicesRequestFailed={services.allServicesRequest.wasExecuted && services.allServicesRequest.isError} | ||
54 | retryServicesRequest={() => services.allServicesRequest.reload()} | ||
55 | /> | ||
56 | ); | ||
57 | } | ||
58 | } | ||
59 | |||
60 | ServicesScreen.wrappedComponent.propTypes = { | ||
61 | stores: PropTypes.shape({ | ||
62 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
63 | services: PropTypes.instanceOf(ServiceStore).isRequired, | ||
64 | router: PropTypes.instanceOf(RouterStore).isRequired, | ||
65 | }).isRequired, | ||
66 | actions: PropTypes.shape({ | ||
67 | service: PropTypes.shape({ | ||
68 | showAddServiceInterface: PropTypes.func.isRequired, | ||
69 | deleteService: PropTypes.func.isRequired, | ||
70 | toggleService: PropTypes.func.isRequired, | ||
71 | filter: PropTypes.func.isRequired, | ||
72 | resetFilter: PropTypes.func.isRequired, | ||
73 | }).isRequired, | ||
74 | }).isRequired, | ||
75 | }; | ||
diff --git a/src/containers/settings/SettingsWindow.js b/src/containers/settings/SettingsWindow.js new file mode 100644 index 000000000..13ca96f72 --- /dev/null +++ b/src/containers/settings/SettingsWindow.js | |||
@@ -0,0 +1,43 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, inject } from 'mobx-react'; | ||
4 | |||
5 | import ServicesStore from '../../stores/ServicesStore'; | ||
6 | |||
7 | import Layout from '../../components/settings/SettingsLayout'; | ||
8 | import Navigation from '../../components/settings/navigation/SettingsNavigation'; | ||
9 | |||
10 | @inject('stores', 'actions') @observer | ||
11 | export default class SettingsContainer extends Component { | ||
12 | render() { | ||
13 | const { children, stores } = this.props; | ||
14 | const { closeSettings } = this.props.actions.ui; | ||
15 | |||
16 | const navigation = ( | ||
17 | <Navigation | ||
18 | serviceCount={stores.services.all.length} | ||
19 | /> | ||
20 | ); | ||
21 | |||
22 | return ( | ||
23 | <Layout | ||
24 | navigation={navigation} | ||
25 | closeSettings={closeSettings} | ||
26 | > | ||
27 | {children} | ||
28 | </Layout> | ||
29 | ); | ||
30 | } | ||
31 | } | ||
32 | |||
33 | SettingsContainer.wrappedComponent.propTypes = { | ||
34 | children: PropTypes.element.isRequired, | ||
35 | stores: PropTypes.shape({ | ||
36 | services: PropTypes.instanceOf(ServicesStore).isRequired, | ||
37 | }).isRequired, | ||
38 | actions: PropTypes.shape({ | ||
39 | ui: PropTypes.shape({ | ||
40 | closeSettings: PropTypes.func.isRequired, | ||
41 | }), | ||
42 | }).isRequired, | ||
43 | }; | ||
diff --git a/src/containers/ui/SubscriptionFormScreen.js b/src/containers/ui/SubscriptionFormScreen.js new file mode 100644 index 000000000..d08507809 --- /dev/null +++ b/src/containers/ui/SubscriptionFormScreen.js | |||
@@ -0,0 +1,126 @@ | |||
1 | import { remote } from 'electron'; | ||
2 | import React, { Component } from 'react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { inject, observer } from 'mobx-react'; | ||
5 | |||
6 | import PaymentStore from '../../stores/PaymentStore'; | ||
7 | |||
8 | import SubscriptionForm from '../../components/ui/Subscription'; | ||
9 | |||
10 | const { BrowserWindow } = remote; | ||
11 | |||
12 | @inject('stores', 'actions') @observer | ||
13 | export default class SubscriptionFormScreen extends Component { | ||
14 | static propTypes = { | ||
15 | onCloseWindow: PropTypes.func, | ||
16 | content: PropTypes.oneOrManyChildElements, | ||
17 | showSkipOption: PropTypes.bool, | ||
18 | skipAction: PropTypes.func, | ||
19 | skipButtonLabel: PropTypes.string, | ||
20 | hideInfo: PropTypes.bool, | ||
21 | } | ||
22 | |||
23 | static defaultProps = { | ||
24 | onCloseWindow: () => null, | ||
25 | content: '', | ||
26 | showSkipOption: false, | ||
27 | skipAction: () => null, | ||
28 | skipButtonLabel: '', | ||
29 | hideInfo: false, | ||
30 | } | ||
31 | |||
32 | async handlePayment(plan) { | ||
33 | const { | ||
34 | actions, | ||
35 | stores, | ||
36 | onCloseWindow, | ||
37 | skipAction, | ||
38 | } = this.props; | ||
39 | |||
40 | if (plan !== 'mining') { | ||
41 | const interval = plan; | ||
42 | |||
43 | const { id } = stores.payment.plan[interval]; | ||
44 | actions.payment.createHostedPage({ | ||
45 | planId: id, | ||
46 | }); | ||
47 | |||
48 | const hostedPage = await stores.payment.createHostedPageRequest; | ||
49 | const url = `file://${__dirname}/../../index.html#/payment/${encodeURIComponent(hostedPage.url)}`; | ||
50 | |||
51 | if (hostedPage.url) { | ||
52 | const paymentWindow = new BrowserWindow({ | ||
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 | |||
66 | paymentWindow.on('closed', () => { | ||
67 | onCloseWindow(); | ||
68 | }); | ||
69 | } | ||
70 | } else { | ||
71 | actions.user.update({ | ||
72 | userData: { | ||
73 | isMiner: true, | ||
74 | }, | ||
75 | }); | ||
76 | |||
77 | skipAction(); | ||
78 | } | ||
79 | } | ||
80 | |||
81 | render() { | ||
82 | const { | ||
83 | content, | ||
84 | actions, | ||
85 | stores, | ||
86 | showSkipOption, | ||
87 | skipAction, | ||
88 | skipButtonLabel, | ||
89 | hideInfo, | ||
90 | } = this.props; | ||
91 | return ( | ||
92 | <SubscriptionForm | ||
93 | plan={stores.payment.plan} | ||
94 | // form={this.prepareForm(stores.payment.plan)} | ||
95 | isLoading={stores.payment.plansRequest.isExecuting} | ||
96 | retryPlanRequest={() => stores.payment.plansRequest.reload()} | ||
97 | isCreatingHostedPage={stores.payment.createHostedPageRequest.isExecuting} | ||
98 | handlePayment={price => this.handlePayment(price)} | ||
99 | content={content} | ||
100 | error={stores.payment.plansRequest.isError} | ||
101 | showSkipOption={showSkipOption} | ||
102 | skipAction={skipAction} | ||
103 | skipButtonLabel={skipButtonLabel} | ||
104 | hideInfo={hideInfo} | ||
105 | openExternalUrl={actions.app.openExternalUrl} | ||
106 | /> | ||
107 | ); | ||
108 | } | ||
109 | } | ||
110 | |||
111 | SubscriptionFormScreen.wrappedComponent.propTypes = { | ||
112 | actions: PropTypes.shape({ | ||
113 | app: PropTypes.shape({ | ||
114 | openExternalUrl: PropTypes.func.isRequired, | ||
115 | }).isRequired, | ||
116 | payment: PropTypes.shape({ | ||
117 | createHostedPage: PropTypes.func.isRequired, | ||
118 | }).isRequired, | ||
119 | user: PropTypes.shape({ | ||
120 | update: PropTypes.func.isRequired, | ||
121 | }).isRequired, | ||
122 | }).isRequired, | ||
123 | stores: PropTypes.shape({ | ||
124 | payment: PropTypes.instanceOf(PaymentStore).isRequired, | ||
125 | }).isRequired, | ||
126 | }; | ||
diff --git a/src/containers/ui/SubscriptionPopupScreen.js b/src/containers/ui/SubscriptionPopupScreen.js new file mode 100644 index 000000000..d17477b1d --- /dev/null +++ b/src/containers/ui/SubscriptionPopupScreen.js | |||
@@ -0,0 +1,43 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | |||
5 | import SubscriptionPopup from '../../components/ui/SubscriptionPopup'; | ||
6 | |||
7 | |||
8 | @inject('stores', 'actions') @observer | ||
9 | export default class SubscriptionPopupScreen extends Component { | ||
10 | state = { | ||
11 | complete: false, | ||
12 | }; | ||
13 | |||
14 | completeCheck(event) { | ||
15 | const { url } = event; | ||
16 | |||
17 | if (url.includes('recurly') && url.includes('confirmation')) { | ||
18 | this.setState({ | ||
19 | complete: true, | ||
20 | }); | ||
21 | } | ||
22 | } | ||
23 | |||
24 | render() { | ||
25 | return ( | ||
26 | <SubscriptionPopup | ||
27 | url={decodeURIComponent(this.props.router.params.url)} | ||
28 | closeWindow={() => window.close()} | ||
29 | completeCheck={e => this.completeCheck(e)} | ||
30 | isCompleted={this.state.complete} | ||
31 | /> | ||
32 | ); | ||
33 | } | ||
34 | } | ||
35 | |||
36 | |||
37 | SubscriptionPopupScreen.wrappedComponent.propTypes = { | ||
38 | router: PropTypes.shape({ | ||
39 | params: PropTypes.shape({ | ||
40 | url: PropTypes.string.isRequired, | ||
41 | }).isRequired, | ||
42 | }).isRequired, | ||
43 | }; | ||