From 58cda9cc7fb79ca9df6746de7f9662bc08dc156a Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Fri, 13 Oct 2017 12:29:40 +0200 Subject: initial commit --- src/containers/auth/AuthLayoutContainer.js | 47 ++++++ src/containers/auth/ImportScreen.js | 41 +++++ src/containers/auth/InviteScreen.js | 29 ++++ src/containers/auth/LoginScreen.js | 45 ++++++ src/containers/auth/PasswordScreen.js | 38 +++++ src/containers/auth/PricingScreen.js | 53 +++++++ src/containers/auth/SignupScreen.js | 43 ++++++ src/containers/auth/WelcomeScreen.js | 34 +++++ src/containers/layout/AppLayoutContainer.js | 166 ++++++++++++++++++++ src/containers/settings/AccountScreen.js | 114 ++++++++++++++ src/containers/settings/EditServiceScreen.js | 208 ++++++++++++++++++++++++++ src/containers/settings/EditSettingsScreen.js | 167 +++++++++++++++++++++ src/containers/settings/EditUserScreen.js | 165 ++++++++++++++++++++ src/containers/settings/RecipesScreen.js | 126 ++++++++++++++++ src/containers/settings/ServicesScreen.js | 75 ++++++++++ src/containers/settings/SettingsWindow.js | 43 ++++++ src/containers/ui/SubscriptionFormScreen.js | 126 ++++++++++++++++ src/containers/ui/SubscriptionPopupScreen.js | 43 ++++++ 18 files changed, 1563 insertions(+) create mode 100644 src/containers/auth/AuthLayoutContainer.js create mode 100644 src/containers/auth/ImportScreen.js create mode 100644 src/containers/auth/InviteScreen.js create mode 100644 src/containers/auth/LoginScreen.js create mode 100644 src/containers/auth/PasswordScreen.js create mode 100644 src/containers/auth/PricingScreen.js create mode 100644 src/containers/auth/SignupScreen.js create mode 100644 src/containers/auth/WelcomeScreen.js create mode 100644 src/containers/layout/AppLayoutContainer.js create mode 100644 src/containers/settings/AccountScreen.js create mode 100644 src/containers/settings/EditServiceScreen.js create mode 100644 src/containers/settings/EditSettingsScreen.js create mode 100644 src/containers/settings/EditUserScreen.js create mode 100644 src/containers/settings/RecipesScreen.js create mode 100644 src/containers/settings/ServicesScreen.js create mode 100644 src/containers/settings/SettingsWindow.js create mode 100644 src/containers/ui/SubscriptionFormScreen.js create mode 100644 src/containers/ui/SubscriptionPopupScreen.js (limited to 'src/containers') 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; + +import AuthLayout from '../../components/auth/AuthLayout'; +import AppStore from '../../stores/AppStore'; +import GlobalErrorStore from '../../stores/GlobalErrorStore'; + +import { oneOrManyChildElements } from '../../prop-types'; + +@inject('stores', 'actions') @observer +export default class AuthLayoutContainer extends Component { + static propTypes = { + children: oneOrManyChildElements.isRequired, + location: PropTypes.shape({ + pathname: PropTypes.string.isRequired, + }).isRequired, + }; + + render() { + const { stores, actions, children, location } = this.props; + return ( + + {children} + + ); + } +} + +AuthLayoutContainer.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + app: PropTypes.instanceOf(AppStore).isRequired, + globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + app: PropTypes.shape({ + healthCheck: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import Import from '../../components/auth/Import'; +import UserStore from '../../stores/UserStore'; +import { gaPage } from '../../lib/analytics'; + +@inject('stores', 'actions') @observer +export default class ImportScreen extends Component { + componentDidMount() { + gaPage('Auth/Import'); + } + + render() { + const { actions, stores } = this.props; + + if (stores.user.isImportLegacyServicesCompleted) { + stores.router.push(stores.user.inviteRoute); + } + + return ( + + ); + } +} + +ImportScreen.wrappedComponent.propTypes = { + actions: PropTypes.shape({ + user: PropTypes.shape({ + importLegacyServices: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import Invite from '../../components/auth/Invite'; +import { gaPage } from '../../lib/analytics'; + +@inject('stores', 'actions') @observer +export default class InviteScreen extends Component { + componentDidMount() { + gaPage('Auth/Invite'); + } + + render() { + const { actions } = this.props; + return ( + + ); + } +} + +InviteScreen.wrappedComponent.propTypes = { + actions: PropTypes.shape({ + user: PropTypes.shape({ + invite: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import Login from '../../components/auth/Login'; +import UserStore from '../../stores/UserStore'; +import { gaPage } from '../../lib/analytics'; + +import { globalError as globalErrorPropType } from '../../prop-types'; + +@inject('stores', 'actions') @observer +export default class LoginScreen extends Component { + static propTypes = { + error: globalErrorPropType.isRequired, + }; + + componentDidMount() { + gaPage('Auth/Login'); + } + + render() { + const { actions, stores, error } = this.props; + return ( + + ); + } +} + +LoginScreen.wrappedComponent.propTypes = { + actions: PropTypes.shape({ + user: PropTypes.shape({ + login: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import Password from '../../components/auth/Password'; +import UserStore from '../../stores/UserStore'; +import { gaPage } from '../../lib/analytics'; + +@inject('stores', 'actions') @observer +export default class PasswordScreen extends Component { + componentDidMount() { + gaPage('Auth/Password Retrieve'); + } + + render() { + const { actions, stores } = this.props; + + return ( + + ); + } +} + +PasswordScreen.wrappedComponent.propTypes = { + actions: PropTypes.shape({ + user: PropTypes.shape({ + retrievePassword: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { RouterStore } from 'mobx-react-router'; + +import Pricing from '../../components/auth/Pricing'; +import UserStore from '../../stores/UserStore'; +import PaymentStore from '../../stores/PaymentStore'; +import { gaPage } from '../../lib/analytics'; + +import { globalError as globalErrorPropType } from '../../prop-types'; + +@inject('stores', 'actions') @observer +export default class PricingScreen extends Component { + static propTypes = { + error: globalErrorPropType.isRequired, + }; + + componentDidMount() { + gaPage('Auth/Pricing'); + } + + render() { + const { actions, stores, error } = this.props; + + const nextStepRoute = stores.user.legacyServices.length ? stores.user.importRoute : stores.user.inviteRoute; + + return ( + this.props.stores.router.push(nextStepRoute)} + isLoading={stores.payment.plansRequest.isExecuting} + isLoadingUser={stores.user.getUserInfoRequest.isExecuting} + error={error} + skipAction={() => this.props.stores.router.push(nextStepRoute)} + /> + ); + } +} + +PricingScreen.wrappedComponent.propTypes = { + actions: PropTypes.shape({ + user: PropTypes.shape({ + signup: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + payment: PropTypes.instanceOf(PaymentStore).isRequired, + router: PropTypes.instanceOf(RouterStore).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; + +import Signup from '../../components/auth/Signup'; +import UserStore from '../../stores/UserStore'; +import { gaPage } from '../../lib/analytics'; + +import { globalError as globalErrorPropType } from '../../prop-types'; + +@inject('stores', 'actions') @observer +export default class SignupScreen extends Component { + static propTypes = { + error: globalErrorPropType.isRequired, + }; + + componentDidMount() { + gaPage('Auth/Signup'); + } + + render() { + const { actions, stores, error } = this.props; + return ( + + ); + } +} + +SignupScreen.wrappedComponent.propTypes = { + actions: PropTypes.shape({ + user: PropTypes.shape({ + signup: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; + +import Welcome from '../../components/auth/Welcome'; +import UserStore from '../../stores/UserStore'; +import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; +import { gaPage } from '../../lib/analytics'; + +@inject('stores', 'actions') @observer +export default class LoginScreen extends Component { + componentDidMount() { + gaPage('Auth/Welcome'); + } + + render() { + const { user, recipePreviews } = this.props.stores; + + return ( + + ); + } +} + +LoginScreen.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + recipePreviews: PropTypes.instanceOf(RecipePreviewsStore).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; + +import AppStore from '../../stores/AppStore'; +import RecipesStore from '../../stores/RecipesStore'; +import ServicesStore from '../../stores/ServicesStore'; +import UIStore from '../../stores/UIStore'; +import NewsStore from '../../stores/NewsStore'; +import UserStore from '../../stores/UserStore'; +import RequestStore from '../../stores/RequestStore'; +import GlobalErrorStore from '../../stores/GlobalErrorStore'; + +import { oneOrManyChildElements } from '../../prop-types'; +import AppLayout from '../../components/layout/AppLayout'; +import Sidebar from '../../components/layout/Sidebar'; +import Services from '../../components/services/content/Services'; +import AppLoader from '../../components/ui/AppLoader'; + +@inject('stores', 'actions') @observer +export default class AppLayoutContainer extends Component { + static defaultProps = { + children: null, + }; + + render() { + const { + app, + services, + ui, + news, + globalError, + user, + requests, + } = this.props.stores; + + const { + setActive, + handleIPCMessage, + setWebviewReference, + openWindow, + reloadUpdatedServices, + reorder, + reload, + toggleNotifications, + deleteService, + updateService, + } = this.props.actions.service; + + const { hide } = this.props.actions.news; + + const { retryRequiredRequests } = this.props.actions.requests; + + const { + installUpdate, + } = this.props.actions.app; + + const { + openSettings, + closeSettings, + } = this.props.actions.ui; + + const { children } = this.props; + const allServices = services.enabled; + + const isLoadingServices = services.allServicesRequest.isExecuting + && services.allServicesRequest.isExecutingFirstTime; + + // const isLoadingRecipes = recipes.allRecipesRequest.isExecuting + // && recipes.allRecipesRequest.isExecutingFirstTime; + + if (isLoadingServices) { + return ( + + ); + } + + const sidebar = ( + + ); + + const servicesContainer = ( + + ); + + return ( + + {React.Children.count(children) > 0 ? children : null} + + ); + } +} + +AppLayoutContainer.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + services: PropTypes.instanceOf(ServicesStore).isRequired, + recipes: PropTypes.instanceOf(RecipesStore).isRequired, + app: PropTypes.instanceOf(AppStore).isRequired, + ui: PropTypes.instanceOf(UIStore).isRequired, + news: PropTypes.instanceOf(NewsStore).isRequired, + user: PropTypes.instanceOf(UserStore).isRequired, + requests: PropTypes.instanceOf(RequestStore).isRequired, + globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + service: PropTypes.shape({ + setActive: PropTypes.func.isRequired, + reload: PropTypes.func.isRequired, + toggleNotifications: PropTypes.func.isRequired, + handleIPCMessage: PropTypes.func.isRequired, + setWebviewReference: PropTypes.func.isRequired, + openWindow: PropTypes.func.isRequired, + reloadUpdatedServices: PropTypes.func.isRequired, + updateService: PropTypes.func.isRequired, + deleteService: PropTypes.func.isRequired, + reorder: PropTypes.func.isRequired, + }).isRequired, + news: PropTypes.shape({ + hide: PropTypes.func.isRequired, + }).isRequired, + ui: PropTypes.shape({ + openSettings: PropTypes.func.isRequired, + closeSettings: PropTypes.func.isRequired, + }).isRequired, + app: PropTypes.shape({ + installUpdate: PropTypes.func.isRequired, + healthCheck: PropTypes.func.isRequired, + }).isRequired, + requests: PropTypes.shape({ + retryRequiredRequests: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + children: oneOrManyChildElements, +}; 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 @@ +import { remote } from 'electron'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; + +import PaymentStore from '../../stores/PaymentStore'; +import UserStore from '../../stores/UserStore'; +import AppStore from '../../stores/AppStore'; +import { gaPage } from '../../lib/analytics'; + +import AccountDashboard from '../../components/settings/account/AccountDashboard'; + +const { BrowserWindow } = remote; + +@inject('stores', 'actions') @observer +export default class AccountScreen extends Component { + componentDidMount() { + gaPage('Settings/Account Dashboard'); + } + + onCloseWindow() { + const { user, payment } = this.props.stores; + user.getUserInfoRequest.invalidate({ immediately: true }); + payment.ordersDataRequest.invalidate({ immediately: true }); + } + + reloadData() { + const { user, payment } = this.props.stores; + + user.getUserInfoRequest.reload(); + payment.ordersDataRequest.reload(); + payment.plansRequest.reload(); + } + + stopMiner() { + const { update } = this.props.actions.user; + + update({ userData: { + isMiner: false, + } }); + } + + async handlePaymentDashboard() { + const { actions, stores } = this.props; + + actions.payment.createDashboardUrl(); + + const dashboard = await stores.payment.createDashboardUrlRequest; + + if (dashboard.url) { + const paymentWindow = new BrowserWindow({ + title: '🔒 Franz Subscription Dashboard', + parent: remote.getCurrentWindow(), + modal: false, + width: 900, + minWidth: 600, + webPreferences: { + nodeIntegration: false, + }, + }); + paymentWindow.loadURL(dashboard.url); + + paymentWindow.on('closed', () => { + this.onCloseWindow(); + }); + } + } + + render() { + const { user, payment, app } = this.props.stores; + const { openExternalUrl } = this.props.actions.app; + + const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; + const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting; + const isLoadingPlans = payment.plansRequest.isExecuting; + + return ( + this.reloadData()} + isCreatingPaymentDashboardUrl={payment.createDashboardUrlRequest.isExecuting} + openDashboard={price => this.handlePaymentDashboard(price)} + openExternalUrl={url => openExternalUrl({ url })} + onCloseSubscriptionWindow={() => this.onCloseWindow()} + stopMiner={() => this.stopMiner()} + /> + ); + } +} + +AccountScreen.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + payment: PropTypes.instanceOf(PaymentStore).isRequired, + app: PropTypes.instanceOf(AppStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + payment: PropTypes.shape({ + createDashboardUrl: PropTypes.func.isRequired, + }).isRequired, + app: PropTypes.shape({ + openExternalUrl: PropTypes.func.isRequired, + }).isRequired, + user: PropTypes.shape({ + update: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import UserStore from '../../stores/UserStore'; +import RecipesStore from '../../stores/RecipesStore'; +import ServicesStore from '../../stores/ServicesStore'; +import Form from '../../lib/Form'; +import { gaPage } from '../../lib/analytics'; + + +import ServiceError from '../../components/settings/services/ServiceError'; +import EditServiceForm from '../../components/settings/services/EditServiceForm'; +import { required, url, oneRequired } from '../../helpers/validation-helpers'; + +const messages = defineMessages({ + name: { + id: 'settings.service.form.name', + defaultMessage: '!!!Name', + }, + enableService: { + id: 'settings.service.form.enableService', + defaultMessage: '!!!Enable service', + }, + enableNotification: { + id: 'settings.service.form.enableNotification', + defaultMessage: '!!!Enable Notifications', + }, + team: { + id: 'settings.service.form.team', + defaultMessage: '!!!Team', + }, + customUrl: { + id: 'settings.service.form.customUrl', + defaultMessage: '!!!Custom server', + }, + indirectMessages: { + id: 'settings.service.form.indirectMessages', + defaultMessage: '!!!Show message badge for all new messages', + }, +}); + +@inject('stores', 'actions') @observer +export default class EditServiceScreen extends Component { + static contextTypes = { + intl: intlShape, + }; + + componentDidMount() { + gaPage('Settings/Service/Edit'); + } + + onSubmit(serviceData) { + const { action } = this.props.router.params; + const { recipes, services } = this.props.stores; + const { createService, updateService } = this.props.actions.service; + + if (action === 'edit') { + updateService({ serviceId: services.activeSettings.id, serviceData }); + } else { + createService({ recipeId: recipes.active.id, serviceData }); + } + } + + prepareForm(recipe, service) { + const { intl } = this.context; + const config = { + fields: { + name: { + label: intl.formatMessage(messages.name), + placeholder: intl.formatMessage(messages.name), + value: service.id ? service.name : recipe.name, + }, + isEnabled: { + label: intl.formatMessage(messages.enableService), + value: service.isEnabled, + default: true, + }, + isNotificationEnabled: { + label: intl.formatMessage(messages.enableNotification), + value: service.isNotificationEnabled, + default: true, + }, + }, + }; + + if (recipe.hasTeamId) { + Object.assign(config.fields, { + team: { + label: intl.formatMessage(messages.team), + placeholder: intl.formatMessage(messages.team), + value: service.team, + validate: [required], + }, + }); + } + + if (recipe.hasCustomUrl) { + Object.assign(config.fields, { + customUrl: { + label: intl.formatMessage(messages.customUrl), + placeholder: 'https://', + value: service.customUrl, + validate: [required, url], + }, + }); + } + + if (recipe.hasTeamId && recipe.hasCustomUrl) { + config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; + config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; + } + + if (recipe.hasIndirectMessages) { + Object.assign(config.fields, { + isIndirectMessageBadgeEnabled: { + label: intl.formatMessage(messages.indirectMessages), + value: service.isIndirectMessageBadgeEnabled, + default: true, + }, + }); + } + + return new Form(config); + } + + deleteService() { + const { deleteService } = this.props.actions.service; + const { action } = this.props.router.params; + + if (action === 'edit') { + const { activeSettings: service } = this.props.stores.services; + deleteService({ + serviceId: service.id, + redirect: '/settings/services', + }); + } + } + + render() { + const { recipes, services, user } = this.props.stores; + const { action } = this.props.router.params; + + let recipe; + let service = {}; + let isLoading = false; + + if (action === 'add') { + recipe = recipes.active; + + // TODO: render error message when recipe is `null` + if (!recipe) { + return ( + + ); + } + } else { + service = services.activeSettings; + isLoading = services.allServicesRequest.isExecuting; + + if (!isLoading && service) { + recipe = service.recipe; + } + } + + if (isLoading) { + return (
Loading...
); + } + + const form = this.prepareForm(recipe, service); + + return ( + this.onSubmit(d)} + onDelete={() => this.deleteService()} + /> + ); + } +} + +EditServiceScreen.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + recipes: PropTypes.instanceOf(RecipesStore).isRequired, + services: PropTypes.instanceOf(ServicesStore).isRequired, + }).isRequired, + router: PropTypes.shape({ + params: PropTypes.shape({ + action: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + actions: PropTypes.shape({ + service: PropTypes.shape({ + createService: PropTypes.func.isRequired, + updateService: PropTypes.func.isRequired, + deleteService: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import AppStore from '../../stores/AppStore'; +import SettingsStore from '../../stores/SettingsStore'; +import UserStore from '../../stores/UserStore'; +import Form from '../../lib/Form'; +import languages from '../../i18n/languages'; +import { gaPage } from '../../lib/analytics'; + + +import EditSettingsForm from '../../components/settings/settings/EditSettingsForm'; + +const messages = defineMessages({ + autoLaunchOnStart: { + id: 'settings.app.form.autoLaunchOnStart', + defaultMessage: '!!!Launch Franz on start', + }, + autoLaunchInBackground: { + id: 'settings.app.form.autoLaunchInBackground', + defaultMessage: '!!!Open in background', + }, + runInBackground: { + id: 'settings.app.form.runInBackground', + defaultMessage: '!!!Keep Franz in background when closing the window', + }, + minimizeToSystemTray: { + id: 'settings.app.form.minimizeToSystemTray', + defaultMessage: '!!!Minimize Franz to system tray', + }, + language: { + id: 'settings.app.form.language', + defaultMessage: '!!!Language', + }, + beta: { + id: 'settings.app.form.beta', + defaultMessage: '!!!Include beta versions', + }, +}); + +@inject('stores', 'actions') @observer +export default class EditSettingsScreen extends Component { + static contextTypes = { + intl: intlShape, + }; + + componentDidMount() { + gaPage('Settings/App'); + } + + onSubmit(settingsData) { + const { app, settings, user } = this.props.actions; + + app.launchOnStartup({ + enable: settingsData.autoLaunchOnStart, + openInBackground: settingsData.autoLaunchInBackground, + }); + + settings.update({ + settings: { + runInBackground: settingsData.runInBackground, + minimizeToSystemTray: settingsData.minimizeToSystemTray, + locale: settingsData.locale, + beta: settingsData.beta, + }, + }); + + user.update({ + userData: { + beta: settingsData.beta, + }, + }); + } + + prepareForm() { + const { app, settings, user } = this.props.stores; + const { intl } = this.context; + + const options = []; + Object.keys(languages).forEach((key) => { + options.push({ + value: key, + label: languages[key], + }); + }); + + const config = { + fields: { + autoLaunchOnStart: { + label: intl.formatMessage(messages.autoLaunchOnStart), + value: app.autoLaunchOnStart, + default: true, + }, + autoLaunchInBackground: { + label: intl.formatMessage(messages.autoLaunchInBackground), + value: app.launchInBackground, + default: false, + }, + runInBackground: { + label: intl.formatMessage(messages.runInBackground), + value: settings.all.runInBackground, + default: true, + }, + minimizeToSystemTray: { + label: intl.formatMessage(messages.minimizeToSystemTray), + value: settings.all.minimizeToSystemTray, + default: false, + }, + locale: { + label: intl.formatMessage(messages.language), + value: app.locale, + options, + default: 'en-US', + }, + beta: { + label: intl.formatMessage(messages.beta), + value: user.data.beta, + default: false, + }, + }, + }; + + return new Form(config); + } + + render() { + const { updateStatus, updateStatusTypes } = this.props.stores.app; + const { checkForUpdates, installUpdate } = this.props.actions.app; + const form = this.prepareForm(); + + return ( + this.onSubmit(d)} + /> + ); + } +} + +EditSettingsScreen.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + app: PropTypes.instanceOf(AppStore).isRequired, + user: PropTypes.instanceOf(UserStore).isRequired, + settings: PropTypes.instanceOf(SettingsStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + app: PropTypes.shape({ + launchOnStartup: PropTypes.func.isRequired, + checkForUpdates: PropTypes.func.isRequired, + installUpdate: PropTypes.func.isRequired, + }).isRequired, + settings: PropTypes.shape({ + update: PropTypes.func.isRequired, + }).isRequired, + user: PropTypes.shape({ + update: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import UserStore from '../../stores/UserStore'; +import Form from '../../lib/Form'; +import EditUserForm from '../../components/settings/user/EditUserForm'; +import { required, email, minLength } from '../../helpers/validation-helpers'; +import { gaPage } from '../../lib/analytics'; + +const messages = defineMessages({ + firstname: { + id: 'settings.user.form.firstname', + defaultMessage: '!!!Firstname', + }, + lastname: { + id: 'settings.user.form.lastname', + defaultMessage: '!!!Lastname', + }, + email: { + id: 'settings.user.form.email', + defaultMessage: '!!!Email', + }, + accountType: { + label: { + id: 'settings.user.form.accountType.label', + defaultMessage: '!!!Account type', + }, + individual: { + id: 'settings.user.form.accountType.individual', + defaultMessage: '!!!Individual', + }, + nonProfit: { + id: 'settings.user.form.accountType.non-profit', + defaultMessage: '!!!Non-Profit', + }, + company: { + id: 'settings.user.form.accountType.company', + defaultMessage: '!!!Company', + }, + }, + currentPassword: { + id: 'settings.user.form.currentPassword', + defaultMessage: '!!!Current password', + }, + newPassword: { + id: 'settings.user.form.newPassword', + defaultMessage: '!!!New password', + }, +}); + +@inject('stores', 'actions') @observer +export default class EditUserScreen extends Component { + static contextTypes = { + intl: intlShape, + }; + + componentDidMount() { + gaPage('Settings/Account/Edit'); + } + + componentWillUnmount() { + this.props.actions.user.resetStatus(); + } + + onSubmit(userData) { + const { update } = this.props.actions.user; + + update({ userData }); + + document.querySelector('#form').scrollIntoView({ behavior: 'smooth' }); + } + + prepareForm(user) { + const { intl } = this.context; + + const config = { + fields: { + firstname: { + label: intl.formatMessage(messages.firstname), + placeholder: intl.formatMessage(messages.firstname), + value: user.firstname, + validate: [required], + }, + lastname: { + label: intl.formatMessage(messages.lastname), + placeholder: intl.formatMessage(messages.lastname), + value: user.lastname, + validate: [required], + }, + email: { + label: intl.formatMessage(messages.email), + placeholder: intl.formatMessage(messages.email), + value: user.email, + validate: [required, email], + }, + accountType: { + value: user.accountType, + validate: [required], + label: intl.formatMessage(messages.accountType.label), + options: [{ + value: 'individual', + label: intl.formatMessage(messages.accountType.individual), + }, { + value: 'non-profit', + label: intl.formatMessage(messages.accountType.nonProfit), + }, { + value: 'company', + label: intl.formatMessage(messages.accountType.company), + }], + }, + organization: { + label: intl.formatMessage(messages.accountType.company), + placeholder: intl.formatMessage(messages.accountType.company), + value: user.organization, + }, + oldPassword: { + label: intl.formatMessage(messages.currentPassword), + type: 'password', + validate: [minLength(6)], + }, + newPassword: { + label: intl.formatMessage(messages.newPassword), + type: 'password', + validate: [minLength(6)], + }, + }, + }; + + return new Form(config); + } + + render() { + const { user } = this.props.stores; + + if (user.getUserInfoRequest.isExecuting) { + return (
Loading...
); + } + + const form = this.prepareForm(user.data); + + return ( + this.onSubmit(d)} + /> + ); + } +} + +EditUserScreen.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + user: PropTypes.shape({ + update: PropTypes.func.isRequired, + resetStatus: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { autorun } from 'mobx'; +import { inject, observer } from 'mobx-react'; + +import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; +import RecipeStore from '../../stores/RecipesStore'; +import ServiceStore from '../../stores/ServicesStore'; +import UserStore from '../../stores/UserStore'; +import { gaPage } from '../../lib/analytics'; + +import RecipesDashboard from '../../components/settings/recipes/RecipesDashboard'; + +@inject('stores', 'actions') @observer +export default class RecipesScreen extends Component { + static propTypes = { + params: PropTypes.shape({ + filter: PropTypes.string, + }).isRequired, + }; + + static defaultProps = { + params: { + filter: null, + }, + }; + + state = { + needle: null, + currentFilter: 'featured', + }; + + componentDidMount() { + gaPage('Settings/Recipe Dashboard/Featured'); + + autorun(() => { + const { filter } = this.props.params; + const { currentFilter } = this.state; + + if (filter === 'all' && currentFilter !== 'all') { + gaPage('Settings/Recipe Dashboard/All'); + this.setState({ currentFilter: 'all' }); + } else if (filter === 'featured' && currentFilter !== 'featured') { + gaPage('Settings/Recipe Dashboard/Featured'); + this.setState({ currentFilter: 'featured' }); + } else if (filter === 'dev' && currentFilter !== 'dev') { + gaPage('Settings/Recipe Dashboard/Dev'); + this.setState({ currentFilter: 'dev' }); + } + }); + } + + componentWillUnmount() { + this.props.stores.services.resetStatus(); + } + + searchRecipes(needle) { + if (needle === '') { + this.resetSearch(); + } else { + const { search } = this.props.actions.recipePreview; + this.setState({ needle }); + search({ needle }); + } + } + + resetSearch() { + this.setState({ needle: null }); + } + + render() { + const { recipePreviews, recipes, services, user } = this.props.stores; + const { showAddServiceInterface } = this.props.actions.service; + + const { filter } = this.props.params; + let recipeFilter; + + if (filter === 'all') { + recipeFilter = recipePreviews.all; + } else if (filter === 'dev') { + recipeFilter = recipePreviews.dev; + } else { + recipeFilter = recipePreviews.featured; + } + + const allRecipes = this.state.needle ? recipePreviews.searchResults : recipeFilter; + + const isLoading = recipePreviews.featuredRecipePreviewsRequest.isExecuting + || recipePreviews.allRecipePreviewsRequest.isExecuting + || recipes.installRecipeRequest.isExecuting + || recipePreviews.searchRecipePreviewsRequest.isExecuting; + + return ( + this.searchRecipes(e)} + resetSearch={() => this.resetSearch()} + searchNeedle={this.state.needle} + serviceStatus={services.actionStatus} + devRecipesCount={recipePreviews.dev.length} + /> + ); + } +} + +RecipesScreen.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + recipePreviews: PropTypes.instanceOf(RecipePreviewsStore).isRequired, + recipes: PropTypes.instanceOf(RecipeStore).isRequired, + services: PropTypes.instanceOf(ServiceStore).isRequired, + user: PropTypes.instanceOf(UserStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + service: PropTypes.shape({ + showAddServiceInterface: PropTypes.func.isRequired, + }).isRequired, + recipePreview: PropTypes.shape({ + search: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { RouterStore } from 'mobx-react-router'; + +// import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; +import UserStore from '../../stores/UserStore'; +import ServiceStore from '../../stores/ServicesStore'; +import { gaPage } from '../../lib/analytics'; + +import ServicesDashboard from '../../components/settings/services/ServicesDashboard'; + +@inject('stores', 'actions') @observer +export default class ServicesScreen extends Component { + componentDidMount() { + gaPage('Settings/Service Dashboard'); + } + + componentWillUnmount() { + this.props.actions.service.resetFilter(); + } + + deleteService() { + this.props.actions.service.deleteService(); + this.props.stores.services.resetFilter(); + } + + render() { + const { user, services, router } = this.props.stores; + const { + toggleService, + filter, + resetFilter, + } = this.props.actions.service; + const isLoading = services.allServicesRequest.isExecuting; + + let allServices = services.all; + if (services.filterNeedle !== null) { + allServices = services.filtered; + } + + return ( + this.deleteService()} + toggleService={toggleService} + isLoading={isLoading} + filterServices={filter} + resetFilter={resetFilter} + goTo={router.push} + servicesRequestFailed={services.allServicesRequest.wasExecuted && services.allServicesRequest.isError} + retryServicesRequest={() => services.allServicesRequest.reload()} + /> + ); + } +} + +ServicesScreen.wrappedComponent.propTypes = { + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + services: PropTypes.instanceOf(ServiceStore).isRequired, + router: PropTypes.instanceOf(RouterStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + service: PropTypes.shape({ + showAddServiceInterface: PropTypes.func.isRequired, + deleteService: PropTypes.func.isRequired, + toggleService: PropTypes.func.isRequired, + filter: PropTypes.func.isRequired, + resetFilter: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, inject } from 'mobx-react'; + +import ServicesStore from '../../stores/ServicesStore'; + +import Layout from '../../components/settings/SettingsLayout'; +import Navigation from '../../components/settings/navigation/SettingsNavigation'; + +@inject('stores', 'actions') @observer +export default class SettingsContainer extends Component { + render() { + const { children, stores } = this.props; + const { closeSettings } = this.props.actions.ui; + + const navigation = ( + + ); + + return ( + + {children} + + ); + } +} + +SettingsContainer.wrappedComponent.propTypes = { + children: PropTypes.element.isRequired, + stores: PropTypes.shape({ + services: PropTypes.instanceOf(ServicesStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + ui: PropTypes.shape({ + closeSettings: PropTypes.func.isRequired, + }), + }).isRequired, +}; 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 @@ +import { remote } from 'electron'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; + +import PaymentStore from '../../stores/PaymentStore'; + +import SubscriptionForm from '../../components/ui/Subscription'; + +const { BrowserWindow } = remote; + +@inject('stores', 'actions') @observer +export default class SubscriptionFormScreen extends Component { + static propTypes = { + onCloseWindow: PropTypes.func, + content: PropTypes.oneOrManyChildElements, + showSkipOption: PropTypes.bool, + skipAction: PropTypes.func, + skipButtonLabel: PropTypes.string, + hideInfo: PropTypes.bool, + } + + static defaultProps = { + onCloseWindow: () => null, + content: '', + showSkipOption: false, + skipAction: () => null, + skipButtonLabel: '', + hideInfo: false, + } + + async handlePayment(plan) { + const { + actions, + stores, + onCloseWindow, + skipAction, + } = this.props; + + if (plan !== 'mining') { + const interval = plan; + + const { id } = stores.payment.plan[interval]; + actions.payment.createHostedPage({ + planId: id, + }); + + const hostedPage = await stores.payment.createHostedPageRequest; + const url = `file://${__dirname}/../../index.html#/payment/${encodeURIComponent(hostedPage.url)}`; + + if (hostedPage.url) { + const paymentWindow = new BrowserWindow({ + parent: remote.getCurrentWindow(), + modal: true, + title: '🔒 Franz Supporter License', + width: 600, + height: window.innerHeight - 100, + maxWidth: 600, + minWidth: 600, + webPreferences: { + nodeIntegration: true, + }, + }); + paymentWindow.loadURL(url); + + paymentWindow.on('closed', () => { + onCloseWindow(); + }); + } + } else { + actions.user.update({ + userData: { + isMiner: true, + }, + }); + + skipAction(); + } + } + + render() { + const { + content, + actions, + stores, + showSkipOption, + skipAction, + skipButtonLabel, + hideInfo, + } = this.props; + return ( + stores.payment.plansRequest.reload()} + isCreatingHostedPage={stores.payment.createHostedPageRequest.isExecuting} + handlePayment={price => this.handlePayment(price)} + content={content} + error={stores.payment.plansRequest.isError} + showSkipOption={showSkipOption} + skipAction={skipAction} + skipButtonLabel={skipButtonLabel} + hideInfo={hideInfo} + openExternalUrl={actions.app.openExternalUrl} + /> + ); + } +} + +SubscriptionFormScreen.wrappedComponent.propTypes = { + actions: PropTypes.shape({ + app: PropTypes.shape({ + openExternalUrl: PropTypes.func.isRequired, + }).isRequired, + payment: PropTypes.shape({ + createHostedPage: PropTypes.func.isRequired, + }).isRequired, + user: PropTypes.shape({ + update: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + stores: PropTypes.shape({ + payment: PropTypes.instanceOf(PaymentStore).isRequired, + }).isRequired, +}; 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 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; + +import SubscriptionPopup from '../../components/ui/SubscriptionPopup'; + + +@inject('stores', 'actions') @observer +export default class SubscriptionPopupScreen extends Component { + state = { + complete: false, + }; + + completeCheck(event) { + const { url } = event; + + if (url.includes('recurly') && url.includes('confirmation')) { + this.setState({ + complete: true, + }); + } + } + + render() { + return ( + window.close()} + completeCheck={e => this.completeCheck(e)} + isCompleted={this.state.complete} + /> + ); + } +} + + +SubscriptionPopupScreen.wrappedComponent.propTypes = { + router: PropTypes.shape({ + params: PropTypes.shape({ + url: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; -- cgit v1.2.3-70-g09d2