From 8811f6d38236ad273f8c9fcdc029563ab6ecd31e Mon Sep 17 00:00:00 2001 From: Manuel Bouza Date: Wed, 20 Feb 2019 15:40:13 +0100 Subject: [PATCH] Render App in iframe --- package.json | 1 + src/css/_form.scss | 23 ++- src/css/_mixins.scss | 63 ------ src/css/_reset.scss | 45 ----- src/css/_spinner.scss | 2 +- src/css/content.scss | 75 +++++-- src/css/options.scss | 4 + src/css/popup.scss | 5 + src/css/styles.css | 22 --- src/js/background.js | 20 +- src/js/components/App.js | 125 ++++++++++++ src/js/components/Bubble.js | 185 ++++++++---------- src/js/components/Form.js | 9 +- .../components/InvalidConfigurationError.js | 12 +- src/js/components/Modal.js | 22 --- src/js/components/{Setup.js => Options.js} | 6 +- src/js/components/Popup.js | 41 ++++ src/js/content.js | 32 +-- src/js/options.js | 12 +- src/js/popup.js | 18 ++ src/js/utils/index.js | 36 ++-- src/js/utils/urlMatcher.js | 6 +- src/manifest.json | 15 +- src/popup.html | 9 + webpack.config.js | 63 +++--- yarn.lock | 13 ++ 26 files changed, 484 insertions(+), 380 deletions(-) delete mode 100644 src/css/_mixins.scss delete mode 100644 src/css/_reset.scss create mode 100644 src/css/popup.scss delete mode 100644 src/css/styles.css create mode 100644 src/js/components/App.js delete mode 100644 src/js/components/Modal.js rename src/js/components/{Setup.js => Options.js} (93%) create mode 100644 src/js/components/Popup.js create mode 100644 src/js/popup.js create mode 100644 src/popup.html diff --git a/package.json b/package.json index bbbb52e..5f40134 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "mobx": "^5.5.0", "mobx-react": "^5.2.8", "prop-types": "^15.6.2", + "query-string": "^6.2.0", "react": "^16.8.0", "react-dom": "^16.8.0", "react-select": "^2.3.0", diff --git a/src/css/_form.scss b/src/css/_form.scss index b8f0dd6..e4d4451 100644 --- a/src/css/_form.scss +++ b/src/css/_form.scss @@ -13,10 +13,13 @@ input { } input, textarea { - padding: 0.25rem 0.5rem; + padding: 0.5rem; background-color: white; border-color: #cccccc; width: 100%; + font-size: 100%; + border-style: solid; + border-width: 1px; } text-muted { @@ -63,7 +66,7 @@ input[name="hours"] { textarea[name="description"] { resize: none; - width: 100%; + width: calc(100% - 1rem); } button { @@ -77,6 +80,7 @@ button { background-image: none; background-color: #7dc332; border-color: #7dc332; + font-size: 100%; cursor: pointer; &:hover:not(:disabled) { @@ -88,4 +92,19 @@ button { opacity: 0.65; cursor: default; } + + &.secondary { + color: black; + background-color: #fff; + border-color: #ccc; + + &:hover { + background-color: #f4f4f4; + border-color: #ccc; + } + } + + & + button { + margin-left: 0.5rem; + } } diff --git a/src/css/_mixins.scss b/src/css/_mixins.scss deleted file mode 100644 index 33d00da..0000000 --- a/src/css/_mixins.scss +++ /dev/null @@ -1,63 +0,0 @@ -$spacer: 1rem !default; -$spacers: ( - 0: 0, - 1: ( - $spacer * 0.25 - ), - 2: ( - $spacer * 0.5 - ), - 3: $spacer, - 4: ( - $spacer * 1.5 - ), - 5: ( - $spacer * 3 - ) -); - -@each $prop, $abbrev in (margin: m, padding: p) { - @each $size, $length in $spacers { - .#{$abbrev}-#{$size} { - #{$prop}: $length !important; - } - .#{$abbrev}t-#{$size}, - .#{$abbrev}y-#{$size} { - #{$prop}-top: $length !important; - } - .#{$abbrev}r-#{$size}, - .#{$abbrev}x-#{$size} { - #{$prop}-right: $length !important; - } - .#{$abbrev}b-#{$size}, - .#{$abbrev}y-#{$size} { - #{$prop}-bottom: $length !important; - } - .#{$abbrev}l-#{$size}, - .#{$abbrev}x-#{$size} { - #{$prop}-left: $length !important; - } - } -} - -@each $size, $length in $spacers { - .m-auto { - margin: auto !important; - } - .mt-auto, - .my-auto { - margin-top: auto !important; - } - .mr-auto, - .mx-auto { - margin-right: auto !important; - } - .mb-auto, - .my-auto { - margin-bottom: auto !important; - } - .ml-auto, - .mx-auto { - margin-left: auto !important; - } -} diff --git a/src/css/_reset.scss b/src/css/_reset.scss deleted file mode 100644 index 356a32d..0000000 --- a/src/css/_reset.scss +++ /dev/null @@ -1,45 +0,0 @@ -#moco-bx-bubble, #moco-bx-container { - html, body, div, span, applet, object, iframe, - h1, h2, h3, h4, h5, h6, p, blockquote, pre, - a, abbr, acronym, address, big, cite, code, - del, dfn, em, img, ins, kbd, q, s, samp, - small, strike, strong, sub, sup, tt, var, - b, u, i, center, - dl, dt, dd, ol, ul, li, - fieldset, form, label, legend, - table, caption, tbody, tfoot, thead, tr, th, td, - article, aside, canvas, details, embed, - figure, figcaption, footer, header, hgroup, - menu, nav, output, ruby, section, summary, - time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; - } - /* HTML5 display-role reset for older browsers */ - article, aside, details, figcaption, figure, - footer, header, hgroup, menu, nav, section { - display: block; - } - body { - line-height: 1; - } - ol, ul { - list-style: none; - } - blockquote, q { - quotes: none; - } - blockquote:before, blockquote:after, - q:before, q:after { - content: ''; - content: none; - } - table { - border-collapse: collapse; - border-spacing: 0; - } -} diff --git a/src/css/_spinner.scss b/src/css/_spinner.scss index acbbc68..c911442 100644 --- a/src/css/_spinner.scss +++ b/src/css/_spinner.scss @@ -1,4 +1,4 @@ -#moco-bx-bubble, #moco-bx-container { +#moco-bx-root { @keyframes moco-bx-spinner { to { transform: rotate(360deg); diff --git a/src/css/content.scss b/src/css/content.scss index 23d304e..3170a19 100644 --- a/src/css/content.scss +++ b/src/css/content.scss @@ -1,7 +1,3 @@ -@import "mixins"; -@import "form"; -@import "spinner"; - #moco-bx-root { position: fixed; bottom: 40px; @@ -17,21 +13,42 @@ 2px 2px 15px 4px rgba(0, 0, 0, 0.05); padding: 5px; - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; - cursor: pointer; + iframe { + border: 0; + } + .moco-bx-bubble { - img { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + width: 100%; + height: 100%; + + img.moco-logo { width: 30px; height: 30px; } + + .moco-bx-badge { + display: inline-block; + min-width: 10px; + padding: 2px 4px; + font-size: 12px; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-color: #7dc332; + border-radius: 10px; + } } - .moco-bx-modal { + .moco-bx-popup { position: fixed; /* Stay in place */ z-index: 2000; /* Sit on top */ padding-top: 100px; /* Location of the box */ @@ -43,11 +60,43 @@ background-color: rgb(0, 0, 0); /* Fallback color */ background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ - .moco-bx-modal-content { + .moco-bx-popup-content { background-color: white; width: 600px; - padding: 40px; + height: 400px; + padding: 2rem; margin: 0 auto; } } + + #moco-bx-invalid-configuration-error { + img { + width: 536px; + } + } + + button { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-weight: normal; + text-align: center; + white-space: nowrap; + color: white; + background-image: none; + background-color: #7dc332; + border-color: #7dc332; + border-radius: 0; + cursor: pointer; + + &:hover:not(:disabled) { + background-color: #639a28; + border-color: #639a28; + } + + &:disabled { + opacity: 0.65; + cursor: default; + } + } } diff --git a/src/css/options.scss b/src/css/options.scss index e69de29..3024886 100644 --- a/src/css/options.scss +++ b/src/css/options.scss @@ -0,0 +1,4 @@ +@import "form"; + +#moco-bx-root { +} diff --git a/src/css/popup.scss b/src/css/popup.scss new file mode 100644 index 0000000..a63683b --- /dev/null +++ b/src/css/popup.scss @@ -0,0 +1,5 @@ +@import "form"; +@import "spinner"; + +#moco-bx-root { +} diff --git a/src/css/styles.css b/src/css/styles.css deleted file mode 100644 index 22e7901..0000000 --- a/src/css/styles.css +++ /dev/null @@ -1,22 +0,0 @@ -#moco-bx-root { - position: fixed; - bottom: 40px; - left: 50%; - margin-left: -30px; - z-index: 1000; - - height: 60px; - width: 60px; - background-color: white; - border-radius: 50%; - box-shadow: -1px -1px 15px 4px rgba(0, 0, 0, 0.05), - 2px 2px 15px 4px rgba(0, 0, 0, 0.05); - padding: 5px; - - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; - - cursor: pointer; -} diff --git a/src/js/background.js b/src/js/background.js index 0c60a71..e5cee5b 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -1,5 +1,5 @@ -import { createMatcher } from "utils/urlMatcher" -import remoteServices from "./remoteServices" +import { createMatcher } from 'utils/urlMatcher' +import remoteServices from './remoteServices' const matcher = createMatcher(remoteServices) const { version } = chrome.runtime.getManifest() @@ -7,7 +7,7 @@ const registeredTabIds = new Set() chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { // run only after the page is fully loaded - if (changeInfo.status != "complete") { + if (changeInfo.status != 'complete') { return } @@ -16,28 +16,28 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (service) { registeredTabIds.add(tabId) chrome.storage.sync.get( - ["subdomain", "apiKey"], + ['subdomain', 'apiKey'], ({ subdomain, apiKey }) => { const payload = { subdomain, apiKey, version } - chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }) + chrome.tabs.sendMessage(tabId, { type: 'mountBubble', payload }) } ) } else { registeredTabIds.delete(tabId) - chrome.tabs.sendMessage(tabId, { type: "unmountBubble" }) + chrome.tabs.sendMessage(tabId, { type: 'unmountBubble' }) } }) chrome.tabs.onRemoved.addListener(tabId => registeredTabIds.delete(tabId)) chrome.storage.onChanged.addListener(({ apiKey, subdomain }, areaName) => { - if (areaName === "sync" && (apiKey || subdomain)) { + if (areaName === 'sync' && (apiKey || subdomain)) { chrome.storage.sync.get( - ["subdomain", "apiKey"], + ['subdomain', 'apiKey'], ({ subdomain, apiKey }) => { const payload = { subdomain, apiKey, version } for (let tabId of registeredTabIds.values()) { - chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }) + chrome.tabs.sendMessage(tabId, { type: 'mountBubble', payload }) } } ) @@ -46,7 +46,7 @@ chrome.storage.onChanged.addListener(({ apiKey, subdomain }, areaName) => { chrome.runtime.onMessage.addListener(({ type }) => { switch (type) { - case "openOptions": { + case 'openOptions': { chrome.tabs.create({ url: `chrome://extensions/?options=${chrome.runtime.id}` }) diff --git a/src/js/components/App.js b/src/js/components/App.js new file mode 100644 index 0000000..915be1e --- /dev/null +++ b/src/js/components/App.js @@ -0,0 +1,125 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" +import ApiClient from "api/Client" +import Form from "components/Form" +import Spinner from "components/Spinner" +import { observable, computed, reaction } from "mobx" +import { observer, disposeOnUnmount } from "mobx-react" +import { + findLastProject, + findLastTask, + groupedProjectOptions, + currentDate, + secondsFromHours +} from "utils" +import { head } from "lodash" + +@observer +class App extends Component { + static propTypes = { + service: PropTypes.shape({ + id: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + projectId: PropTypes.string, + taskId: PropTypes.string + }).isRequired, + projects: PropTypes.arrayOf(PropTypes.object).isRequired, + lastProjectId: PropTypes.number, + lastTaskId: PropTypes.number, + browser: PropTypes.object.isRequired + }; + + @observable changeset = {}; + @observable formErrors = {}; + + @computed get changesetWithDefaults() { + const { service, projects, lastProjectId, lastTaskId } = this.props + + const project = + findLastProject(service.projectId || lastProjectId)(projects) || + head(projects) + + const task = + findLastTask(service.taskId || lastTaskId)(project) || + head(project?.tasks) + + const defaults = { + remote_service: service.name, + remote_id: service.id, + remote_url: service.url, + date: currentDate(), + assignment_id: project?.value, + task_id: task?.value, + billable: task?.billable, + hours: "", + seconds: secondsFromHours(this.changeset.hours), + description: service.description + } + + return { + ...defaults, + ...this.changeset + } + } + + componentDidMount() { + window.addEventListener("keydown", this.handleKeyDown) + } + + componentWillUnmount() { + window.removeEventListener("keydown", this.handleKeyDown) + } + + handleKeyDown = event => { + event.stopPropagation() + if (event.keyCode === 27) { + this.sendMessage({ type: 'closeForm' }) + } + }; + + handleChange = event => { + const { + target: { name, value } + } = event + + this.changeset[name] = value + + if (name === "assignment_id") { + this.changeset.task_id = null + } + }; + + handleSubmit = event => { + event.preventDefault() + this.sendMessage({ type: 'submitForm', payload: this.changesetWithDefaults }) + } + + handleCancel = () => { + this.sendMessage({ type: 'closeForm' }) + } + + sendMessage = action => + this.props.browser.tabs.query( + { active: true, currentWindow: true }, + tabs => chrome.tabs.sendMessage(tabs[0].id, action) + ) + + render() { + const { service, projects } = this.props; + + return ( +
+ ) + } +} + +export default App diff --git a/src/js/components/Bubble.js b/src/js/components/Bubble.js index 0efe667..762ffe7 100644 --- a/src/js/components/Bubble.js +++ b/src/js/components/Bubble.js @@ -1,7 +1,7 @@ import React, { Component } from "react" import PropTypes from "prop-types" import ApiClient from "api/Client" -import Modal from "components/Modal" +import Popup from "components/Popup" import InvalidConfigurationError from "components/InvalidConfigurationError" import Form from "components/Form" import Spinner from "components/Spinner" @@ -32,14 +32,15 @@ class Bubble extends Component { subdomain: PropTypes.string, apiKey: PropTypes.string, version: PropTypes.string - }) + }), + browser: PropTypes.object.isRequired, }; #apiClient; @observable isLoading = false; @observable isOpen = false; - @observable projects; + @observable projects = []; @observable lastProjectId; @observable lastTaskId; @observable bookedHours = 0; @@ -47,91 +48,77 @@ class Bubble extends Component { @observable formErrors = {}; @observable unauthorizedError = false; - @computed get changesetWithDefaults() { - const { service } = this.props - - const project = - findLastProject(service.projectId || this.lastProjectId)(this.projects) || - head(this.projects) - - const task = findLastTask(service.taskId || this.lastTaskId)(project) - - const defaults = { - remote_service: service.name, - remote_id: service.id, - remote_url: window.location.href, - date: currentDate(), - assignment_id: project?.value, - task_id: task?.value, - billable: task?.billable, - hours: "", - seconds: secondsFromHours(this.changeset.hours), - description: service.description - } - - return { - ...defaults, - ...this.changeset - } - } - constructor(props) { super(props) - this.#apiClient = new ApiClient(props.settings) + this.initializeApiClient(props.settings) } componentDidMount() { disposeOnUnmount( this, - reaction( - () => (this.hasInvalidConfiguration() ? null : this.props.settings), - this.fetchProjects, - { - fireImmediately: true - } - ) + reaction(() => this.props.settings, settings => { + this.initializeApiClient(settings) + this.fetchBookedHours() + this.close() + }) ) + disposeOnUnmount( this, reaction(() => this.props.service, this.fetchBookedHours, { fireImmediately: true }) ) - window.addEventListener("keydown", this.handleKeyDown) + this.props.browser.runtime.onMessage.addListener(this.receiveMessage) + window.addEventListener("keydown", this.handleKeyDown, true) } componentWillUnmount() { + this.props.browser.runtime.onMessage.removeListener(this.receiveMessage) window.removeEventListener("keydown", this.handleKeyDown) } - open = _event => { + open = event => { + if (event && event.target && event.target.classList.contains('moco-bx-popup')) { + return this.close() + } this.isOpen = true + this.fetchProjects().then(() => (this.isOpen = true)) }; close = _event => { this.isOpen = false }; - hasInvalidConfiguration = () => { - const { settings } = this.props - return ["subdomain", "apiKey"].some(key => !settings[key]) - }; + receiveMessage = ({ type, payload }) => { + switch(type) { + case 'submitForm': { + return this.createActivity(payload).then(() => this.close()) + } + case 'closeForm': { + return this.close() + } + } + } - fetchProjects = settings => { - if (!settings) { - return + initializeApiClient = settings => { + this.#apiClient = new ApiClient(settings) + } + + fetchProjects = () => { + if (this.projects.length > 0) { + return Promise.resolve(); } this.isLoading = true - this.#apiClient = new ApiClient(settings) - this.#apiClient + return this.#apiClient .projects() .then(({ data }) => { + this.unauthorizedError = false this.projects = groupedProjectOptions(data.projects) this.lastProjectId = data.last_project_id this.lastTaskId = data.lastTaskId - this.unauthorizedError = false }) .catch(error => { if (error.response?.status === 401) { @@ -143,12 +130,16 @@ class Bubble extends Component { }) }; - fetchBookedHours = service => { + fetchBookedHours = () => { + const { service } = this.props this.isLoading = true this.#apiClient .bookedHours(service) - .then(({ data }) => (this.bookedHours = parseFloat(data[0]?.hours) || 0)) + .then(({ data }) => { + this.bookedHours = parseFloat(data[0]?.hours) || 0 + this.unauthorizedError = false + }) .catch(error => { if (error.response?.status === 401) { this.unauthorizedError = true @@ -157,83 +148,63 @@ class Bubble extends Component { .finally(() => (this.isLoading = false)) }; - // EVENT HANDLERS ----------------------------------------------------------- - - handleKeyDown = event => { - event.stopPropagation() - if (event.keyCode === 27) { - this.close() - } - }; - - handleChange = event => { - const { - target: { name, value } - } = event - - this.changeset[name] = value - - if (name === "assignment_id") { - this.changeset.task_id = null - } - }; - - handleSubmit = event => { - event.preventDefault() + createActivity = payload => this.#apiClient - .createActivity(this.changesetWithDefaults) + .createActivity(payload) .then(({ data }) => { - this.close() this.bookedHours += data.hours this.changeset = {} this.formErrors = {} + this.unauthorizedError = false }) .catch(this.handleSubmitError) - }; handleSubmitError = error => { if (error.response?.status === 422) { this.formErrors = error.response.data } + if (error.response?.status === 401) { + this.unauthorizedError = true + } + }; + + handleKeyDown = event => { + if (event.key === 'm' && (event.metaKey || event.ctrlKey)) { + event.preventDefault() + this.open() + } + }; + + hasInvalidConfiguration = () => { + const { settings } = this.props + return ["subdomain", "apiKey"].some(key => !settings[key]) }; // RENDER ------------------------------------------------------------------- - renderContent = () => { - if (this.unauthorizedError || this.hasInvalidConfiguration()) { - return - } else if (this.isOpen) { - return ( - - ) - } else { - return null - } - } - render() { if (this.isLoading) { return } + const { service, browser } = this.props; + return ( -
- - {this.bookedHours > 0 && {this.bookedHours}h} +
+ + {this.bookedHours > 0 + ? {this.bookedHours}h + : null + } {this.isOpen && ( - - {this.renderContent()} - + )}
) diff --git a/src/js/components/Form.js b/src/js/components/Form.js index 455d727..0567605 100644 --- a/src/js/components/Form.js +++ b/src/js/components/Form.js @@ -5,7 +5,6 @@ import cn from "classnames" class Form extends Component { static propTypes = { - isLoading: PropTypes.bool.isRequired, changeset: PropTypes.shape({ project: PropTypes.object, task: PropTypes.object, @@ -14,7 +13,8 @@ class Form extends Component { errors: PropTypes.object, projects: PropTypes.array.isRequired, onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired }; static defaultProps = { @@ -31,10 +31,6 @@ class Form extends Component { // RENDER ------------------------------------------------------------------- render() { - if (this.isLoading) { - return null - } - const { projects, changeset, errors, onChange, onSubmit } = this.props const project = Select.findOptionByValue(projects, changeset.assignment_id) @@ -95,6 +91,7 @@ class Form extends Component {
+ ) } diff --git a/src/js/components/InvalidConfigurationError.js b/src/js/components/InvalidConfigurationError.js index d01c086..7dd2ca2 100644 --- a/src/js/components/InvalidConfigurationError.js +++ b/src/js/components/InvalidConfigurationError.js @@ -1,20 +1,22 @@ -import React from "react" -import configurationSettingsUrl from "images/configurationSettings.png" +import React from 'react' +import configurationSettingsUrl from 'images/configurationSettings.png' const InvalidConfigurationError = () => ( -
+

Konfiguration ungültig

Bitte trage deine Internetadresse und deinen API-Schlüssel in den Einstellungen der MOCO Browser-Erweiterung ein. Deinen API-Key findest du in der MOCO App in deinem Profil im Register "Integrationen".

- +
+
Browser extension configuration settings
) diff --git a/src/js/components/Modal.js b/src/js/components/Modal.js deleted file mode 100644 index d0d2a5e..0000000 --- a/src/js/components/Modal.js +++ /dev/null @@ -1,22 +0,0 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" - -class Modal extends Component { - static propTypes = { - children: PropTypes.node.isRequired - } - - // RENDER ------------------------------------------------------------------- - - render() { - return ( -
-
- {this.props.children} -
-
- ) - } -} - -export default Modal diff --git a/src/js/components/Setup.js b/src/js/components/Options.js similarity index 93% rename from src/js/components/Setup.js rename to src/js/components/Options.js index 7c4f4a2..a0a6a37 100644 --- a/src/js/components/Setup.js +++ b/src/js/components/Options.js @@ -3,7 +3,7 @@ import { observable } from "mobx" import { observer } from "mobx-react" @observer -class Setup extends Component { +class Options extends Component { @observable loading = true @observable subdomain = "" @observable apiKey = "" @@ -62,7 +62,7 @@ class Setup extends Component { value={this.apiKey} onChange={this.onChange} /> -
+
Deinen API-Schlüssel findest du in der MOCO-App unter Profil/Integrationen.
@@ -73,4 +73,4 @@ class Setup extends Component { } } -export default Setup +export default Options diff --git a/src/js/components/Popup.js b/src/js/components/Popup.js new file mode 100644 index 0000000..9b9df03 --- /dev/null +++ b/src/js/components/Popup.js @@ -0,0 +1,41 @@ +import React, { Component, useMemo } from 'react' +import PropTypes from 'prop-types' +import InvalidConfigurationError from 'components/InvalidConfigurationError' +import queryString from 'query-string' +import { serializeProps } from 'utils' + +const Popup = props => { + const serializedProps = serializeProps( + ['service', 'projects', 'lastProjectId', 'lastTaskId'] + )(props) + + const styles = useMemo(() => ({ + width: '536px', + height: props.unauthorizedError ? '890px' : '400px' + }), [props.unauthorizedError]) + + return ( +
+
+ {props.unauthorizedError + ? + :