diff options
author | vantezzen <hello@vantezzen.io> | 2019-10-24 15:58:20 +0200 |
---|---|---|
committer | vantezzen <hello@vantezzen.io> | 2019-10-24 15:58:20 +0200 |
commit | acbd4ac3e3930a2ea6ef32fa66fb7b4074896873 (patch) | |
tree | 50c8ecb3d08e997106e48d1de5b904a2dba30991 /src/components | |
parent | Merge branch 'develop' (diff) | |
parent | Merge pull request #155 from getferdi/l10n_develop (diff) | |
download | ferdium-app-acbd4ac3e3930a2ea6ef32fa66fb7b4074896873.tar.gz ferdium-app-acbd4ac3e3930a2ea6ef32fa66fb7b4074896873.tar.zst ferdium-app-acbd4ac3e3930a2ea6ef32fa66fb7b4074896873.zip |
Merge branch 'develop'v5.4.0-beta.3
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/auth/Login.js | 2 | ||||
-rw-r--r-- | src/components/auth/Pricing.js | 122 | ||||
-rw-r--r-- | src/components/auth/Signup.js | 2 | ||||
-rw-r--r-- | src/components/auth/Welcome.js | 2 | ||||
-rw-r--r-- | src/components/layout/AppLayout.js | 4 | ||||
-rw-r--r-- | src/components/layout/Sidebar.js | 2 | ||||
-rw-r--r-- | src/components/settings/account/AccountDashboard.js | 18 | ||||
-rw-r--r-- | src/components/settings/navigation/SettingsNavigation.js | 50 | ||||
-rw-r--r-- | src/components/settings/services/EditServiceForm.js | 19 | ||||
-rw-r--r-- | src/components/settings/team/TeamDashboard.js | 16 | ||||
-rw-r--r-- | src/components/subscription/SubscriptionForm.js | 6 | ||||
-rw-r--r-- | src/components/subscription/SubscriptionPopup.js | 2 | ||||
-rw-r--r-- | src/components/subscription/TrialForm.js | 4 | ||||
-rw-r--r-- | src/components/ui/AppLoader/index.js | 9 | ||||
-rw-r--r-- | src/components/ui/FeatureItem.js | 1 | ||||
-rw-r--r-- | src/components/ui/FeatureList.js | 74 | ||||
-rw-r--r-- | src/components/ui/PremiumFeatureContainer/index.js | 4 |
17 files changed, 262 insertions, 75 deletions
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js index e58016e25..e25121de0 100644 --- a/src/components/auth/Login.js +++ b/src/components/auth/Login.js | |||
@@ -70,7 +70,7 @@ const messages = defineMessages({ | |||
70 | }, | 70 | }, |
71 | }); | 71 | }); |
72 | 72 | ||
73 | export default @observer @inject('actions') class Login extends Component { | 73 | export default @inject('actions') @observer class Login extends Component { |
74 | static propTypes = { | 74 | static propTypes = { |
75 | onSubmit: PropTypes.func.isRequired, | 75 | onSubmit: PropTypes.func.isRequired, |
76 | isSubmitting: PropTypes.bool.isRequired, | 76 | isSubmitting: PropTypes.bool.isRequired, |
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js index aadb18d91..593cb9c4b 100644 --- a/src/components/auth/Pricing.js +++ b/src/components/auth/Pricing.js | |||
@@ -13,12 +13,20 @@ import { FeatureList } from '../ui/FeatureList'; | |||
13 | 13 | ||
14 | const messages = defineMessages({ | 14 | const messages = defineMessages({ |
15 | headline: { | 15 | headline: { |
16 | id: 'pricing.trial.headline', | 16 | id: 'pricing.trial.headline.pro', |
17 | defaultMessage: '!!!Franz Professional', | 17 | defaultMessage: '!!!Hi {name}, welcome to Franz', |
18 | }, | 18 | }, |
19 | personalOffer: { | 19 | specialTreat: { |
20 | id: 'pricing.trial.subheadline', | 20 | id: 'pricing.trial.intro.specialTreat', |
21 | defaultMessage: '!!!Your personal welcome offer:', | 21 | defaultMessage: '!!!We have a special treat for you.', |
22 | }, | ||
23 | tryPro: { | ||
24 | id: 'pricing.trial.intro.tryPro', | ||
25 | defaultMessage: '!!!Enjoy the full Franz Professional experience completely free for 14 days.', | ||
26 | }, | ||
27 | happyMessaging: { | ||
28 | id: 'pricing.trial.intro.happyMessaging', | ||
29 | defaultMessage: '!!!Happy messaging,', | ||
22 | }, | 30 | }, |
23 | noStringsAttachedHeadline: { | 31 | noStringsAttachedHeadline: { |
24 | id: 'pricing.trial.terms.headline', | 32 | id: 'pricing.trial.terms.headline', |
@@ -32,13 +40,21 @@ const messages = defineMessages({ | |||
32 | id: 'pricing.trial.terms.automaticTrialEnd', | 40 | id: 'pricing.trial.terms.automaticTrialEnd', |
33 | defaultMessage: '!!!Your free trial ends automatically after 14 days', | 41 | defaultMessage: '!!!Your free trial ends automatically after 14 days', |
34 | }, | 42 | }, |
43 | trialWorth: { | ||
44 | id: 'pricing.trial.terms.trialWorth', | ||
45 | defaultMessage: '!!!Free trial (normally {currency}{price} per month)', | ||
46 | }, | ||
35 | activationError: { | 47 | activationError: { |
36 | id: 'pricing.trial.error', | 48 | id: 'pricing.trial.error', |
37 | defaultMessage: '!!!Sorry, we could not activate your trial!', | 49 | defaultMessage: '!!!Sorry, we could not activate your trial!', |
38 | }, | 50 | }, |
39 | ctaAccept: { | 51 | ctaAccept: { |
40 | id: 'pricing.trial.cta.accept', | 52 | id: 'pricing.trial.cta.accept', |
41 | defaultMessage: '!!!Yes, upgrade my account to Franz Professional', | 53 | defaultMessage: '!!!Start my 14-day Franz Professional Trial ', |
54 | }, | ||
55 | ctaStart: { | ||
56 | id: 'pricing.trial.cta.start', | ||
57 | defaultMessage: '!!!Start using Franz', | ||
42 | }, | 58 | }, |
43 | ctaSkip: { | 59 | ctaSkip: { |
44 | id: 'pricing.trial.cta.skip', | 60 | id: 'pricing.trial.cta.skip', |
@@ -58,6 +74,7 @@ const styles = theme => ({ | |||
58 | welcomeOffer: { | 74 | welcomeOffer: { |
59 | textAlign: 'center', | 75 | textAlign: 'center', |
60 | fontWeight: 'bold', | 76 | fontWeight: 'bold', |
77 | marginBottom: '6 !important', | ||
61 | }, | 78 | }, |
62 | keyTerms: { | 79 | keyTerms: { |
63 | textAlign: 'center', | 80 | textAlign: 'center', |
@@ -93,6 +110,34 @@ const styles = theme => ({ | |||
93 | margin: [20, 0, 0], | 110 | margin: [20, 0, 0], |
94 | color: theme.styleTypes.danger.accent, | 111 | color: theme.styleTypes.danger.accent, |
95 | }, | 112 | }, |
113 | priceContainer: { | ||
114 | display: 'flex', | ||
115 | justifyContent: 'space-evenly', | ||
116 | margin: [10, 0, 15], | ||
117 | }, | ||
118 | price: { | ||
119 | '& sup': { | ||
120 | verticalAlign: 14, | ||
121 | fontSize: 20, | ||
122 | }, | ||
123 | }, | ||
124 | figure: { | ||
125 | fontSize: 40, | ||
126 | }, | ||
127 | regularPrice: { | ||
128 | position: 'relative', | ||
129 | |||
130 | '&:before': { | ||
131 | content: '" "', | ||
132 | position: 'absolute', | ||
133 | width: '130%', | ||
134 | height: 1, | ||
135 | top: 14, | ||
136 | left: -12, | ||
137 | borderBottom: [3, 'solid', 'red'], | ||
138 | transform: 'rotateZ(-20deg)', | ||
139 | }, | ||
140 | }, | ||
96 | }); | 141 | }); |
97 | 142 | ||
98 | export default @injectSheet(styles) @observer class Signup extends Component { | 143 | export default @injectSheet(styles) @observer class Signup extends Component { |
@@ -101,7 +146,11 @@ export default @injectSheet(styles) @observer class Signup extends Component { | |||
101 | isLoadingRequiredData: PropTypes.bool.isRequired, | 146 | isLoadingRequiredData: PropTypes.bool.isRequired, |
102 | isActivatingTrial: PropTypes.bool.isRequired, | 147 | isActivatingTrial: PropTypes.bool.isRequired, |
103 | trialActivationError: PropTypes.bool.isRequired, | 148 | trialActivationError: PropTypes.bool.isRequired, |
149 | canSkipTrial: PropTypes.bool.isRequired, | ||
104 | classes: PropTypes.object.isRequired, | 150 | classes: PropTypes.object.isRequired, |
151 | currency: PropTypes.string.isRequired, | ||
152 | price: PropTypes.number.isRequired, | ||
153 | name: PropTypes.string.isRequired, | ||
105 | }; | 154 | }; |
106 | 155 | ||
107 | static contextTypes = { | 156 | static contextTypes = { |
@@ -114,10 +163,16 @@ export default @injectSheet(styles) @observer class Signup extends Component { | |||
114 | isLoadingRequiredData, | 163 | isLoadingRequiredData, |
115 | isActivatingTrial, | 164 | isActivatingTrial, |
116 | trialActivationError, | 165 | trialActivationError, |
166 | canSkipTrial, | ||
117 | classes, | 167 | classes, |
168 | currency, | ||
169 | price, | ||
170 | name, | ||
118 | } = this.props; | 171 | } = this.props; |
119 | const { intl } = this.context; | 172 | const { intl } = this.context; |
120 | 173 | ||
174 | const [intPart, fractionPart] = (price).toString().split('.'); | ||
175 | |||
121 | return ( | 176 | return ( |
122 | <div className={classnames('auth__scroll-container', classes.container)}> | 177 | <div className={classnames('auth__scroll-container', classes.container)}> |
123 | <div className={classnames('auth__container', 'auth__container--signup', classes.content)}> | 178 | <div className={classnames('auth__container', 'auth__container--signup', classes.content)}> |
@@ -129,30 +184,51 @@ export default @injectSheet(styles) @observer class Signup extends Component { | |||
129 | alt="" | 184 | alt="" |
130 | /> | 185 | /> |
131 | )} | 186 | )} |
132 | <p className={classes.welcomeOffer}>{intl.formatMessage(messages.personalOffer)}</p> | 187 | <h1>{intl.formatMessage(messages.headline, { name })}</h1> |
133 | <h1>{intl.formatMessage(messages.headline)}</h1> | ||
134 | <div className="auth__letter"> | 188 | <div className="auth__letter"> |
135 | <p> | 189 | <p> |
136 | We built Franz with a lot of effort, manpower and love, | 190 | {intl.formatMessage(messages.specialTreat)} |
137 | to boost up your messaging experience. | ||
138 | <br /> | 191 | <br /> |
139 | </p> | 192 | </p> |
140 | <p> | 193 | <p> |
141 | Get the free 14 day Franz Professional trial and see your communication evolving. | 194 | {intl.formatMessage(messages.tryPro)} |
142 | <br /> | 195 | <br /> |
143 | </p> | 196 | </p> |
144 | <p> | 197 | <p> |
145 | Thanks for being a hero. | 198 | {intl.formatMessage(messages.happyMessaging)} |
146 | </p> | 199 | </p> |
147 | <p> | 200 | <p> |
148 | <strong>Stefan Malzner</strong> | 201 | <strong>Stefan Malzner</strong> |
149 | </p> | 202 | </p> |
150 | </div> | 203 | </div> |
204 | <div className={classes.priceContainer}> | ||
205 | <p className={classnames(classes.price, classes.regularPrice)}> | ||
206 | <span className={classes.figure}> | ||
207 | {currency} | ||
208 | {intPart} | ||
209 | </span> | ||
210 | <sup>{fractionPart}</sup> | ||
211 | </p> | ||
212 | <p className={classnames(classes.price, classes.trialPrice)}> | ||
213 | <span className={classes.figure}> | ||
214 | {currency} | ||
215 | 0 | ||
216 | </span> | ||
217 | <sup>00</sup> | ||
218 | </p> | ||
219 | </div> | ||
151 | <div className={classes.keyTerms}> | 220 | <div className={classes.keyTerms}> |
152 | <H2> | 221 | <H2> |
153 | {intl.formatMessage(messages.noStringsAttachedHeadline)} | 222 | {intl.formatMessage(messages.noStringsAttachedHeadline)} |
154 | </H2> | 223 | </H2> |
155 | <ul className={classes.keyTermsList}> | 224 | <ul className={classes.keyTermsList}> |
225 | <FeatureItem | ||
226 | icon="👉" | ||
227 | name={intl.formatMessage(messages.trialWorth, { | ||
228 | currency, | ||
229 | price, | ||
230 | })} | ||
231 | /> | ||
156 | <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} /> | 232 | <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} /> |
157 | <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} /> | 233 | <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} /> |
158 | </ul> | 234 | </ul> |
@@ -161,33 +237,23 @@ export default @injectSheet(styles) @observer class Signup extends Component { | |||
161 | <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p> | 237 | <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p> |
162 | )} | 238 | )} |
163 | <Button | 239 | <Button |
164 | label={intl.formatMessage(messages.ctaAccept)} | 240 | label={intl.formatMessage(!canSkipTrial ? messages.ctaStart : messages.ctaAccept)} |
165 | className={classes.cta} | 241 | className={classes.cta} |
166 | onClick={onSubmit} | 242 | onClick={onSubmit} |
167 | busy={isActivatingTrial} | 243 | busy={isActivatingTrial} |
168 | disabled={isLoadingRequiredData || isActivatingTrial} | 244 | disabled={isLoadingRequiredData || isActivatingTrial} |
169 | /> | 245 | /> |
170 | <p className={classes.skipLink}> | 246 | {canSkipTrial && ( |
171 | <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a> | 247 | <p className={classes.skipLink}> |
172 | </p> | 248 | <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a> |
249 | </p> | ||
250 | )} | ||
173 | </form> | 251 | </form> |
174 | </div> | 252 | </div> |
175 | <div className={classes.featureContainer}> | 253 | <div className={classes.featureContainer}> |
176 | <H2> | 254 | <H2> |
177 | {intl.formatMessage(messages.featuresHeadline)} | 255 | {intl.formatMessage(messages.featuresHeadline)} |
178 | </H2> | 256 | </H2> |
179 | {/* <ul className={classes.features}> | ||
180 | <FeatureItem name="Add unlimited services" className={classes.featureItem} /> | ||
181 | <FeatureItem name="Spellchecker support" className={classes.featureItem} /> | ||
182 | <FeatureItem name="Workspaces" className={classes.featureItem} /> | ||
183 | <FeatureItem name="Add Custom Websites" className={classes.featureItem} /> | ||
184 | <FeatureItem name="On-premise & other Hosted Services" className={classes.featureItem} /> | ||
185 | <FeatureItem name="Install 3rd party services" className={classes.featureItem} /> | ||
186 | <FeatureItem name="Service Proxies" className={classes.featureItem} /> | ||
187 | <FeatureItem name="Team Management" className={classes.featureItem} /> | ||
188 | <FeatureItem name="No Waiting Screens" className={classes.featureItem} /> | ||
189 | <FeatureItem name="Forever ad-free" className={classes.featureItem} /> | ||
190 | </ul> */} | ||
191 | <FeatureList /> | 257 | <FeatureList /> |
192 | </div> | 258 | </div> |
193 | </div> | 259 | </div> |
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js index 47e9daf18..a166155a7 100644 --- a/src/components/auth/Signup.js +++ b/src/components/auth/Signup.js | |||
@@ -74,7 +74,7 @@ const messages = defineMessages({ | |||
74 | }, | 74 | }, |
75 | }); | 75 | }); |
76 | 76 | ||
77 | export default @observer @inject('actions') class Signup extends Component { | 77 | export default @inject('actions') @observer class Signup extends Component { |
78 | static propTypes = { | 78 | static propTypes = { |
79 | onSubmit: PropTypes.func.isRequired, | 79 | onSubmit: PropTypes.func.isRequired, |
80 | isSubmitting: PropTypes.bool.isRequired, | 80 | isSubmitting: PropTypes.bool.isRequired, |
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js index 2ca8b430f..1453c1d7c 100644 --- a/src/components/auth/Welcome.js +++ b/src/components/auth/Welcome.js | |||
@@ -22,7 +22,7 @@ const messages = defineMessages({ | |||
22 | }, | 22 | }, |
23 | }); | 23 | }); |
24 | 24 | ||
25 | export default @observer @inject('actions') class Login extends Component { | 25 | export default @inject('actions') @observer class Login extends Component { |
26 | static propTypes = { | 26 | static propTypes = { |
27 | loginRoute: PropTypes.string.isRequired, | 27 | loginRoute: PropTypes.string.isRequired, |
28 | signupRoute: PropTypes.string.isRequired, | 28 | signupRoute: PropTypes.string.isRequired, |
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js index 2b0719f92..80e6daf19 100644 --- a/src/components/layout/AppLayout.js +++ b/src/components/layout/AppLayout.js | |||
@@ -19,6 +19,8 @@ import { workspaceStore } from '../../features/workspaces'; | |||
19 | import AppUpdateInfoBar from '../AppUpdateInfoBar'; | 19 | import AppUpdateInfoBar from '../AppUpdateInfoBar'; |
20 | import TrialActivationInfoBar from '../TrialActivationInfoBar'; | 20 | import TrialActivationInfoBar from '../TrialActivationInfoBar'; |
21 | import Todos from '../../features/todos/containers/TodosScreen'; | 21 | import Todos from '../../features/todos/containers/TodosScreen'; |
22 | import PlanSelection from '../../features/planSelection/containers/PlanSelectionScreen'; | ||
23 | import TrialStatusBar from '../../features/trialStatusBar/containers/TrialStatusBarScreen'; | ||
22 | 24 | ||
23 | function createMarkup(HTMLString) { | 25 | function createMarkup(HTMLString) { |
24 | return { __html: HTMLString }; | 26 | return { __html: HTMLString }; |
@@ -189,9 +191,11 @@ class AppLayout extends Component { | |||
189 | <QuickSwitch /> | 191 | <QuickSwitch /> |
190 | {services} | 192 | {services} |
191 | {children} | 193 | {children} |
194 | <TrialStatusBar /> | ||
192 | </div> | 195 | </div> |
193 | <Todos /> | 196 | <Todos /> |
194 | </div> | 197 | </div> |
198 | <PlanSelection /> | ||
195 | </div> | 199 | </div> |
196 | </ErrorBoundary> | 200 | </ErrorBoundary> |
197 | ); | 201 | ); |
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js index f7d10735c..48a83c5a1 100644 --- a/src/components/layout/Sidebar.js +++ b/src/components/layout/Sidebar.js | |||
@@ -203,7 +203,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp | |||
203 | <i className="mdi mdi-settings" /> | 203 | <i className="mdi mdi-settings" /> |
204 | { (this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.AVAILABLE | 204 | { (this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.AVAILABLE |
205 | || this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.DOWNLOADED) && ( | 205 | || this.props.stores.app.updateStatus === this.props.stores.app.updateStatusTypes.DOWNLOADED) && ( |
206 | <span className="update-availible"> | 206 | <span className="update-available"> |
207 | • | 207 | • |
208 | </span> | 208 | </span> |
209 | ) } | 209 | ) } |
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js index f588449f4..83dc34a52 100644 --- a/src/components/settings/account/AccountDashboard.js +++ b/src/components/settings/account/AccountDashboard.js | |||
@@ -109,6 +109,7 @@ class AccountDashboard extends Component { | |||
109 | openBilling: PropTypes.func.isRequired, | 109 | openBilling: PropTypes.func.isRequired, |
110 | upgradeToPro: PropTypes.func.isRequired, | 110 | upgradeToPro: PropTypes.func.isRequired, |
111 | openInvoices: PropTypes.func.isRequired, | 111 | openInvoices: PropTypes.func.isRequired, |
112 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | ||
112 | }; | 113 | }; |
113 | 114 | ||
114 | static contextTypes = { | 115 | static contextTypes = { |
@@ -130,6 +131,7 @@ class AccountDashboard extends Component { | |||
130 | openBilling, | 131 | openBilling, |
131 | upgradeToPro, | 132 | upgradeToPro, |
132 | openInvoices, | 133 | openInvoices, |
134 | onCloseSubscriptionWindow, | ||
133 | } = this.props; | 135 | } = this.props; |
134 | const { intl } = this.context; | 136 | const { intl } = this.context; |
135 | 137 | ||
@@ -215,7 +217,9 @@ class AccountDashboard extends Component { | |||
215 | {intl.formatMessage(messages.yourLicense)} | 217 | {intl.formatMessage(messages.yourLicense)} |
216 | </H2> | 218 | </H2> |
217 | <p> | 219 | <p> |
218 | {isPremiumOverrideUser ? 'Franz Premium' : planName} | 220 | Franz |
221 | {' '} | ||
222 | {isPremiumOverrideUser ? 'Premium' : planName} | ||
219 | {user.team.isTrial && ( | 223 | {user.team.isTrial && ( |
220 | <> | 224 | <> |
221 | {' – '} | 225 | {' – '} |
@@ -238,14 +242,16 @@ class AccountDashboard extends Component { | |||
238 | </p> | 242 | </p> |
239 | </> | 243 | </> |
240 | )} | 244 | )} |
241 | <div className="manage-user-links"> | 245 | {!isProUser && ( |
242 | {!isProUser && ( | 246 | <div className="manage-user-links"> |
243 | <Button | 247 | <Button |
244 | label={intl.formatMessage(messages.upgradeAccountToPro)} | 248 | label={intl.formatMessage(messages.upgradeAccountToPro)} |
245 | className="franz-form__button--primary" | 249 | className="franz-form__button--primary" |
246 | onClick={upgradeToPro} | 250 | onClick={upgradeToPro} |
247 | /> | 251 | /> |
248 | )} | 252 | </div> |
253 | )} | ||
254 | <div className="manage-user-links"> | ||
249 | <Button | 255 | <Button |
250 | label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} | 256 | label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} |
251 | className="franz-form__button--inverted" | 257 | className="franz-form__button--inverted" |
@@ -263,7 +269,9 @@ class AccountDashboard extends Component { | |||
263 | {!user.isPremium && ( | 269 | {!user.isPremium && ( |
264 | <div className="account franz-form"> | 270 | <div className="account franz-form"> |
265 | <div className="account__box"> | 271 | <div className="account__box"> |
266 | <SubscriptionForm /> | 272 | <SubscriptionForm |
273 | onCloseWindow={onCloseSubscriptionWindow} | ||
274 | /> | ||
267 | </div> | 275 | </div> |
268 | </div> | 276 | </div> |
269 | )} | 277 | )} |
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js index 2711bc107..192cfde7a 100644 --- a/src/components/settings/navigation/SettingsNavigation.js +++ b/src/components/settings/navigation/SettingsNavigation.js | |||
@@ -3,10 +3,13 @@ import PropTypes from 'prop-types'; | |||
3 | import { defineMessages, intlShape } from 'react-intl'; | 3 | import { defineMessages, intlShape } from 'react-intl'; |
4 | import { inject, observer } from 'mobx-react'; | 4 | import { inject, observer } from 'mobx-react'; |
5 | import { ProBadge } from '@meetfranz/ui'; | 5 | import { ProBadge } from '@meetfranz/ui'; |
6 | import { RouterStore } from 'mobx-react-router'; | ||
6 | 7 | ||
8 | import { LOCAL_SERVER, LIVE_API } from '../../../config'; | ||
7 | import Link from '../../ui/Link'; | 9 | import Link from '../../ui/Link'; |
8 | import { workspaceStore } from '../../../features/workspaces'; | 10 | import { workspaceStore } from '../../../features/workspaces'; |
9 | import UIStore from '../../../stores/UIStore'; | 11 | import UIStore from '../../../stores/UIStore'; |
12 | import SettingsStore from '../../../stores/SettingsStore'; | ||
10 | import UserStore from '../../../stores/UserStore'; | 13 | import UserStore from '../../../stores/UserStore'; |
11 | import { serviceLimitStore } from '../../../features/serviceLimit'; | 14 | import { serviceLimitStore } from '../../../features/serviceLimit'; |
12 | 15 | ||
@@ -45,11 +48,18 @@ const messages = defineMessages({ | |||
45 | }, | 48 | }, |
46 | }); | 49 | }); |
47 | 50 | ||
48 | export default @inject('stores') @observer class SettingsNavigation extends Component { | 51 | export default @inject('stores', 'actions') @observer class SettingsNavigation extends Component { |
49 | static propTypes = { | 52 | static propTypes = { |
50 | stores: PropTypes.shape({ | 53 | stores: PropTypes.shape({ |
51 | ui: PropTypes.instanceOf(UIStore).isRequired, | 54 | ui: PropTypes.instanceOf(UIStore).isRequired, |
52 | user: PropTypes.instanceOf(UserStore).isRequired, | 55 | user: PropTypes.instanceOf(UserStore).isRequired, |
56 | settings: PropTypes.instanceOf(SettingsStore).isRequired, | ||
57 | router: PropTypes.instanceOf(RouterStore).isRequired, | ||
58 | }).isRequired, | ||
59 | actions: PropTypes.shape({ | ||
60 | settings: PropTypes.shape({ | ||
61 | update: PropTypes.func.isRequired, | ||
62 | }).isRequired, | ||
53 | }).isRequired, | 63 | }).isRequired, |
54 | serviceCount: PropTypes.number.isRequired, | 64 | serviceCount: PropTypes.number.isRequired, |
55 | workspaceCount: PropTypes.number.isRequired, | 65 | workspaceCount: PropTypes.number.isRequired, |
@@ -59,12 +69,42 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp | |||
59 | intl: intlShape, | 69 | intl: intlShape, |
60 | }; | 70 | }; |
61 | 71 | ||
72 | handleLoginLogout() { | ||
73 | const isLoggedIn = Boolean(localStorage.getItem('authToken')); | ||
74 | const isUsingWithoutAccount = this.props.stores.settings.app.server === LOCAL_SERVER; | ||
75 | |||
76 | if (isLoggedIn) { | ||
77 | // Remove current auth token | ||
78 | localStorage.removeItem('authToken'); | ||
79 | |||
80 | if (isUsingWithoutAccount) { | ||
81 | // Reset server back to Ferdi API | ||
82 | this.props.actions.settings.update({ | ||
83 | type: 'app', | ||
84 | data: { | ||
85 | server: LIVE_API, | ||
86 | }, | ||
87 | }); | ||
88 | } | ||
89 | this.props.stores.user.isLoggingOut = true; | ||
90 | } | ||
91 | |||
92 | this.props.stores.router.push(isLoggedIn ? '/auth/logout' : '/auth/welcome'); | ||
93 | |||
94 | if (isLoggedIn) { | ||
95 | // Reload Ferdi, otherwise many settings won't sync correctly with the server | ||
96 | // after logging into another account | ||
97 | window.location.reload(); | ||
98 | } | ||
99 | } | ||
100 | |||
62 | render() { | 101 | render() { |
63 | const { serviceCount, workspaceCount, stores } = this.props; | 102 | const { serviceCount, workspaceCount, stores } = this.props; |
64 | const { isDarkThemeActive } = stores.ui; | 103 | const { isDarkThemeActive } = stores.ui; |
65 | const { router, user } = stores; | 104 | const { router, user } = stores; |
66 | const { intl } = this.context; | 105 | const { intl } = this.context; |
67 | const isLoggedIn = Boolean(localStorage.getItem('authToken')); | 106 | const isLoggedIn = Boolean(localStorage.getItem('authToken')); |
107 | const isUsingWithoutAccount = stores.settings.app.server === LOCAL_SERVER; | ||
68 | 108 | ||
69 | return ( | 109 | return ( |
70 | <div className="settings-navigation"> | 110 | <div className="settings-navigation"> |
@@ -136,12 +176,14 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp | |||
136 | {intl.formatMessage(messages.supportFerdi)} | 176 | {intl.formatMessage(messages.supportFerdi)} |
137 | </Link> | 177 | </Link> |
138 | <span className="settings-navigation__expander" /> | 178 | <span className="settings-navigation__expander" /> |
139 | <Link | 179 | <button |
180 | type="button" | ||
140 | to={isLoggedIn ? '/auth/logout' : '/auth/welcome'} | 181 | to={isLoggedIn ? '/auth/logout' : '/auth/welcome'} |
141 | className="settings-navigation__link" | 182 | className="settings-navigation__link" |
183 | onClick={this.handleLoginLogout.bind(this)} | ||
142 | > | 184 | > |
143 | { isLoggedIn ? intl.formatMessage(messages.logout) : 'Login'} | 185 | { isLoggedIn && !isUsingWithoutAccount ? intl.formatMessage(messages.logout) : 'Login'} |
144 | </Link> | 186 | </button> |
145 | </div> | 187 | </div> |
146 | ); | 188 | ); |
147 | } | 189 | } |
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js index 76138aa15..fa34ac60b 100644 --- a/src/components/settings/services/EditServiceForm.js +++ b/src/components/settings/services/EditServiceForm.js | |||
@@ -328,6 +328,18 @@ export default @observer class EditServiceForm extends Component { | |||
328 | )} | 328 | )} |
329 | </Tabs> | 329 | </Tabs> |
330 | )} | 330 | )} |
331 | |||
332 | {recipe.message && ( | ||
333 | <p | ||
334 | className="settings__message" | ||
335 | style={{ | ||
336 | marginTop: 0, | ||
337 | }} | ||
338 | > | ||
339 | <span className="mdi mdi-information" /> | ||
340 | {recipe.message} | ||
341 | </p> | ||
342 | )} | ||
331 | <div className="service-flex-grid"> | 343 | <div className="service-flex-grid"> |
332 | <div className="settings__options"> | 344 | <div className="settings__options"> |
333 | <div className="settings__settings-group"> | 345 | <div className="settings__settings-group"> |
@@ -417,13 +429,6 @@ export default @observer class EditServiceForm extends Component { | |||
417 | </div> | 429 | </div> |
418 | </PremiumFeatureContainer> | 430 | </PremiumFeatureContainer> |
419 | )} | 431 | )} |
420 | |||
421 | {recipe.message && ( | ||
422 | <p className="settings__message"> | ||
423 | <span className="mdi mdi-information" /> | ||
424 | {recipe.message} | ||
425 | </p> | ||
426 | )} | ||
427 | </form> | 432 | </form> |
428 | </div> | 433 | </div> |
429 | <div className="settings__controls"> | 434 | <div className="settings__controls"> |
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js index 3a38d682b..7e6d93997 100644 --- a/src/components/settings/team/TeamDashboard.js +++ b/src/components/settings/team/TeamDashboard.js | |||
@@ -38,13 +38,13 @@ const messages = defineMessages({ | |||
38 | id: 'settings.team.upgradeAction', | 38 | id: 'settings.team.upgradeAction', |
39 | defaultMessage: '!!!Upgrade your Account', | 39 | defaultMessage: '!!!Upgrade your Account', |
40 | }, | 40 | }, |
41 | teamsUnavailible: { | 41 | teamsUnavailable: { |
42 | id: 'settings.team.teamsUnavailible', | 42 | id: 'settings.team.teamsUnavailable', |
43 | defaultMessage: '!!!Teams are unavailible', | 43 | defaultMessage: '!!!Teams are unavailable', |
44 | }, | 44 | }, |
45 | teamsUnavailibleInfo: { | 45 | teamsUnavailableInfo: { |
46 | id: 'settings.team.teamsUnavailibleInfo', | 46 | id: 'settings.team.teamsUnavailableInfo', |
47 | defaultMessage: '!!!Teams are currently only availible when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.', | 47 | defaultMessage: '!!!Teams are currently only available when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.', |
48 | }, | 48 | }, |
49 | }); | 49 | }); |
50 | 50 | ||
@@ -208,9 +208,9 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon | |||
208 | </div> | 208 | </div> |
209 | <div className="settings__body"> | 209 | <div className="settings__body"> |
210 | <h1 className={classes.headline}> | 210 | <h1 className={classes.headline}> |
211 | {intl.formatMessage(messages.teamsUnavailible)} | 211 | {intl.formatMessage(messages.teamsUnavailable)} |
212 | </h1> | 212 | </h1> |
213 | {intl.formatMessage(messages.teamsUnavailibleInfo)} | 213 | {intl.formatMessage(messages.teamsUnavailableInfo)} |
214 | </div> | 214 | </div> |
215 | </div> | 215 | </div> |
216 | ); | 216 | ); |
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js index cdfbbe60d..ec486e5d0 100644 --- a/src/components/subscription/SubscriptionForm.js +++ b/src/components/subscription/SubscriptionForm.js | |||
@@ -30,11 +30,12 @@ const messages = defineMessages({ | |||
30 | 30 | ||
31 | const styles = () => ({ | 31 | const styles = () => ({ |
32 | activateTrialButton: { | 32 | activateTrialButton: { |
33 | margin: [40, 0, 50], | 33 | margin: [40, 'auto', 50], |
34 | display: 'flex', | ||
34 | }, | 35 | }, |
35 | }); | 36 | }); |
36 | 37 | ||
37 | export default @observer @injectSheet(styles) class SubscriptionForm extends Component { | 38 | export default @injectSheet(styles) @observer class SubscriptionForm extends Component { |
38 | static propTypes = { | 39 | static propTypes = { |
39 | selectPlan: PropTypes.func.isRequired, | 40 | selectPlan: PropTypes.func.isRequired, |
40 | isActivatingTrial: PropTypes.bool.isRequired, | 41 | isActivatingTrial: PropTypes.bool.isRequired, |
@@ -62,7 +63,6 @@ export default @observer @injectSheet(styles) class SubscriptionForm extends Com | |||
62 | className={classes.activateTrialButton} | 63 | className={classes.activateTrialButton} |
63 | busy={isActivatingTrial} | 64 | busy={isActivatingTrial} |
64 | onClick={selectPlan} | 65 | onClick={selectPlan} |
65 | stretch | ||
66 | /> | 66 | /> |
67 | <div className="subscription__premium-info"> | 67 | <div className="subscription__premium-info"> |
68 | <H3> | 68 | <H3> |
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js index 12ef8a6e9..12a51ad7b 100644 --- a/src/components/subscription/SubscriptionPopup.js +++ b/src/components/subscription/SubscriptionPopup.js | |||
@@ -59,8 +59,10 @@ export default @observer class SubscriptionPopup extends Component { | |||
59 | className="subscription-popup__webview" | 59 | className="subscription-popup__webview" |
60 | 60 | ||
61 | autosize | 61 | autosize |
62 | allowpopups | ||
62 | src={encodeURI(url)} | 63 | src={encodeURI(url)} |
63 | onDidNavigate={completeCheck} | 64 | onDidNavigate={completeCheck} |
65 | onDidNavigateInPage={completeCheck} | ||
64 | /> | 66 | /> |
65 | </div> | 67 | </div> |
66 | <div className="subscription-popup__toolbar franz-form"> | 68 | <div className="subscription-popup__toolbar franz-form"> |
diff --git a/src/components/subscription/TrialForm.js b/src/components/subscription/TrialForm.js index 6ccdf20ae..d61b779ed 100644 --- a/src/components/subscription/TrialForm.js +++ b/src/components/subscription/TrialForm.js | |||
@@ -43,7 +43,8 @@ const messages = defineMessages({ | |||
43 | 43 | ||
44 | const styles = theme => ({ | 44 | const styles = theme => ({ |
45 | activateTrialButton: { | 45 | activateTrialButton: { |
46 | margin: [40, 0, 10], | 46 | margin: [40, 'auto', 10], |
47 | display: 'flex', | ||
47 | }, | 48 | }, |
48 | allOptionsButton: { | 49 | allOptionsButton: { |
49 | margin: [0, 0, 40], | 50 | margin: [0, 0, 40], |
@@ -93,7 +94,6 @@ export default @injectSheet(styles) @observer class TrialForm extends Component | |||
93 | className={classes.activateTrialButton} | 94 | className={classes.activateTrialButton} |
94 | busy={isActivatingTrial} | 95 | busy={isActivatingTrial} |
95 | onClick={activateTrial} | 96 | onClick={activateTrial} |
96 | stretch | ||
97 | /> | 97 | /> |
98 | <Button | 98 | <Button |
99 | label={intl.formatMessage(messages.allOptionsButton)} | 99 | label={intl.formatMessage(messages.allOptionsButton)} |
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js index 1fd247d17..a7f6f4545 100644 --- a/src/components/ui/AppLoader/index.js +++ b/src/components/ui/AppLoader/index.js | |||
@@ -22,8 +22,13 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component | |||
22 | static propTypes = { | 22 | static propTypes = { |
23 | classes: PropTypes.object.isRequired, | 23 | classes: PropTypes.object.isRequired, |
24 | theme: PropTypes.object.isRequired, | 24 | theme: PropTypes.object.isRequired, |
25 | texts: PropTypes.array, | ||
25 | }; | 26 | }; |
26 | 27 | ||
28 | static defaultProps = { | ||
29 | texts: textList, | ||
30 | } | ||
31 | |||
27 | state = { | 32 | state = { |
28 | step: 0, | 33 | step: 0, |
29 | }; | 34 | }; |
@@ -43,7 +48,7 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component | |||
43 | } | 48 | } |
44 | 49 | ||
45 | render() { | 50 | render() { |
46 | const { classes, theme } = this.props; | 51 | const { classes, theme, texts } = this.props; |
47 | const { step } = this.state; | 52 | const { step } = this.state; |
48 | 53 | ||
49 | return ( | 54 | return ( |
@@ -52,7 +57,7 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component | |||
52 | className={classes.component} | 57 | className={classes.component} |
53 | spinnerColor={theme.colorAppLoaderSpinner} | 58 | spinnerColor={theme.colorAppLoaderSpinner} |
54 | > | 59 | > |
55 | {textList.map((text, i) => ( | 60 | {texts.map((text, i) => ( |
56 | <span | 61 | <span |
57 | key={text} | 62 | key={text} |
58 | className={classnames({ | 63 | className={classnames({ |
diff --git a/src/components/ui/FeatureItem.js b/src/components/ui/FeatureItem.js index 7c482c4d4..4926df470 100644 --- a/src/components/ui/FeatureItem.js +++ b/src/components/ui/FeatureItem.js | |||
@@ -10,6 +10,7 @@ const styles = theme => ({ | |||
10 | padding: [8, 0], | 10 | padding: [8, 0], |
11 | display: 'flex', | 11 | display: 'flex', |
12 | alignItems: 'center', | 12 | alignItems: 'center', |
13 | textAlign: 'left', | ||
13 | }, | 14 | }, |
14 | featureIcon: { | 15 | featureIcon: { |
15 | fill: theme.brandSuccess, | 16 | fill: theme.brandSuccess, |
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js index 62944ad75..7ba8b54d7 100644 --- a/src/components/ui/FeatureList.js +++ b/src/components/ui/FeatureList.js | |||
@@ -3,12 +3,33 @@ import PropTypes from 'prop-types'; | |||
3 | import { defineMessages, intlShape } from 'react-intl'; | 3 | import { defineMessages, intlShape } from 'react-intl'; |
4 | 4 | ||
5 | import { FeatureItem } from './FeatureItem'; | 5 | import { FeatureItem } from './FeatureItem'; |
6 | import { PLANS } from '../../config'; | ||
6 | 7 | ||
7 | const messages = defineMessages({ | 8 | const messages = defineMessages({ |
9 | availableRecipes: { | ||
10 | id: 'pricing.features.recipes', | ||
11 | defaultMessage: '!!!Choose from more than 70 Services', | ||
12 | }, | ||
13 | accountSync: { | ||
14 | id: 'pricing.features.accountSync', | ||
15 | defaultMessage: '!!!Account Synchronisation', | ||
16 | }, | ||
17 | desktopNotifications: { | ||
18 | id: 'pricing.features.desktopNotifications', | ||
19 | defaultMessage: '!!!Desktop Notifications', | ||
20 | }, | ||
8 | unlimitedServices: { | 21 | unlimitedServices: { |
9 | id: 'pricing.features.unlimitedServices', | 22 | id: 'pricing.features.unlimitedServices', |
10 | defaultMessage: '!!!Add unlimited services', | 23 | defaultMessage: '!!!Add unlimited services', |
11 | }, | 24 | }, |
25 | upToThreeServices: { | ||
26 | id: 'pricing.features.upToThreeServices', | ||
27 | defaultMessage: '!!!Add up to 3 services', | ||
28 | }, | ||
29 | upToSixServices: { | ||
30 | id: 'pricing.features.upToSixServices', | ||
31 | defaultMessage: '!!!Add up to 6 services', | ||
32 | }, | ||
12 | spellchecker: { | 33 | spellchecker: { |
13 | id: 'pricing.features.spellchecker', | 34 | id: 'pricing.features.spellchecker', |
14 | defaultMessage: '!!!Spellchecker support', | 35 | defaultMessage: '!!!Spellchecker support', |
@@ -51,11 +72,13 @@ export class FeatureList extends Component { | |||
51 | static propTypes = { | 72 | static propTypes = { |
52 | className: PropTypes.string, | 73 | className: PropTypes.string, |
53 | featureClassName: PropTypes.string, | 74 | featureClassName: PropTypes.string, |
75 | plan: PropTypes.oneOf(PLANS), | ||
54 | }; | 76 | }; |
55 | 77 | ||
56 | static defaultProps = { | 78 | static defaultProps = { |
57 | className: '', | 79 | className: '', |
58 | featureClassName: '', | 80 | featureClassName: '', |
81 | plan: false, | ||
59 | } | 82 | } |
60 | 83 | ||
61 | static contextTypes = { | 84 | static contextTypes = { |
@@ -66,21 +89,52 @@ export class FeatureList extends Component { | |||
66 | const { | 89 | const { |
67 | className, | 90 | className, |
68 | featureClassName, | 91 | featureClassName, |
92 | plan, | ||
69 | } = this.props; | 93 | } = this.props; |
70 | const { intl } = this.context; | 94 | const { intl } = this.context; |
71 | 95 | ||
96 | const features = []; | ||
97 | if (plan === PLANS.FREE) { | ||
98 | features.push( | ||
99 | messages.upToThreeServices, | ||
100 | messages.availableRecipes, | ||
101 | messages.accountSync, | ||
102 | messages.desktopNotifications, | ||
103 | ); | ||
104 | } else if (plan === PLANS.PERSONAL) { | ||
105 | features.push( | ||
106 | messages.upToSixServices, | ||
107 | messages.spellchecker, | ||
108 | messages.appDelays, | ||
109 | messages.adFree, | ||
110 | ); | ||
111 | } else if (plan === PLANS.PRO) { | ||
112 | features.push( | ||
113 | messages.unlimitedServices, | ||
114 | messages.workspaces, | ||
115 | messages.customWebsites, | ||
116 | // messages.onPremise, | ||
117 | messages.thirdPartyServices, | ||
118 | // messages.serviceProxies, | ||
119 | ); | ||
120 | } else { | ||
121 | features.push( | ||
122 | messages.unlimitedServices, | ||
123 | messages.spellchecker, | ||
124 | messages.workspaces, | ||
125 | messages.customWebsites, | ||
126 | messages.onPremise, | ||
127 | messages.thirdPartyServices, | ||
128 | messages.serviceProxies, | ||
129 | messages.teamManagement, | ||
130 | messages.appDelays, | ||
131 | messages.adFree, | ||
132 | ); | ||
133 | } | ||
134 | |||
72 | return ( | 135 | return ( |
73 | <ul className={className}> | 136 | <ul className={className}> |
74 | <FeatureItem name={intl.formatMessage(messages.unlimitedServices)} className={featureClassName} /> | 137 | {features.map(feature => <FeatureItem name={intl.formatMessage(feature)} className={featureClassName} />)} |
75 | <FeatureItem name={intl.formatMessage(messages.spellchecker)} className={featureClassName} /> | ||
76 | <FeatureItem name={intl.formatMessage(messages.workspaces)} className={featureClassName} /> | ||
77 | <FeatureItem name={intl.formatMessage(messages.customWebsites)} className={featureClassName} /> | ||
78 | <FeatureItem name={intl.formatMessage(messages.onPremise)} className={featureClassName} /> | ||
79 | <FeatureItem name={intl.formatMessage(messages.thirdPartyServices)} className={featureClassName} /> | ||
80 | <FeatureItem name={intl.formatMessage(messages.serviceProxies)} className={featureClassName} /> | ||
81 | <FeatureItem name={intl.formatMessage(messages.teamManagement)} className={featureClassName} /> | ||
82 | <FeatureItem name={intl.formatMessage(messages.appDelays)} className={featureClassName} /> | ||
83 | <FeatureItem name={intl.formatMessage(messages.adFree)} className={featureClassName} /> | ||
84 | </ul> | 138 | </ul> |
85 | ); | 139 | ); |
86 | } | 140 | } |
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js index 7ba353be3..36bf38c98 100644 --- a/src/components/ui/PremiumFeatureContainer/index.js +++ b/src/components/ui/PremiumFeatureContainer/index.js | |||
@@ -9,7 +9,7 @@ import { oneOrManyChildElements } from '../../../prop-types'; | |||
9 | import UserStore from '../../../stores/UserStore'; | 9 | import UserStore from '../../../stores/UserStore'; |
10 | 10 | ||
11 | import styles from './styles'; | 11 | import styles from './styles'; |
12 | import FeatureStore from '../../../stores/FeaturesStore'; | 12 | import FeaturesStore from '../../../stores/FeaturesStore'; |
13 | 13 | ||
14 | const messages = defineMessages({ | 14 | const messages = defineMessages({ |
15 | action: { | 15 | action: { |
@@ -90,7 +90,7 @@ PremiumFeatureContainer.wrappedComponent.propTypes = { | |||
90 | children: oneOrManyChildElements.isRequired, | 90 | children: oneOrManyChildElements.isRequired, |
91 | stores: PropTypes.shape({ | 91 | stores: PropTypes.shape({ |
92 | user: PropTypes.instanceOf(UserStore).isRequired, | 92 | user: PropTypes.instanceOf(UserStore).isRequired, |
93 | features: PropTypes.instanceOf(FeatureStore).isRequired, | 93 | features: PropTypes.instanceOf(FeaturesStore).isRequired, |
94 | }).isRequired, | 94 | }).isRequired, |
95 | actions: PropTypes.shape({ | 95 | actions: PropTypes.shape({ |
96 | ui: PropTypes.shape({ | 96 | ui: PropTypes.shape({ |