diff --git a/__tests__/src/components/App.test.js b/__tests__/src/components/App.test.js index e67b7edf3203f1aa1304b2293d192969d9a743c8..5046765124baa2018ddb8769162a0b42a03b0fd9 100644 --- a/__tests__/src/components/App.test.js +++ b/__tests__/src/components/App.test.js @@ -2,8 +2,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import PluginProvider from '../../../src/extend/PluginProvider'; import AppProviders from '../../../src/containers/AppProviders'; -import AccessTokenSender from '../../../src/containers/AccessTokenSender'; -import AuthenticationSender from '../../../src/containers/AuthenticationSender'; import { App } from '../../../src/components/App'; /** */ @@ -21,7 +19,5 @@ describe('App', () => { expect(wrapper.find(PluginProvider).length).toBe(1); expect(wrapper.find(AppProviders).length).toBe(1); expect(wrapper.find('Suspense').length).toBe(1); - expect(wrapper.find(AuthenticationSender).length).toBe(1); - expect(wrapper.find(AccessTokenSender).length).toBe(1); }); }); diff --git a/__tests__/src/components/AuthenticationLogout.test.js b/__tests__/src/components/AuthenticationLogout.test.js deleted file mode 100644 index e120d6e9188e17a7cbcacfb18993dad313de7de4..0000000000000000000000000000000000000000 --- a/__tests__/src/components/AuthenticationLogout.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import Fab from '@material-ui/core/Fab'; -import { AuthenticationLogout } from '../../../src/components/AuthenticationLogout'; - -/** - * Helper function to create a shallow wrapper around AuthenticationLogout - */ -function createWrapper(props) { - return shallow( - <AuthenticationLogout - authServiceId="http://example.com/auth" - label="Log out now!" - logoutServiceId="http://example.com/logout" - status="ok" - t={key => key} - windowId="w" - {...props} - />, - ); -} - -describe('AuthenticationLogout', () => { - it('when status is not ok, render fragment', () => { - const wrapper = createWrapper({ status: 'fail' }); - expect(wrapper.matchesElement(<></>)).toBe(true); - }); - it('renders Fab with logout label', () => { - const wrapper = createWrapper(); - expect(wrapper.find(Fab).length).toBe(1); - expect(wrapper.find(Fab).text()).toBe('Log out now!'); - }); - it('click opens a new window to logout and resets state', () => { - const mockWindow = {}; - const open = jest.fn(() => mockWindow); - const reset = jest.fn(() => {}); - const wrapper = createWrapper({ depWindow: { open }, resetAuthenticationState: reset }); - expect(wrapper.find(Fab).props().onClick()); - expect(open).toHaveBeenCalledWith('http://example.com/logout'); - expect(reset).toHaveBeenCalledWith({ authServiceId: 'http://example.com/auth' }); - }); -}); diff --git a/__tests__/src/components/AuthenticationSender.test.js b/__tests__/src/components/AuthenticationSender.test.js deleted file mode 100644 index 4b18215e28673b93297a1e61bc9791b392c941a9..0000000000000000000000000000000000000000 --- a/__tests__/src/components/AuthenticationSender.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { NewWindow } from '../../../src/components/NewWindow'; -import { AuthenticationSender } from '../../../src/components/AuthenticationSender'; - -/** - * Helper function to create a shallow wrapper around ErrorDialog - */ -function createWrapper(props) { - return shallow( - <AuthenticationSender - t={key => key} - handleInteraction={() => {}} - {...props} - />, - ); -} - -describe('AuthenticationSender', () => { - let wrapper; - - it('renders nothing if there is no url', () => { - wrapper = createWrapper({}); - expect(wrapper.matchesElement(<></>)).toBe(true); - }); - - it('renders properly', () => { - Object.defineProperty(window, 'origin', { - value: 'http://localhost', - writable: true, - }); - wrapper = createWrapper({ url: 'http://example.com' }); - expect(wrapper.find(NewWindow).length).toBe(1); - expect(wrapper.find(NewWindow).props().url).toBe('http://example.com?origin=http://localhost'); - }); - - it('triggers an action when the window is unloaded', () => { - const handleInteraction = jest.fn(); - wrapper = createWrapper({ handleInteraction, url: 'http://example.com' }); - wrapper.find(NewWindow).simulate('close'); - - expect(handleInteraction).toHaveBeenCalledWith('http://example.com'); - }); -}); diff --git a/__tests__/src/components/IIIFAuthentication.test.js b/__tests__/src/components/IIIFAuthentication.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c43d762cfe973be06165903c5b2472dad96d069c --- /dev/null +++ b/__tests__/src/components/IIIFAuthentication.test.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import WindowAuthenticationBar from '../../../src/containers/WindowAuthenticationBar'; +import { NewWindow } from '../../../src/components/NewWindow'; +import { AccessTokenSender } from '../../../src/components/AccessTokenSender'; +import { IIIFAuthentication } from '../../../src/components/IIIFAuthentication'; + +/** + * Helper function to create a shallow wrapper around IIIFAuthentication + */ +function createWrapper(props) { + return shallow( + <IIIFAuthentication + accessTokenServiceId="http://example.com/token" + authServiceId="http://example.com/auth" + failureDescription="... and this is why." + failureHeader="Login failed" + handleAuthInteraction={() => {}} + isInteractive + logoutServiceId="http://example.com/logout" + resetAuthenticationState={() => {}} + resolveAccessTokenRequest={() => {}} + resolveAuthenticationRequest={() => {}} + t={key => key} + windowId="w" + {...props} + />, + ); +} + +describe('IIIFAuthentication', () => { + describe('without an auth service', () => { + it('renders nothing', () => { + const wrapper = createWrapper({ authServiceId: null }); + expect(wrapper.isEmptyRender()).toBe(true); + }); + }); + describe('with an available auth service', () => { + it('renders a login bar', () => { + const handleAuthInteraction = jest.fn(); + const wrapper = createWrapper({ handleAuthInteraction }); + expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); + expect(wrapper.find(WindowAuthenticationBar).simulate('confirm')); + expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth'); + }); + it('renders nothing for a non-interactive login', () => { + const wrapper = createWrapper({ isInteractive: false }); + expect(wrapper.isEmptyRender()).toBe(true); + }); + }); + describe('with a failed authentication', () => { + it('renders with an error message', () => { + const handleAuthInteraction = jest.fn(); + const wrapper = createWrapper({ handleAuthInteraction, status: 'failed' }); + expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); + expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('retry'); + expect(wrapper.find(WindowAuthenticationBar).prop('status')).toEqual('failed'); + expect(wrapper.find(WindowAuthenticationBar).prop('header')).toEqual('Login failed'); + expect(wrapper.find(WindowAuthenticationBar).prop('description')).toEqual('... and this is why.'); + expect(wrapper.find(WindowAuthenticationBar).simulate('confirm')); + expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth'); + }); + }); + describe('in the middle of authenicating', () => { + it('does the IIIF access cookie behavior', () => { + const wrapper = createWrapper({ status: 'cookie' }); + expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); + expect(wrapper.find(NewWindow).length).toBe(1); + expect(wrapper.find(NewWindow).prop('url')).toContain('http://example.com/auth?origin='); + }); + it('does the IIIF access token behavior', () => { + const wrapper = createWrapper({ status: 'token' }); + expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); + expect(wrapper.find(AccessTokenSender).length).toBe(1); + expect(wrapper.find(AccessTokenSender).prop('url')).toEqual('http://example.com/token'); + }); + }); + describe('when logged in', () => { + it('renders a logout button', () => { + const openWindow = jest.fn(); + const resetAuthenticationState = jest.fn(); + const wrapper = createWrapper({ openWindow, resetAuthenticationState, status: 'ok' }); + expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); + expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('logout'); + expect(wrapper.find(WindowAuthenticationBar).prop('hasLogoutService')).toEqual(true); + + wrapper.find(WindowAuthenticationBar).simulate('confirm'); + + expect(openWindow).toHaveBeenCalledWith('http://example.com/logout', 'centerscreen'); + expect(resetAuthenticationState).toHaveBeenCalledWith({ + authServiceId: 'http://example.com/auth', tokenServiceId: 'http://example.com/token', + }); + }); + }); +}); diff --git a/__tests__/src/components/Window.test.js b/__tests__/src/components/Window.test.js index e21220fa6b7efbf35eb33b892a992e702016db7a..c16b361fef230d10765295efaccef0786e4ec971 100644 --- a/__tests__/src/components/Window.test.js +++ b/__tests__/src/components/Window.test.js @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import { Window } from '../../../src/components/Window'; import WindowTopBar from '../../../src/containers/WindowTopBar'; import PrimaryWindow from '../../../src/containers/PrimaryWindow'; -import WindowAuthenticationControl from '../../../src/containers/WindowAuthenticationControl'; +import IIIFAuthentication from '../../../src/containers/IIIFAuthentication'; import ErrorContent from '../../../src/containers/ErrorContent'; /** create wrapper */ @@ -34,9 +34,9 @@ describe('Window', () => { wrapper = createWrapper(); expect(wrapper.find(PrimaryWindow)).toHaveLength(1); }); - it('renders <WindowAuthenticationControl>', () => { + it('renders <WindowAuthenticationBar>', () => { wrapper = createWrapper(); - expect(wrapper.find(WindowAuthenticationControl)).toHaveLength(1); + expect(wrapper.find(IIIFAuthentication)).toHaveLength(1); }); it('renders manifest error', () => { wrapper = createWrapper({ manifestError: 'Invalid JSON' }); diff --git a/__tests__/src/components/WindowAuthenticationBar.test.js b/__tests__/src/components/WindowAuthenticationBar.test.js new file mode 100644 index 0000000000000000000000000000000000000000..72e9b029922cac5e935d21f347103759ec7e3d80 --- /dev/null +++ b/__tests__/src/components/WindowAuthenticationBar.test.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Button from '@material-ui/core/Button'; +import Collapse from '@material-ui/core/Collapse'; +import DialogActions from '@material-ui/core/DialogActions'; +import SanitizedHtml from '../../../src/containers/SanitizedHtml'; +import { WindowAuthenticationBar } from '../../../src/components/WindowAuthenticationBar'; + +/** + * Helper function to create a shallow wrapper around AuthenticationLogout + */ +function createWrapper(props) { + return shallow( + <WindowAuthenticationBar + classes={{}} + hasLogoutService + confirmButton="Click here" + label="Log in to see more" + onConfirm={() => {}} + status="ok" + t={key => key} + windowId="w" + {...props} + />, + ); +} + +describe('AuthenticationControl', () => { + it('renders nothing if the user is logged in and there is no logout service', () => { + const wrapper = createWrapper({ hasLogoutService: false }); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders a non-collapsing version if there is no description', () => { + const wrapper = createWrapper({ description: undefined, header: undefined }); + expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more'); + expect(wrapper.find(Button).children().text()).toEqual('Click here'); + }); + + it('renders a collapsable version if there is a description', () => { + const onConfirm = jest.fn(); + const wrapper = createWrapper({ description: 'long description', header: 'header', onConfirm }); + expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more'); + expect(wrapper.find(Button).at(0).find('span').text()).toEqual('continue'); + // is expandable + expect(wrapper.find(Collapse).prop('in')).toEqual(false); + wrapper.find(Button).at(0).simulate('click'); + expect(wrapper.find(Collapse).prop('in')).toEqual(true); + + // has more information + expect(wrapper.find(Collapse).find(SanitizedHtml).at(0).props().htmlString).toEqual('header'); + expect(wrapper.find(Collapse).find(SanitizedHtml).at(1).props().htmlString).toEqual('long description'); + + // is recollapsable + wrapper.find(DialogActions).find(Button).at(0).simulate('click'); + expect(wrapper.find(Collapse).prop('in')).toEqual(false); + wrapper.find(Button).at(0).simulate('click'); + + // starts the auth process + wrapper.find(DialogActions).find(Button).at(1).simulate('click'); + expect(onConfirm).toHaveBeenCalled(); + }); + + it('triggers an action when the confirm button is clicked', () => { + const onConfirm = jest.fn(); + const wrapper = createWrapper({ + onConfirm, + }); + + wrapper.find(Button).simulate('click'); + expect(onConfirm).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/src/components/WindowAuthenticationControl.test.js b/__tests__/src/components/WindowAuthenticationControl.test.js deleted file mode 100644 index ffa31741da69f6c345cc8ee1de67ac0ba56e12d3..0000000000000000000000000000000000000000 --- a/__tests__/src/components/WindowAuthenticationControl.test.js +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import Button from '@material-ui/core/Button'; -import Collapse from '@material-ui/core/Collapse'; -import DialogActions from '@material-ui/core/DialogActions'; -import SanitizedHtml from '../../../src/containers/SanitizedHtml'; -import { WindowAuthenticationControl } from '../../../src/components/WindowAuthenticationControl'; -import AuthenticationLogout from '../../../src/containers/AuthenticationLogout'; - -/** - * Helper function to create a shallow wrapper around WindowAuthenticationControl - */ -function createWrapper(props) { - return shallow( - <WindowAuthenticationControl - t={key => key} - classes={{}} - degraded - handleAuthInteraction={() => {}} - label="authenticate" - windowId="w" - profile="http://iiif.io/api/auth/1/login" - {...props} - />, - ); -} - -describe('WindowAuthenticationControl', () => { - let wrapper; - - it('renders AuthenticationLogout if it is not degraded', () => { - wrapper = createWrapper({ degraded: false }); - expect(wrapper.find(AuthenticationLogout).length).toBe(1); - }); - - describe('with a non-interactive login', () => { - it('renders failure messages', () => { - wrapper = createWrapper({ - degraded: true, - failureDescription: 'failure description', - failureHeader: 'failure header', - profile: 'http://iiif.io/api/auth/1/external', - status: 'failed', - }); - expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header'); - expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description'); - expect(wrapper.find(DialogActions)).toHaveLength(1); - expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2); - expect(wrapper.find(DialogActions).find(Button).at(1).children() - .text()).toEqual('retry'); - }); - }); - - it('renders properly', () => { - wrapper = createWrapper({ confirmLabel: 'some confirm label', description: 'some description' }); - expect(wrapper.find(SanitizedHtml).at(2).props().htmlString).toEqual('some description'); - expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2); - expect(wrapper.find(DialogActions).find(Button).at(1).children() - .text()).toEqual('some confirm label'); - }); - - it('hides the cancel button if there is nothing to collapose', () => { - wrapper = createWrapper({ classes: { topBar: 'topBar' }, confirmLabel: 'some confirm label' }); - expect(wrapper.find('.topBar').children().find(Button).at(0) - .children() - .text()).toEqual('some confirm label'); - - expect(wrapper.find(DialogActions).find(Button)).toHaveLength(0); - }); - - it('shows the auth dialog when the login button is clicked', () => { - wrapper = createWrapper({ classes: { topBar: 'topBar' }, description: 'some description' }); - wrapper.find('.topBar').props().onClick(); - expect(wrapper.find(Collapse).props().in).toEqual(true); - }); - - it('triggers an action when the confirm button is clicked', () => { - const handleAuthInteraction = jest.fn(); - wrapper = createWrapper({ - confirmLabel: 'some confirm label', - description: 'some description', - handleAuthInteraction, - infoId: 'i', - serviceId: 's', - }); - wrapper.instance().setState({ open: true }); - expect(wrapper.find(Collapse).props().in).toEqual(true); - - wrapper.find(DialogActions).find(Button).at(1).simulate('click'); - expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'i', 's'); - }); - - it('displays a failure message if the login has failed', () => { - wrapper = createWrapper({ - failureDescription: 'failure description', - failureHeader: 'failure header', - status: 'failed', - }); - - expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header'); - expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description'); - - expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2); - expect(wrapper.find(DialogActions).find(Button).at(1).children() - .text()).toEqual('login'); - }); - - it('displays the login messages if the user dismisses the failure messages', () => { - wrapper = createWrapper({ - failureDescription: 'failure description', - failureHeader: 'failure header', - status: 'failed', - }); - - expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header'); - expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description'); - - wrapper.find(DialogActions).find(Button).at(0).simulate('click'); - - expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('authenticate'); - }); -}); diff --git a/src/components/App.js b/src/components/App.js index a415d6ff823e2b900aa84345f993ccf874858530..ed42b7aabf312e6b8df56865f8bb23311b8d06c8 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -2,8 +2,6 @@ import React, { Component, lazy, Suspense } from 'react'; import PropTypes from 'prop-types'; import PluginProvider from '../extend/PluginProvider'; import AppProviders from '../containers/AppProviders'; -import AuthenticationSender from '../containers/AuthenticationSender'; -import AccessTokenSender from '../containers/AccessTokenSender'; const WorkspaceArea = lazy(() => import('../containers/WorkspaceArea')); @@ -22,8 +20,6 @@ export class App extends Component { return ( <PluginProvider plugins={plugins}> <AppProviders dndManager={dndManager}> - <AuthenticationSender /> - <AccessTokenSender /> <Suspense fallback={<div />} > diff --git a/src/components/AuthenticationLogout.js b/src/components/AuthenticationLogout.js deleted file mode 100644 index 82c37de15f562c0053fa00a740d31ada12b1bfbb..0000000000000000000000000000000000000000 --- a/src/components/AuthenticationLogout.js +++ /dev/null @@ -1,57 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Fab from '@material-ui/core/Fab'; - -/** - * - */ -export class AuthenticationLogout extends Component { - /** */ - constructor(props) { - super(props); - - this.handleLogout = this.handleLogout.bind(this); - } - - /** */ - handleLogout() { - const { - authServiceId, depWindow, logoutServiceId, resetAuthenticationState, - } = this.props; - (depWindow || window).open(logoutServiceId); - - resetAuthenticationState({ authServiceId }); - } - - /** */ - render() { - const { - label, status, t, - } = this.props; - if (status !== 'ok') return <></>; - return ( - <Fab color="primary" variant="extended" onClick={this.handleLogout}> - {label || t('logout')} - </Fab> - ); - } -} - -AuthenticationLogout.propTypes = { - authServiceId: PropTypes.string, - depWindow: PropTypes.object, // eslint-disable-line react/forbid-prop-types - label: PropTypes.string, - logoutServiceId: PropTypes.string, - resetAuthenticationState: PropTypes.func.isRequired, - status: PropTypes.string, - t: PropTypes.func, -}; - -AuthenticationLogout.defaultProps = { - authServiceId: undefined, - depWindow: undefined, - label: undefined, - logoutServiceId: undefined, - status: undefined, - t: () => {}, -}; diff --git a/src/components/AuthenticationSender.js b/src/components/AuthenticationSender.js deleted file mode 100644 index e277838069e5f7a88ed2d34f0609a4edec046962..0000000000000000000000000000000000000000 --- a/src/components/AuthenticationSender.js +++ /dev/null @@ -1,40 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { NewWindow } from './NewWindow'; - -/** - * Opens a new window for click - */ -export class AuthenticationSender extends Component { - /** */ - constructor(props) { - super(props); - - this.onClose = this.onClose.bind(this); - } - - /** @private */ - onClose() { - const { handleInteraction, url } = this.props; - - handleInteraction(url); - } - - /** */ - render() { - const { url } = this.props; - - if (!url) return <></>; - - return <NewWindow name="IiifAuthenticationSender" url={`${url}?origin=${window.origin}`} features="centerscreen" onClose={this.onClose} />; - } -} - -AuthenticationSender.propTypes = { - handleInteraction: PropTypes.func.isRequired, - url: PropTypes.string, -}; - -AuthenticationSender.defaultProps = { - url: undefined, -}; diff --git a/src/components/IIIFAuthentication.js b/src/components/IIIFAuthentication.js new file mode 100644 index 0000000000000000000000000000000000000000..098b6ca4a32a179879258e4968d1679df80a7633 --- /dev/null +++ b/src/components/IIIFAuthentication.js @@ -0,0 +1,190 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { AccessTokenSender } from './AccessTokenSender'; +import { NewWindow } from './NewWindow'; +import WindowAuthenticationBar from '../containers/WindowAuthenticationBar'; + +/** + * Opens a new window for click + */ +export class IIIFAuthentication extends Component { + /** */ + constructor(props) { + super(props); + + this.performLogout = this.performLogout.bind(this); + this.onReceiveAccessTokenMessage = this.onReceiveAccessTokenMessage.bind(this); + } + + /** */ + onReceiveAccessTokenMessage(payload) { + const { + authServiceId, accessTokenServiceId, resolveAccessTokenRequest, + } = this.props; + + resolveAccessTokenRequest(authServiceId, accessTokenServiceId, payload); + } + + /** */ + defaultAuthBarProps() { + const { + authServiceId, windowId, status, logoutServiceId, + } = this.props; + + return { + authServiceId, + hasLogoutService: !!logoutServiceId, + status, + windowId, + }; + } + + /** handle the IIIF logout workflow */ + performLogout() { + const { + accessTokenServiceId, authServiceId, features, + logoutServiceId, resetAuthenticationState, openWindow, + } = this.props; + openWindow(logoutServiceId, features); + + resetAuthenticationState({ authServiceId, tokenServiceId: accessTokenServiceId }); + } + + /** Render the auth bar for logged in users */ + renderLoggedIn() { + const { + isInteractive, t, + } = this.props; + + if (!isInteractive) return null; + + return ( + <WindowAuthenticationBar + confirmButton={t('logout')} + onConfirm={this.performLogout} + {...this.defaultAuthBarProps()} + /> + ); + } + + /** Render whatever shows up after the interactive login attempt fails */ + renderFailure() { + const { + handleAuthInteraction, failureHeader: header, failureDescription: description, t, + authServiceId, windowId, + } = this.props; + + return ( + <WindowAuthenticationBar + header={header} + description={description} + confirmButton={t('retry')} + onConfirm={() => handleAuthInteraction(windowId, authServiceId)} + {...this.defaultAuthBarProps()} + /> + ); + } + + /** Render the login bar after we're logging in */ + renderLoggingInCookie() { + const { + accessTokenServiceId, authServiceId, resolveAuthenticationRequest, features, + } = this.props; + + return ( + <> + {this.renderLogin()} + <NewWindow name="IiifLoginSender" url={`${authServiceId}?origin=${window.origin}`} features={features} onClose={() => resolveAuthenticationRequest(authServiceId, accessTokenServiceId)} /> + </> + ); + } + + /** Render the login bar after we're logging in */ + renderLoggingInToken() { + const { + accessTokenServiceId, + } = this.props; + + return ( + <> + {this.renderLogin()} + <AccessTokenSender + handleAccessTokenMessage={this.onReceiveAccessTokenMessage} + url={accessTokenServiceId} + /> + </> + ); + } + + /** Render a login bar */ + renderLogin() { + const { + confirm, description, handleAuthInteraction, header, isInteractive, label, + authServiceId, windowId, + } = this.props; + if (!isInteractive) return null; + + return ( + <WindowAuthenticationBar + header={header} + description={description} + label={label} + confirmButton={confirm} + onConfirm={() => handleAuthInteraction(windowId, authServiceId)} + {...this.defaultAuthBarProps()} + /> + ); + } + + /** */ + render() { + const { authServiceId, status } = this.props; + + if (!authServiceId) return null; + + if (status === null) return this.renderLogin(); + if (status === 'cookie') return this.renderLoggingInCookie(); + if (status === 'token') return this.renderLoggingInToken(); + if (status === 'failed') return this.renderFailure(); + if (status === 'ok') return this.renderLoggedIn(); + + return null; + } +} + +IIIFAuthentication.propTypes = { + accessTokenServiceId: PropTypes.string.isRequired, + authServiceId: PropTypes.string.isRequired, + confirm: PropTypes.string, + description: PropTypes.string, + failureDescription: PropTypes.string, + failureHeader: PropTypes.string, + features: PropTypes.string, + handleAuthInteraction: PropTypes.func.isRequired, + header: PropTypes.string, + isInteractive: PropTypes.bool, + label: PropTypes.string, + logoutServiceId: PropTypes.string, + openWindow: PropTypes.func, + resetAuthenticationState: PropTypes.func.isRequired, + resolveAccessTokenRequest: PropTypes.func.isRequired, + resolveAuthenticationRequest: PropTypes.func.isRequired, + status: PropTypes.oneOf(['logout', 'ok', 'token', 'cookie', 'failed', null]), + t: PropTypes.func, + windowId: PropTypes.string.isRequired, +}; + +IIIFAuthentication.defaultProps = { + confirm: undefined, + description: undefined, + failureDescription: undefined, + failureHeader: undefined, + features: 'centerscreen', + header: undefined, + isInteractive: true, + label: undefined, + logoutServiceId: undefined, + openWindow: window.open, + status: null, + t: k => k, +}; diff --git a/src/components/Window.js b/src/components/Window.js index 62466dd06d74ee869da7f31cbf7d73acd6e39f50..56d3bd45e6c893b89638f30836c3aaccf1c771ed 100644 --- a/src/components/Window.js +++ b/src/components/Window.js @@ -9,7 +9,7 @@ import PrimaryWindow from '../containers/PrimaryWindow'; import CompanionArea from '../containers/CompanionArea'; import MinimalWindow from '../containers/MinimalWindow'; import ErrorContent from '../containers/ErrorContent'; -import WindowAuthenticationControl from '../containers/WindowAuthenticationControl'; +import IIIFAuthentication from '../containers/IIIFAuthentication'; import { PluginHook } from './PluginHook'; /** @@ -44,7 +44,7 @@ export class Window extends Component { windowId={windowId} windowDraggable={windowDraggable} /> - <WindowAuthenticationControl key="auth" windowId={windowId} /> + <IIIFAuthentication windowId={windowId} /> </div> ); if (workspaceType === 'mosaic' && windowDraggable) { diff --git a/src/components/WindowAuthenticationBar.js b/src/components/WindowAuthenticationBar.js new file mode 100644 index 0000000000000000000000000000000000000000..c0765eacf06705cbc911c718bd24be80f866a3e9 --- /dev/null +++ b/src/components/WindowAuthenticationBar.js @@ -0,0 +1,133 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Button from '@material-ui/core/Button'; +import Paper from '@material-ui/core/Paper'; +import Collapse from '@material-ui/core/Collapse'; +import DialogActions from '@material-ui/core/DialogActions'; +import Typography from '@material-ui/core/Typography'; +import LockIcon from '@material-ui/icons/LockSharp'; +import SanitizedHtml from '../containers/SanitizedHtml'; +import { PluginHook } from './PluginHook'; + +/** */ +export class WindowAuthenticationBar extends Component { + /** */ + constructor(props) { + super(props); + + this.state = { open: false }; + this.setOpen = this.setOpen.bind(this); + this.onSubmit = this.onSubmit.bind(this); + } + + /** */ + onSubmit() { + const { onConfirm } = this.props; + this.setOpen(false); + onConfirm(); + } + + /** Toggle the full description */ + setOpen(open) { + this.setState(state => ({ ...state, open })); + } + + /** */ + render() { + const { + classes, confirmButton, continueLabel, + header, description, icon, label, t, + ruleSet, hasLogoutService, status, ConfirmProps, + } = this.props; + + if (status === 'ok' && !hasLogoutService) return null; + + const { open } = this.state; + + const button = ( + <Button onClick={this.onSubmit} className={classes.buttonInvert} autoFocus color="secondary" {...ConfirmProps}> + {confirmButton || t('login')} + </Button> + ); + + if (!description && !header) { + return ( + <Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}> + <div className={classes.topBar}> + { icon || <LockIcon className={classes.icon} /> } + <Typography className={classes.label} component="h3" variant="body1" color="inherit"> + <SanitizedHtml htmlString={label} ruleSet={ruleSet} /> + </Typography> + <PluginHook {...this.props} /> + { button } + </div> + </Paper> + ); + } + + return ( + <Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}> + <Button fullWidth className={classes.topBar} onClick={() => this.setOpen(true)} component="div" color="inherit"> + { icon || <LockIcon className={classes.icon} /> } + <Typography className={classes.label} component="h3" variant="body1" color="inherit"> + <SanitizedHtml htmlString={label} ruleSet="iiif" /> + </Typography> + <PluginHook {...this.props} /> + <span className={classes.fauxButton}> + { !open && ( + <Typography variant="button" color="inherit"> + { continueLabel || t('continue') } + </Typography> + )} + </span> + </Button> + <Collapse + in={open} + onClose={() => this.setOpen(false)} + > + <Typography variant="body1" color="inherit" className={classes.expanded}> + <SanitizedHtml htmlString={header} ruleSet={ruleSet} /> + { header && description ? ': ' : '' } + <SanitizedHtml htmlString={description} ruleSet={ruleSet} /> + </Typography> + <DialogActions> + <Button onClick={() => this.setOpen(false)} color="inherit"> + { t('cancel') } + </Button> + + { button } + </DialogActions> + </Collapse> + </Paper> + ); + } +} + +WindowAuthenticationBar.propTypes = { + classes: PropTypes.objectOf(PropTypes.string).isRequired, + confirmButton: PropTypes.string, + ConfirmProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types + continueLabel: PropTypes.string, + description: PropTypes.node, + hasLogoutService: PropTypes.bool, + header: PropTypes.node, + icon: PropTypes.node, + label: PropTypes.node.isRequired, + onConfirm: PropTypes.func.isRequired, + ruleSet: PropTypes.string, + status: PropTypes.string, + t: PropTypes.func, +}; + +WindowAuthenticationBar.defaultProps = { + confirmButton: undefined, + ConfirmProps: {}, + continueLabel: undefined, + description: undefined, + hasLogoutService: true, + header: undefined, + icon: undefined, + ruleSet: 'iiif', + status: undefined, + t: k => k, +}; diff --git a/src/components/WindowAuthenticationControl.js b/src/components/WindowAuthenticationControl.js deleted file mode 100644 index 32b3cbef3856c4fffdf3adcb6af8c9cb050c6d5b..0000000000000000000000000000000000000000 --- a/src/components/WindowAuthenticationControl.js +++ /dev/null @@ -1,173 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import Paper from '@material-ui/core/Paper'; -import Collapse from '@material-ui/core/Collapse'; -import DialogActions from '@material-ui/core/DialogActions'; -import Typography from '@material-ui/core/Typography'; -import LockIcon from '@material-ui/icons/LockSharp'; -import SanitizedHtml from '../containers/SanitizedHtml'; -import AuthenticationLogout from '../containers/AuthenticationLogout'; - -/** - */ -export class WindowAuthenticationControl extends Component { - /** */ - constructor(props) { - super(props); - - this.state = { - open: false, - showFailureMessage: true, - }; - - this.handleClickOpen = this.handleClickOpen.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleConfirm = this.handleConfirm.bind(this); - } - - /** */ - handleClickOpen() { - this.setState({ open: true }); - } - - /** */ - handleClose() { - this.setState({ open: false, showFailureMessage: false }); - } - - /** */ - handleConfirm() { - const { - handleAuthInteraction, infoId, serviceId, windowId, - } = this.props; - handleAuthInteraction(windowId, infoId, serviceId); - this.setState({ showFailureMessage: true }); - } - - /** */ - isInteractive() { - const { - profile, - } = this.props; - - return profile === 'http://iiif.io/api/auth/1/clickthrough' || profile === 'http://iiif.io/api/auth/1/login'; - } - - /** */ - render() { - const { - classes, - confirmLabel, - degraded, - description, - failureDescription, - failureHeader, - header, - label, - profile, - status, - t, - windowId, - } = this.props; - - const failed = status === 'failed'; - if ((!degraded || !profile) && status !== 'fetching') return <AuthenticationLogout windowId={windowId} />; - if (!this.isInteractive() && !failed) return <></>; - - const { showFailureMessage, open } = this.state; - - const isInFailureState = showFailureMessage && failed; - - const hasCollapsedContent = isInFailureState - ? failureDescription - : header || description; - - const confirmButton = ( - <Button onClick={this.handleConfirm} className={classes.buttonInvert} autoFocus color="secondary"> - {confirmLabel || (this.isInteractive() ? t('login') : t('retry')) } - </Button> - ); - - return ( - <Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}> - <Button fullWidth className={classes.topBar} onClick={hasCollapsedContent ? this.handleClickOpen : this.handleConfirm} component="div" color="inherit"> - <LockIcon className={classes.icon} /> - <Typography className={classes.label} component="h3" variant="body1" color="inherit"> - <SanitizedHtml htmlString={(isInFailureState ? failureHeader : label) || t('authenticationRequired')} ruleSet="iiif" /> - </Typography> - <span className={classes.fauxButton}> - { hasCollapsedContent - ? !open && ( - <Typography variant="button" color="inherit"> - { t('continue') } - </Typography> - ) - : confirmButton} - </span> - </Button> - { - hasCollapsedContent && ( - <Collapse - in={open} - onClose={this.handleClose} - > - <Typography variant="body1" color="inherit" className={classes.expanded}> - { - isInFailureState - ? <SanitizedHtml htmlString={failureDescription || ''} ruleSet="iiif" /> - : ( - <> - <SanitizedHtml htmlString={header || ''} ruleSet="iiif" /> - { header && description ? ': ' : '' } - <SanitizedHtml htmlString={description || ''} ruleSet="iiif" /> - </> - ) - } - </Typography> - <DialogActions> - <Button onClick={this.handleClose} color="inherit"> - {t('cancel')} - </Button> - {confirmButton} - </DialogActions> - </Collapse> - ) - } - </Paper> - ); - } -} - -WindowAuthenticationControl.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, - confirmLabel: PropTypes.string, - degraded: PropTypes.bool, - description: PropTypes.string, - failureDescription: PropTypes.string, - failureHeader: PropTypes.string, - handleAuthInteraction: PropTypes.func.isRequired, - header: PropTypes.string, - infoId: PropTypes.string, - label: PropTypes.string, - profile: PropTypes.string, - serviceId: PropTypes.string, - status: PropTypes.oneOf(['ok', 'fetching', 'failed', null]), - t: PropTypes.func, - windowId: PropTypes.string.isRequired, -}; - -WindowAuthenticationControl.defaultProps = { - confirmLabel: undefined, - degraded: false, - description: undefined, - failureDescription: undefined, - failureHeader: undefined, - header: undefined, - infoId: undefined, - label: undefined, - profile: undefined, - serviceId: undefined, - status: null, - t: () => {}, -}; diff --git a/src/containers/AccessTokenSender.js b/src/containers/AccessTokenSender.js deleted file mode 100644 index eb8b51fea3aa7441fd6b2b04c43b33eec34a8b93..0000000000000000000000000000000000000000 --- a/src/containers/AccessTokenSender.js +++ /dev/null @@ -1,31 +0,0 @@ -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import { withPlugins } from '../extend/withPlugins'; -import * as actions from '../state/actions'; -import { getAccessTokens } from '../state/selectors'; -import { AccessTokenSender } from '../components/AccessTokenSender'; - -/** - * mapStateToProps - to hook up connect - * @memberof App - * @private - */ -const mapStateToProps = (state) => ({ - url: (Object.values(getAccessTokens(state)).find(e => e.isFetching) || {}).id, -}); - -/** - * mapDispatchToProps - used to hook up connect to action creators - * @memberof App - * @private - */ -const mapDispatchToProps = { - handleAccessTokenMessage: actions.resolveAccessTokenRequest, -}; - -const enhance = compose( - connect(mapStateToProps, mapDispatchToProps), - withPlugins('AccessTokenSender'), -); - -export default enhance(AccessTokenSender); diff --git a/src/containers/AuthenticationLogout.js b/src/containers/AuthenticationLogout.js deleted file mode 100644 index 1178992d1a2e45e993bbded92f091b62211cdb14..0000000000000000000000000000000000000000 --- a/src/containers/AuthenticationLogout.js +++ /dev/null @@ -1,46 +0,0 @@ -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; -import { withPlugins } from '../extend/withPlugins'; - -import { - getCurrentCanvas, - selectAuthStatus, - selectCanvasAuthService, - selectLogoutAuthService, -} from '../state/selectors'; -import * as actions from '../state/actions'; -import { AuthenticationLogout } from '../components/AuthenticationLogout'; - -/** - * mapStateToProps - to hook up connect - * @memberof App - * @private - */ -const mapStateToProps = (state, { windowId }) => { - const canvasId = (getCurrentCanvas(state, { windowId }) || {}).id; - const service = selectCanvasAuthService(state, { canvasId, windowId }); - const logoutService = selectLogoutAuthService(state, { canvasId, windowId }); - return { - authServiceId: service && service.id, - label: logoutService && logoutService.getLabel()[0].value, - logoutServiceId: logoutService && logoutService.id, - status: service && selectAuthStatus(state, service), - }; -}; - -const mapDispatchToProps = { - resetAuthenticationState: actions.resetAuthenticationState, -}; - -const styles = {}; - -const enhance = compose( - connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), - withTranslation(), - withPlugins('AuthenticationLogout'), -); - -export default enhance(AuthenticationLogout); diff --git a/src/containers/AuthenticationSender.js b/src/containers/AuthenticationSender.js deleted file mode 100644 index 97ab2847d775bc1e5467ac1f47f91ff88c4b12d7..0000000000000000000000000000000000000000 --- a/src/containers/AuthenticationSender.js +++ /dev/null @@ -1,32 +0,0 @@ -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import { withPlugins } from '../extend/withPlugins'; -import * as actions from '../state/actions'; -import { getAuth, getConfig } from '../state/selectors'; -import { AuthenticationSender } from '../components/AuthenticationSender'; - -/** - * mapStateToProps - to hook up connect - * @memberof App - * @private - */ -const mapStateToProps = (state) => ({ - center: getConfig(state).window.authNewWindowCenter, - url: (Object.values(getAuth(state)).find(e => e.isFetching && e.profile !== 'http://iiif.io/api/auth/1/external') || {}).id, -}); - -/** - * mapDispatchToProps - used to hook up connect to action creators - * @memberof App - * @private - */ -const mapDispatchToProps = { - handleInteraction: actions.resolveAuthenticationRequest, -}; - -const enhance = compose( - connect(mapStateToProps, mapDispatchToProps), - withPlugins('AuthenticationSender'), -); - -export default enhance(AuthenticationSender); diff --git a/src/containers/IIIFAuthentication.js b/src/containers/IIIFAuthentication.js new file mode 100644 index 0000000000000000000000000000000000000000..ba2eb5aed5724352d2a4b77cfcabe21633325b31 --- /dev/null +++ b/src/containers/IIIFAuthentication.js @@ -0,0 +1,94 @@ +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { withTranslation } from 'react-i18next'; +import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { withPlugins } from '../extend/withPlugins'; +import * as actions from '../state/actions'; +import { + getCurrentCanvas, + getAuth, + selectCanvasAuthService, + getAccessTokens, +} from '../state/selectors'; +import { IIIFAuthentication } from '../components/IIIFAuthentication'; + +/** + * mapStateToProps - to hook up connect + * @memberof FullScreenButton + * @private + */ +const mapStateToProps = (state, { windowId }) => { + const canvasId = (getCurrentCanvas(state, { windowId }) || {}).id; + const service = selectCanvasAuthService(state, { canvasId, windowId }); + + const accessTokenService = service && ( + Utils.getService(service, 'http://iiif.io/api/auth/1/token') + ); + const logoutService = service && ( + Utils.getService(service, 'http://iiif.io/api/auth/1/logout') + ); + + const authStatuses = getAuth(state) || {}; + const authStatus = service && authStatuses[service.id]; + const accessTokens = authStatus && accessTokenService && getAccessTokens(state); + const accessTokenStatus = accessTokens && Object.values(accessTokens).find( + e => e.id === accessTokenService.id, + ); + + const profile = service && service.getProfile(); + + let status; + + if (!authStatus) { + status = null; + } else if (authStatus.ok) { + status = 'ok'; + } else if (authStatus.isFetching) { + if (authStatus.windowId === windowId) { + status = 'cookie'; + } + } else if (accessTokenStatus && accessTokenStatus.isFetching) { + if (authStatus.windowId === windowId) { + status = 'token'; + } + } else { + status = 'failed'; + } + + const isInteractive = profile !== 'http://iiif.io/api/auth/1/external' && profile !== 'http://iiif.io/api/auth/1/kiosk'; + + return { + accessTokenServiceId: accessTokenService && accessTokenService.id, + authServiceId: service && service.id, + confirm: service && service.getConfirmLabel(), + description: service && service.getDescription(), + failureDescription: service && service.getFailureDescription(), + failureHeader: service && service.getFailureHeader(), + header: service && service.getHeader(), + isInteractive, + label: service && service.getLabel()[0].value, + logoutServiceId: logoutService && logoutService.id, + profile, + status, + }; +}; + +/** + * mapDispatchToProps - used to hook up connect to action creators + * @memberof ManifestListItem + * @private + */ +const mapDispatchToProps = { + handleAuthInteraction: actions.addAuthenticationRequest, + resetAuthenticationState: actions.resetAuthenticationState, + resolveAccessTokenRequest: actions.resolveAccessTokenRequest, + resolveAuthenticationRequest: actions.resolveAuthenticationRequest, +}; + +const enhance = compose( + withTranslation(), + connect(mapStateToProps, mapDispatchToProps), + withPlugins('IIIFAuthentication'), +); + +export default enhance(IIIFAuthentication); diff --git a/src/containers/WindowAuthenticationBar.js b/src/containers/WindowAuthenticationBar.js new file mode 100644 index 0000000000000000000000000000000000000000..881bf419a1423289b504995b616cb96270366050 --- /dev/null +++ b/src/containers/WindowAuthenticationBar.js @@ -0,0 +1,65 @@ +import { compose } from 'redux'; +import { withTranslation } from 'react-i18next'; +import { withStyles } from '@material-ui/core/styles'; +import { fade } from '@material-ui/core/styles/colorManipulator'; +import { withPlugins } from '../extend/withPlugins'; +import { WindowAuthenticationBar } from '../components/WindowAuthenticationBar'; + +/** + * @param theme + * @returns {{typographyBody: {flexGrow: number, fontSize: number|string}, + * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}} + */ +const styles = theme => ({ + buttonInvert: { + '&:hover': { + backgroundColor: fade( + theme.palette.secondary.contrastText, 1 - theme.palette.action.hoverOpacity, + ), + }, + backgroundColor: theme.palette.secondary.contrastText, + marginLeft: theme.spacing(5), + paddingBottom: 0, + paddingTop: 0, + }, + expanded: { + paddingLeft: theme.spacing(), + paddingRight: theme.spacing(), + }, + failure: { + backgroundColor: theme.palette.error.dark, + }, + fauxButton: { + marginLeft: theme.spacing(2.5), + }, + icon: { + marginRight: theme.spacing(1.5), + verticalAlign: 'text-bottom', + }, + label: { + lineHeight: 2.25, + }, + paper: { + backgroundColor: theme.palette.secondary.main, + color: theme.palette.secondary.contrastText, + cursor: 'pointer', + }, + topBar: { + '&:hover': { + backgroundColor: theme.palette.secondary.main, + }, + alignItems: 'center', + display: 'flex', + justifyContent: 'inherit', + padding: theme.spacing(1), + textTransform: 'none', + }, +}); + +const enhance = compose( + withTranslation(), + withStyles(styles), + withPlugins('WindowAuthenticationBar'), +); + +export default enhance(WindowAuthenticationBar); diff --git a/src/containers/WindowAuthenticationControl.js b/src/containers/WindowAuthenticationControl.js deleted file mode 100644 index 97b8978ae201e4bc7b5ee87a19251741cc4009b3..0000000000000000000000000000000000000000 --- a/src/containers/WindowAuthenticationControl.js +++ /dev/null @@ -1,100 +0,0 @@ -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; -import { fade } from '@material-ui/core/styles/colorManipulator'; -import { withPlugins } from '../extend/withPlugins'; -import * as actions from '../state/actions'; -import { - getCurrentCanvas, - selectAuthStatus, - selectInfoResponse, - selectCanvasAuthService, -} from '../state/selectors'; -import { WindowAuthenticationControl } from '../components/WindowAuthenticationControl'; - -/** - * mapStateToProps - to hook up connect - * @memberof App - * @private - */ -const mapStateToProps = (state, { windowId }) => { - const canvasId = (getCurrentCanvas(state, { windowId }) || {}).id; - const service = selectCanvasAuthService(state, { canvasId, windowId }); - const infoResponse = selectInfoResponse(state, { canvasId, windowId }) || {}; - - return { - confirmLabel: service && service.getConfirmLabel(), - degraded: infoResponse.degraded, - description: service && service.getDescription(), - failureDescription: service && service.getFailureDescription(), - failureHeader: service && service.getFailureHeader(), - header: service && service.getHeader(), - infoId: infoResponse.id, - label: service && service.getLabel()[0].value, - profile: service && service.getProfile(), - serviceId: service && service.id, - status: service && selectAuthStatus(state, service), - }; -}; - -/** - * mapDispatchToProps - used to hook up connect to action creators - * @memberof App - * @private - */ -const mapDispatchToProps = { - handleAuthInteraction: actions.addAuthenticationRequest, -}; -/** - * @param theme - * @returns {{typographyBody: {flexGrow: number, fontSize: number|string}, - * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}} - */ -const styles = theme => ({ - buttonInvert: { - '&:hover': { - backgroundColor: fade( - theme.palette.secondary.contrastText, 1 - theme.palette.action.hoverOpacity, - ), - }, - backgroundColor: theme.palette.secondary.contrastText, - }, - expanded: { - paddingLeft: theme.spacing(), - paddingRight: theme.spacing(), - }, - failure: { - backgroundColor: theme.palette.error.dark, - }, - fauxButton: { - marginLeft: theme.spacing(2.5), - }, - icon: { - marginRight: theme.spacing(1.5), - verticalAlign: 'text-bottom', - }, - label: { - lineHeight: 2.25, - }, - paper: { - backgroundColor: theme.palette.secondary.main, - color: theme.palette.secondary.contrastText, - cursor: 'pointer', - }, - topBar: { - '&:hover': { - backgroundColor: theme.palette.secondary.main, - }, - justifyContent: 'inherit', - textTransform: 'none', - }, -}); -const enhance = compose( - connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), - withTranslation(), - withPlugins('WindowAuthenticationControl'), -); - -export default enhance(WindowAuthenticationControl);