diff --git a/identifier/package.json b/identifier/package.json index 7400e18a..35c9918d 100644 --- a/identifier/package.json +++ b/identifier/package.json @@ -10,8 +10,9 @@ "axios": "^0.22.0", "classnames": "^2.3.2", "glob": "^8.1.0", + "history": "^5.3.0", "i18next": "^21.10.0", - "i18next-browser-languagedetector": "^6.1.8", + "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^1.4.5", "i18next-resources-to-backend": "^1.0.0", "query-string": "^7.1.3", @@ -47,6 +48,7 @@ "@types/react": "^17.0.70", "@types/react-dom": "^17.0.23", "@types/react-redux": "^7.1.25", + "@types/react-router-dom": "^5.3.3", "@types/redux-logger": "^3.0.12", "@types/validator": "^13", "@typescript-eslint/eslint-plugin": "^6.11.0", diff --git a/identifier/src/App.jsx b/identifier/src/App.tsx similarity index 78% rename from identifier/src/App.jsx rename to identifier/src/App.tsx index bf97168f..e89306f6 100644 --- a/identifier/src/App.jsx +++ b/identifier/src/App.tsx @@ -3,7 +3,7 @@ import React, { Suspense, lazy } from 'react'; import { MuiThemeProvider } from '@material-ui/core/styles'; import { CssBaseline, - } from '@material-ui/core'; +} from '@material-ui/core'; import './App.css'; import './fancy-background.css'; @@ -11,19 +11,20 @@ import Spinner from './components/Spinner'; import * as version from './version'; import theme from './theme'; -const LazyMain = lazy(() => import(/* webpackChunkName: "identifier-main" */ './Main')); +const LazyMain = lazy(() => import(/* webpackChunkName: "identifier-main" */ './Main') as Promise<{ default: React.ComponentType }>); console.info(`Kopano Identifier build version: ${version.build}`); // eslint-disable-line no-console + const App = () => { return ( - - }> + + }> - ); + ) } export default App; diff --git a/identifier/src/Main.jsx b/identifier/src/Main.jsx deleted file mode 100644 index 22bbf9a6..00000000 --- a/identifier/src/Main.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { BrowserRouter } from 'react-router-dom'; - -import { withStyles } from '@material-ui/core/styles'; - -import Routes from './Routes'; - -const styles = () => ({ - root: { - position: 'relative', - display: 'flex', - flex: 1 - } -}); - -class Main extends PureComponent { - render() { - const { classes, hello, pathPrefix } = this.props; - - return ( -
- - - -
- ); - } - - reload(event) { - event.preventDefault(); - - window.location.reload(); - } -} - -Main.propTypes = { - classes: PropTypes.object.isRequired, - - hello: PropTypes.object, - updateAvailable: PropTypes.bool.isRequired, - pathPrefix: PropTypes.string.isRequired -}; - -const mapStateToProps = (state) => { - const { hello, updateAvailable, pathPrefix } = state.common; - - return { - hello, - updateAvailable, - pathPrefix - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(Main)); diff --git a/identifier/src/Main.test.jsx b/identifier/src/Main.test.tsx similarity index 100% rename from identifier/src/Main.test.jsx rename to identifier/src/Main.test.tsx diff --git a/identifier/src/Main.tsx b/identifier/src/Main.tsx new file mode 100644 index 00000000..d9d31d6b --- /dev/null +++ b/identifier/src/Main.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { BrowserRouter } from 'react-router-dom'; + +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; + +import Routes from './Routes'; +import { RootState } from './store'; +import { ObjectType } from './types'; + +const styles = () => createStyles({ + root: { + position: 'relative', + display: 'flex', + flex: 1 + } +}); + +export interface MainProps extends WithStyles { + hello: ObjectType; + pathPrefix: string; + updateAvailable: boolean; +} + + +const Main: React.FC = ({ classes, hello, pathPrefix }) => { + + return ( +
+ + + +
+ ); +} + +const mapStateToProps = (state: RootState) => { + const { hello, updateAvailable, pathPrefix } = state.common; + + return { + hello: hello as ObjectType, + updateAvailable, + pathPrefix + }; +}; + +export default connect(mapStateToProps)(withStyles(styles)(Main)); diff --git a/identifier/src/Routes.jsx b/identifier/src/Routes.tsx similarity index 71% rename from identifier/src/Routes.jsx rename to identifier/src/Routes.tsx index 02abf056..a84de118 100644 --- a/identifier/src/Routes.jsx +++ b/identifier/src/Routes.tsx @@ -1,18 +1,25 @@ import React, { lazy } from 'react'; -import PropTypes from 'prop-types'; import { Route, Switch } from 'react-router-dom'; import PrivateRoute from './components/PrivateRoute'; +import { ObjectType } from './types'; + +type RootType = { + hello: ObjectType; +} + + +type ComponentType = Promise<{ default: React.ComponentType }> const AsyncLogin = lazy(() => - import(/* webpackChunkName: "containers-login" */ './containers/Login')); + import(/* webpackChunkName: "containers-login" */ './containers/Login') as ComponentType); const AsyncWelcome = lazy(() => - import(/* webpackChunkName: "containers-welcome" */ './containers/Welcome')); + import(/* webpackChunkName: "containers-welcome" */ './containers/Welcome') as ComponentType); const AsyncGoodbye = lazy(() => - import(/* webpackChunkName: "containers-goodbye" */ './containers/Goodbye')); + import(/* webpackChunkName: "containers-goodbye" */ './containers/Goodbye') as ComponentType); -const Routes = ({ hello }) => ( +const Routes = ({ hello }: RootType) => ( ( ); -Routes.propTypes = { - hello: PropTypes.object -}; - export default Routes; diff --git a/identifier/src/actions/common.js b/identifier/src/actions/common.ts similarity index 74% rename from identifier/src/actions/common.js rename to identifier/src/actions/common.ts index 49e6beb8..0939299a 100644 --- a/identifier/src/actions/common.js +++ b/identifier/src/actions/common.ts @@ -1,6 +1,6 @@ -import axios from 'axios'; +import axios, { AxiosError, AxiosResponse } from 'axios'; -import { newHelloRequest } from '../models/hello'; +import { HelloQuery, newHelloRequest } from '../models/hello'; import { withClientRequestState } from '../utils'; import { ExtendedError, @@ -10,8 +10,11 @@ import { import { handleAxiosError } from './utils'; import * as types from './types'; +import { Dispatch } from 'redux'; +import { AppDispatch, PromiseDispatch, RootState } from '../store'; +import { ResponseType } from '../types'; -export function receiveError(error) { +export function receiveError(error?: ExtendedError | AxiosError | null) { return { type: types.RECEIVE_ERROR, error @@ -24,7 +27,7 @@ export function resetHello() { }; } -export function receiveHello(hello) { +export function receiveHello(hello: {success?: boolean, username?: string, displayName?: string}) { const { success, username, displayName } = hello; return { @@ -37,12 +40,12 @@ export function receiveHello(hello) { } export function executeHello() { - return function(dispatch, getState) { + return function(dispatch:Dispatch , getState: () => RootState) { dispatch(resetHello()); const { flow, query } = getState().common; - const r = withClientRequestState(newHelloRequest(flow, query)); + const r = withClientRequestState(newHelloRequest(flow as string, query as HelloQuery)); return axios.post('./identifier/_/hello', r, { headers: { 'Kopano-Konnect-XSRF': '1' @@ -60,11 +63,11 @@ export function executeHello() { }; default: // error. - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response as AxiosResponse); } }).then(response => { if (response.state !== r.state) { - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response as ResponseType); } dispatch(receiveHello(response)); @@ -78,7 +81,7 @@ export function executeHello() { } export function retryHello() { - return function(dispatch) { + return function(dispatch: PromiseDispatch) { dispatch(receiveError(null)); return dispatch(executeHello()); @@ -91,7 +94,7 @@ export function requestLogoff() { }; } -export function receiveLogoff(state) { +export function receiveLogoff(state: boolean) { return { type: types.RECEIVE_LOGOFF, state @@ -99,7 +102,7 @@ export function receiveLogoff(state) { } export function executeLogoff() { - return function(dispatch) { + return function(dispatch: AppDispatch) { dispatch(resetHello()); dispatch(requestLogoff()); @@ -115,11 +118,11 @@ export function executeLogoff() { return response.data; default: // error. - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response as AxiosResponse); } }).then(response => { if (response.state !== r.state) { - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response as ResponseType); } dispatch(receiveLogoff(response.success === true)); diff --git a/identifier/src/actions/login.js b/identifier/src/actions/login.ts similarity index 71% rename from identifier/src/actions/login.js rename to identifier/src/actions/login.ts index b57b1aaa..bc869954 100644 --- a/identifier/src/actions/login.js +++ b/identifier/src/actions/login.ts @@ -1,7 +1,7 @@ -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; import queryString from 'query-string'; -import { newHelloRequest } from '../models/hello'; +import { HelloQuery, newHelloRequest } from '../models/hello'; import { withClientRequestState } from '../utils'; import { ExtendedError, @@ -15,12 +15,15 @@ import { import * as types from './types'; import { receiveHello } from './common'; import { handleAxiosError } from './utils'; +import { AppDispatch, PromiseDispatch, RootState } from '../store'; +import { History } from "history"; +import { ModelResponseType, ObjectType, ResponseType, StringObject } from '../types'; // Modes for logon. export const ModeLogonUsernameEmptyPasswordCookie = '0'; export const ModeLogonUsernamePassword = '1'; -export function updateInput(name, value) { +export function updateInput(name: string, value?: string | null) { return { type: types.UPDATE_INPUT, name, @@ -28,14 +31,14 @@ export function updateInput(name, value) { }; } -export function receiveValidateLogon(errors) { +export function receiveValidateLogon(errors: Error | {[key: string]: Error}) { return { type: types.RECEIVE_VALIDATE_LOGON, errors }; } -export function requestLogon(username, password) { +export function requestLogon(username: string, password: string) { return { type: types.REQUEST_LOGON, username, @@ -43,7 +46,7 @@ export function requestLogon(username, password) { }; } -export function receiveLogon(logon) { +export function receiveLogon(logon: {success: boolean, errors: {http: Error}}) { const { success, errors } = logon; return { @@ -59,7 +62,7 @@ export function requestConsent(allow=false) { }; } -export function receiveConsent(logon) { +export function receiveConsent(logon: {success: boolean, errors: {http: Error}}) { const { success, errors } = logon; return { @@ -69,8 +72,8 @@ export function receiveConsent(logon) { }; } -export function executeLogon(username, password, mode=ModeLogonUsernamePassword) { - return function(dispatch, getState) { +export function executeLogon(username: string, password: string, mode=ModeLogonUsernamePassword) { + return function(dispatch: AppDispatch, getState: () => RootState) { dispatch(requestLogon(username, password)); dispatch(receiveHello({ username @@ -96,7 +99,7 @@ export function executeLogon(username, password, mode=ModeLogonUsernamePassword) const r = withClientRequestState({ params: params, - hello: newHelloRequest(flow, query) + hello: newHelloRequest(flow as string, query as HelloQuery) }); return axios.post('./identifier/_/logon', r, { headers: { @@ -118,14 +121,14 @@ export function executeLogon(username, password, mode=ModeLogonUsernamePassword) }; default: // error. - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response as AxiosResponse); } }).then(response => { if (response.state !== r.state) { - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response as ResponseType); } - let { hello } = response; + let { hello } = response as object & { hello: ObjectType}; if (!hello) { hello = { success: response.success, @@ -133,7 +136,7 @@ export function executeLogon(username, password, mode=ModeLogonUsernamePassword) }; } dispatch(receiveHello(hello)); - dispatch(receiveLogon(response)); + dispatch(receiveLogon(response as ModelResponseType)); return Promise.resolve(response); }).catch(error => { error = handleAxiosError(error); @@ -151,7 +154,7 @@ export function executeLogon(username, password, mode=ModeLogonUsernamePassword) } export function executeConsent(allow=false, scope='') { - return function(dispatch, getState) { + return function(dispatch: AppDispatch, getState: () => RootState) { dispatch(requestConsent(allow)); const { query } = getState().common; @@ -181,14 +184,14 @@ export function executeConsent(allow=false, scope='') { }; default: // error. - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response as AxiosResponse); } }).then(response => { if (response.state !== r.state) { - throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response); + throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response as ResponseType); } - dispatch(receiveConsent(response)); + dispatch(receiveConsent(response as ModelResponseType)); return Promise.resolve(response); }).catch(error => { error = handleAxiosError(error); @@ -205,10 +208,10 @@ export function executeConsent(allow=false, scope='') { }; } -export function validateUsernamePassword(username, password, isSignedIn) { - return function(dispatch) { +export function validateUsernamePassword(username: string, password: string, isSignedIn: boolean) { + return function(dispatch: AppDispatch) { return new Promise((resolve, reject) => { - const errors = {}; + const errors:{[key: string]: Error} = {}; if (!username) { errors.username = new Error(ERROR_LOGIN_VALIDATE_MISSINGUSERNAME); @@ -227,14 +230,14 @@ export function validateUsernamePassword(username, password, isSignedIn) { }; } -export function executeLogonIfFormValid(username, password, isSignedIn) { - return (dispatch) => { +export function executeLogonIfFormValid(username: string, password: string, isSignedIn: boolean) { + return (dispatch: PromiseDispatch) => { return dispatch( validateUsernamePassword(username, password, isSignedIn) ).then(() => { const mode = isSignedIn ? ModeLogonUsernameEmptyPasswordCookie : ModeLogonUsernamePassword; return dispatch(executeLogon(username, password, mode)); - }).catch((errors) => { + }).catch((errors: Error | {[key: string]:Error}) => { return { success: false, errors: errors @@ -243,8 +246,9 @@ export function executeLogonIfFormValid(username, password, isSignedIn) { }; } -export function advanceLogonFlow(success, history, done=false, extraQuery={}) { - return (dispatch, getState) => { + +export function advanceLogonFlow(success: boolean, history?: History, done=false, extraQuery={}) { + return (dispatch:AppDispatch, getState: () => RootState) => { if (!success) { return; } @@ -256,18 +260,18 @@ export function advanceLogonFlow(success, history, done=false, extraQuery={}) { case 'oauth': case 'consent': case 'oidc': - if (hello.details.flow !== flow) { + if (((hello?.details as StringObject)?.flow ) !== (flow as string)) { // Ignore requested flow if hello flow does not match. break; } - if (!done && hello.details.next === 'consent') { - history.replace(`/consent${history.location.search}${history.location.hash}`); + if (!done && (hello?.details as StringObject).next === 'consent') { + history?.replace(`/consent${history?.location?.search}${history?.location?.hash}`); return; } - if (hello.details.continue_uri) { + if ((hello?.details as StringObject).continue_uri) { q.prompt = 'none'; - window.location.replace(hello.details.continue_uri + '?' + queryString.stringify(q)); + window.location.replace((hello?.details as StringObject).continue_uri + '?' + queryString.stringify(q)); return; } @@ -276,18 +280,18 @@ export function advanceLogonFlow(success, history, done=false, extraQuery={}) { default: // Legacy stupid modes. if (q.continue && q.continue.indexOf(document.location.origin) === 0) { - window.location.replace(q.continue); + window.location.replace(q.continue as string); return; } } // Default action. let target = '/welcome'; - if (history.action === 'REPLACE') { + if (history?.action === 'REPLACE') { target = target + history.location.search + history.location.hash; } dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors. - history.push(target); + history?.push(target); }; } diff --git a/identifier/src/actions/types.js b/identifier/src/actions/types.ts similarity index 100% rename from identifier/src/actions/types.js rename to identifier/src/actions/types.ts diff --git a/identifier/src/actions/utils.js b/identifier/src/actions/utils.ts similarity index 57% rename from identifier/src/actions/utils.js rename to identifier/src/actions/utils.ts index d91c04a0..9df82e41 100644 --- a/identifier/src/actions/utils.js +++ b/identifier/src/actions/utils.ts @@ -1,14 +1,15 @@ +import { AxiosError } from 'axios'; import { ExtendedError, ERROR_HTTP_NETWORK_ERROR, ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS } from '../errors'; -export function handleAxiosError(error) { - if (error.request) { +export function handleAxiosError(error: AxiosError | ExtendedError) { + if ((error as AxiosError).request) { // Axios errors. - if (error.response) { - error = new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, error.response); + if ((error as AxiosError).response) { + error = new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, (error as AxiosError).response); } else { error = new ExtendedError(ERROR_HTTP_NETWORK_ERROR); } diff --git a/identifier/src/components/ClientDisplayName.jsx b/identifier/src/components/ClientDisplayName.jsx deleted file mode 100644 index e712162c..00000000 --- a/identifier/src/components/ClientDisplayName.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const ClientDisplayName = ({ client, ...rest }) => ( - {client.display_name ? client.display_name : client.id} -); - -ClientDisplayName.propTypes = { - client: PropTypes.object.isRequired -}; - -export default ClientDisplayName; diff --git a/identifier/src/components/ClientDisplayName.tsx b/identifier/src/components/ClientDisplayName.tsx new file mode 100644 index 00000000..8038cce7 --- /dev/null +++ b/identifier/src/components/ClientDisplayName.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export interface ClientDisplayNameProps extends React.HTMLAttributes { + client: { [key: string]: string }, +} + + +const ClientDisplayName: React.FC = ({ client, ...rest }) => ( + {client.display_name ? client.display_name : client.id} +); + +export default ClientDisplayName; diff --git a/identifier/src/components/Loading.jsx b/identifier/src/components/Loading.jsx deleted file mode 100644 index 8412d215..00000000 --- a/identifier/src/components/Loading.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { withTranslation } from 'react-i18next'; - -import { withStyles } from '@material-ui/core/styles'; -import LinearProgress from '@material-ui/core/LinearProgress'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import renderIf from 'render-if'; - -import { retryHello } from '../actions/common'; -import { ErrorMessage } from '../errors'; - -const styles = theme => ({ - root: { - flexGrow: 1, - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0 - }, - progress: { - height: '4px', - width: '100px' - }, - button: { - marginTop: theme.spacing(5) - } -}); - -class Loading extends React.PureComponent { - render() { - const { classes, error, t } = this.props; - - return ( - - - {renderIf(error === null)(() => ( - - ))} - {renderIf(error !== null)(() => ( -
- - {t("konnect.loading.error.headline", "Failed to connect to server")} - - - - - -
- ))} -
-
- ); - } - - retry(event) { - event.preventDefault(); - - this.props.dispatch(retryHello()); - } -} - -Loading.propTypes = { - classes: PropTypes.object.isRequired, - t: PropTypes.func.isRequired, - - error: PropTypes.object, - - dispatch: PropTypes.func.isRequired, -}; - -const mapStateToProps = (state) => { - const { error } = state.common; - - return { - error - }; -}; - -export default connect(mapStateToProps)(withTranslation()(withStyles(styles)(Loading))); diff --git a/identifier/src/components/Loading.tsx b/identifier/src/components/Loading.tsx new file mode 100644 index 00000000..fb4e3fd3 --- /dev/null +++ b/identifier/src/components/Loading.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { withTranslation } from 'react-i18next'; + +import { createStyles, Theme, withStyles } from '@material-ui/core/styles'; +import LinearProgress from '@material-ui/core/LinearProgress'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import renderIf from 'render-if'; + +import { retryHello } from '../actions/common'; +import { ErrorMessage, ErrorType } from '../errors'; +import { PromiseDispatch, RootState } from '../store'; +import { TFunction } from 'i18next'; +import { WithStyles } from '@material-ui/styles'; + +const styles = (theme: Theme) => createStyles({ + root: { + flexGrow: 1, + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0 + }, + progress: { + height: '4px', + width: '100px' + }, + button: { + marginTop: theme.spacing(5) + } +}); + +interface LoadingProps extends WithStyles { + error: ErrorType, t: TFunction, dispatch: PromiseDispatch +} + +const Loading = ({ classes, error, t, dispatch }: LoadingProps): JSX.Element => { + + const retry = (event: React.MouseEvent) => { + event.preventDefault(); + + dispatch(retryHello()); + } + + return ( + + + {renderIf(error === null)(() => ( + + ))} + {renderIf(error !== null)(() => ( +
+ + {t("konnect.loading.error.headline", "Failed to connect to server")} + + + + + +
+ ))} +
+
+ ); +} + +const mapStateToProps = (state: RootState) => { + const { error } = state.common; + + return { + error + }; +}; + +export default connect(mapStateToProps)(withTranslation()(withStyles(styles)(Loading))); diff --git a/identifier/src/components/LocaleSelect.jsx b/identifier/src/components/LocaleSelect.tsx similarity index 66% rename from identifier/src/components/LocaleSelect.jsx rename to identifier/src/components/LocaleSelect.tsx index d8b118f0..947d74c5 100644 --- a/identifier/src/components/LocaleSelect.jsx +++ b/identifier/src/components/LocaleSelect.tsx @@ -1,19 +1,26 @@ import React, { useCallback, useMemo, useEffect } from 'react'; -import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import MenuItem from '@material-ui/core/MenuItem'; -import Select from '@material-ui/core/Select'; +import Select, { SelectProps } from '@material-ui/core/Select'; import allLocales from '../locales'; +import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector'; -function LocaleSelect({ locales: localesProp, ...other } = {}) { + +interface LocaleSelectProps extends SelectProps { + locales?: string[], + +} + + +const LocaleSelect: React.FC = ({ locales: localesProp, ...other } = {}) => { const { i18n, ready } = useTranslation(); const handleChange = useCallback((event) => { i18n.changeLanguage(event.target.value); - }, [ i18n ]) + }, [i18n]) const locales = useMemo(() => { if (!localesProp) { @@ -34,11 +41,14 @@ function LocaleSelect({ locales: localesProp, ...other } = {}) { // Have language -> is supported all good. return; } - const wanted = i18n.modules.languageDetector.detectors.navigator.lookup(); - i18n.modules.languageDetector.services.languageUtils.options.supportedLngs = locales.map(locale => locale.locale); - i18n.modules.languageDetector.services.languageUtils.options.fallbackLng = null; - let best = i18n.modules.languageDetector.services.languageUtils.getBestMatchFromCodes(wanted); + const detector = i18n.modules.languageDetector as I18nextBrowserLanguageDetector; + + const wanted = detector.detectors.navigator.lookup(); + detector.services.languageUtils.options.supportedLngs = locales.map(locale => locale.locale); + detector.services.languageUtils.options.fallbackLng = null; + + let best = detector.services.languageUtils.getBestMatchFromCodes(wanted); if (!best) { best = locales[0].locale; } @@ -69,8 +79,4 @@ function LocaleSelect({ locales: localesProp, ...other } = {}) { ; } -LocaleSelect.propTypes = { - locales: PropTypes.arrayOf(PropTypes.string), -}; - export default LocaleSelect; diff --git a/identifier/src/components/PrivateRoute.jsx b/identifier/src/components/PrivateRoute.jsx deleted file mode 100644 index ee70038e..00000000 --- a/identifier/src/components/PrivateRoute.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Route } from 'react-router-dom'; - -import RedirectWithQuery from './RedirectWithQuery'; - -const PrivateRoute = ({ component: Target, hello, ...rest }) => ( - ( - hello ? ( - - ) : ( - - ) - )}/> -); - -PrivateRoute.propTypes = { - component: PropTypes.elementType.isRequired, - hello: PropTypes.object -}; - -export default PrivateRoute; diff --git a/identifier/src/components/PrivateRoute.tsx b/identifier/src/components/PrivateRoute.tsx new file mode 100644 index 00000000..78599bc3 --- /dev/null +++ b/identifier/src/components/PrivateRoute.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Route, RouteComponentProps } from "react-router-dom"; + +import RedirectWithQuery from "./RedirectWithQuery"; +import { ReactElement } from "react"; + +const PrivateRoute: React.FC<{ component: React.FC, hello: object, [key: string]: boolean | string | object | ReactElement }> = ({ component: Target, hello, ...rest }) => ( + ) => + hello ? : + } + /> +); + +export default PrivateRoute; diff --git a/identifier/src/components/RedirectWithQuery.jsx b/identifier/src/components/RedirectWithQuery.jsx deleted file mode 100644 index e75aa59c..00000000 --- a/identifier/src/components/RedirectWithQuery.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withRouter } from 'react-router'; -import { Redirect } from 'react-router-dom'; - -const RedirectWithQuery = ({target, location, ...rest}) => { - const to = { - pathname: target, - search: location.search, - hash: location.hash - }; - - return ( - - ); -}; - -RedirectWithQuery.propTypes = { - target: PropTypes.string.isRequired, - location: PropTypes.object.isRequired -}; - -export default withRouter(RedirectWithQuery); diff --git a/identifier/src/components/RedirectWithQuery.tsx b/identifier/src/components/RedirectWithQuery.tsx new file mode 100644 index 00000000..e62f9a1e --- /dev/null +++ b/identifier/src/components/RedirectWithQuery.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { RouteComponentProps, withRouter } from 'react-router'; +import { Redirect } from 'react-router-dom'; + +interface RedirectWithQueryProps extends RouteComponentProps { + target: string, location: { [key: string]: string | undefined } +} + +const RedirectWithQuery: React.FC = ({ target, location, ...rest }) => { + const to = { + pathname: target, + search: location.search, + hash: location.hash + }; + + return ( + + ); +}; + +export default withRouter(RedirectWithQuery); diff --git a/identifier/src/components/ResponsiveDialog.jsx b/identifier/src/components/ResponsiveDialog.jsx deleted file mode 100644 index 17331ddd..00000000 --- a/identifier/src/components/ResponsiveDialog.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Dialog from '@material-ui/core/Dialog'; -import withMobileDialog from '@material-ui/core/withMobileDialog'; - -const ResponsiveDialog = (props) => { - return ; -}; - -ResponsiveDialog.propTypes = { - fullScreen: PropTypes.bool.isRequired -}; - -export default withMobileDialog()(ResponsiveDialog); diff --git a/identifier/src/components/ResponsiveDialog.tsx b/identifier/src/components/ResponsiveDialog.tsx new file mode 100644 index 00000000..0aa31377 --- /dev/null +++ b/identifier/src/components/ResponsiveDialog.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +import Dialog, { DialogProps } from "@material-ui/core/Dialog"; +import withMobileDialog from "@material-ui/core/withMobileDialog"; + +const ResponsiveDialog = (props: DialogProps & { fullScreen: boolean }) => { + return ; +}; + +export default withMobileDialog()(ResponsiveDialog); diff --git a/identifier/src/components/ResponsiveScreen.jsx b/identifier/src/components/ResponsiveScreen.jsx deleted file mode 100644 index 76194200..00000000 --- a/identifier/src/components/ResponsiveScreen.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { withStyles } from '@material-ui/core/styles'; -import Grid from '@material-ui/core/Grid'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; - -import ResponsiveDialog from './ResponsiveDialog'; -import Logo from '../images/app-icon.svg'; -import Loading from './Loading'; -import LocaleSelect from './LocaleSelect'; - -const styles = theme => ({ - root: { - display: 'flex', - flex: 1, - }, - content: { - paddingTop: 24, - paddingBottom: 12, - minHeight: 350, - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - position: 'relative' - }, - dialog: { - maxWidth: 440, - }, - logo: { - height: 24, - }, - actions: { - marginTop: -40, - minHeight: 45, - justifyContent: 'flex-start', - paddingLeft: theme.spacing(3), - paddingRight: theme.spacing(3) - } -}); - -const ResponsiveScreen = (props) => { - const { - classes, - withoutLogo, - withoutPadding, - loading, - branding, - children, - className, - DialogProps, - PaperProps, - ...other - } = props; - - const bannerLogoSrc = branding?.bannerLogo ? branding.bannerLogo : Logo; - const logo = withoutLogo ? null : - ; - - const content = loading ? : (withoutPadding ? children : {children}); - - return ( - - -
- {logo} - {content} -
- {!loading && } -
-
- ); -}; - -ResponsiveScreen.defaultProps = { - withoutLogo: false, - withoutPadding: false, - loading: false -}; - -ResponsiveScreen.propTypes = { - classes: PropTypes.object.isRequired, - withoutLogo: PropTypes.bool, - withoutPadding: PropTypes.bool, - loading: PropTypes.bool, - branding: PropTypes.object, - children: PropTypes.node.isRequired, - className: PropTypes.string, - PaperProps: PropTypes.object, - DialogProps: PropTypes.object -}; - -export default withStyles(styles)(ResponsiveScreen); diff --git a/identifier/src/components/ResponsiveScreen.tsx b/identifier/src/components/ResponsiveScreen.tsx new file mode 100644 index 00000000..bb8ef500 --- /dev/null +++ b/identifier/src/components/ResponsiveScreen.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import classNames from "classnames"; + +import { createStyles, Theme, WithStyles, withStyles } from "@material-ui/core/styles"; +import Grid from "@material-ui/core/Grid"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; + +import ResponsiveDialog from "./ResponsiveDialog"; +import Logo from "../images/app-icon.svg"; +import Loading from "./Loading"; +import LocaleSelect from "./LocaleSelect"; +import { ReactNode } from "react"; +import { DialogProps, PaperProps } from "@material-ui/core"; + +const styles = (theme: Theme) => createStyles({ + root: { + display: "flex", + flex: 1, + }, + content: { + paddingTop: 24, + paddingBottom: 12, + minHeight: 350, + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + position: "relative", + }, + dialog: { + maxWidth: 440, + }, + logo: { + height: 24, + }, + actions: { + marginTop: -40, + minHeight: 45, + justifyContent: "flex-start", + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + }, +}); + + +interface ResponsiveScreenProps extends WithStyles { + withoutLogo?: boolean, + withoutPadding?: boolean, + loading?: boolean, + branding?: { bannerLogo: string, locales: string[] } | null, + children: ReactNode, + className?: string, + PaperProps?: PaperProps, + DialogProps?: DialogProps, +} + +const ResponsiveScreen: React.FC = (props) => { + const { + classes, + withoutLogo = false, + withoutPadding = false, + loading = false, + branding, + children, + className, + DialogProps, + PaperProps, + ...other + } = props; + + const bannerLogoSrc = branding?.bannerLogo ? branding.bannerLogo : Logo; + const logo = withoutLogo ? null : ( + + + + ); + + const LoadingComponent = Loading as React.FC; + + const content = loading ? ( + + ) : withoutPadding ? ( + children + ) : ( + {children} + ); + + return ( + + +
+ {logo} + {content} +
+ {!loading && ( + + + + )} +
+
+ ); +}; + +export default withStyles(styles)(ResponsiveScreen); diff --git a/identifier/src/components/ScopesList.jsx b/identifier/src/components/ScopesList.jsx deleted file mode 100644 index 3adcf492..00000000 --- a/identifier/src/components/ScopesList.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import { withStyles } from '@material-ui/core/styles'; -import PropTypes from 'prop-types'; -import Checkbox from '@material-ui/core/Checkbox'; - -import { useTranslation } from 'react-i18next'; - -const styles = () => ({ - row: { - paddingTop: 0, - paddingBottom: 0 - } -}); - -const ScopesList = ({scopes, meta, classes, ...rest}) => { - const { mapping, definitions } = meta; - - const { t } = useTranslation(); - - const rows = []; - const known = {}; - - // TODO(longsleep): Sort scopes according to priority. - for (let scope in scopes) { - if (!scopes[scope]) { - continue; - } - let id = mapping[scope]; - if (id) { - if (known[id]) { - continue; - } - known[id] = true; - } else { - id = scope; - } - let definition = definitions[id]; - let label; - if (definition) { - switch (definition.id) { - case 'scope_alias_basic': - label = t("konnect.scopeDescription.aliasBasic", "Access your basic account information"); - break; - case 'scope_offline_access': - label = t("konnect.scopeDescription.offlineAccess", "Keep the allowed access persistently and forever"); - break; - default: - } - if (!label) { - label = definition.description; - } - } - if (!label) { - label = t("konnect.scopeDescription.scope", "Scope: {{scope}}", { scope }); - } - - rows.push( - - - - ); - } - - return ( - - {rows} - - ); -}; - -ScopesList.propTypes = { - classes: PropTypes.object.isRequired, - - scopes: PropTypes.object.isRequired, - meta: PropTypes.object.isRequired -}; - -export default withStyles(styles)(ScopesList); diff --git a/identifier/src/components/ScopesList.tsx b/identifier/src/components/ScopesList.tsx new file mode 100644 index 00000000..ac2cc650 --- /dev/null +++ b/identifier/src/components/ScopesList.tsx @@ -0,0 +1,87 @@ +import React from "react"; + +import List, { ListProps } from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemText from "@material-ui/core/ListItemText"; +import { createStyles, WithStyles, withStyles } from "@material-ui/core/styles"; +import Checkbox from "@material-ui/core/Checkbox"; + +import { useTranslation } from "react-i18next"; + +const styles = () => createStyles({ + row: { + paddingTop: 0, + paddingBottom: 0, + }, +}); + +interface ScopesListProps extends WithStyles { + scopes: string[], + meta: { + mapping: string[], + definitions: { [key: string]: { [key: string]: string } } + } +} + +const ScopesList: React.FC = ({ scopes, meta, classes, ...rest }) => { + const { mapping, definitions } = meta; + + const { t } = useTranslation(); + + const rows = []; + const known: { [key: string]: boolean } = {}; + + // TODO(longsleep): Sort scopes according to priority. + for (const scope in scopes) { + if (!scopes[scope]) { + continue; + } + let id = mapping[scope]; + if (id) { + if (known[id]) { + continue; + } + known[id] = true; + } else { + id = scope; + } + const definition = definitions[id]; + let label; + if (definition) { + switch (definition.id) { + case "scope_alias_basic": + label = t( + "konnect.scopeDescription.aliasBasic", + "Access your basic account information" + ); + break; + case "scope_offline_access": + label = t( + "konnect.scopeDescription.offlineAccess", + "Keep the allowed access persistently and forever" + ); + break; + default: + } + if (!label) { + label = definition.description; + } + } + if (!label) { + label = t("konnect.scopeDescription.scope", "Scope: {{scope}}", { + scope, + }); + } + + rows.push( + + + + + ); + } + + return {rows}; +}; + +export default withStyles(styles)(ScopesList); diff --git a/identifier/src/components/Spinner.jsx b/identifier/src/components/Spinner.tsx similarity index 70% rename from identifier/src/components/Spinner.jsx rename to identifier/src/components/Spinner.tsx index 190bd889..24813fe2 100644 --- a/identifier/src/components/Spinner.jsx +++ b/identifier/src/components/Spinner.tsx @@ -3,17 +3,17 @@ import React from 'react'; import { Fade, CircularProgress, - } from '@material-ui/core'; - import { makeStyles } from '@material-ui/core/styles'; +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; - const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(() => ({ root: { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', }, - })); +})); const Spinner = () => { const classes = useStyles(); @@ -26,7 +26,7 @@ const Spinner = () => { }} unmountOnExit > - + ; } diff --git a/identifier/src/containers/Goodbye/Goodbyescreen.jsx b/identifier/src/containers/Goodbye/Goodbyescreen.jsx deleted file mode 100644 index 511fedb3..00000000 --- a/identifier/src/containers/Goodbye/Goodbyescreen.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { withTranslation } from 'react-i18next'; - -import renderIf from 'render-if'; - -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import DialogActions from '@material-ui/core/DialogActions'; - -import ResponsiveScreen from '../../components/ResponsiveScreen'; -import { executeHello, executeLogoff } from '../../actions/common'; - -const styles = theme => ({ - subHeader: { - marginBottom: theme.spacing(5) - }, - wrapper: { - marginTop: theme.spacing(5), - position: 'relative', - display: 'inline-block' - } -}); - -class Goodbyescreen extends React.PureComponent { - componentDidMount() { - this.props.dispatch(executeHello()); - } - - render() { - const { classes, branding, hello, t } = this.props; - - const loading = hello === null; - return ( - - {renderIf(hello !== null && !hello.state)(() => ( -
- - {t("konnect.goodbye.headline", "Goodbye")} - - - {t("konnect.goodbye.subHeader", "you have been signed out from your account")} - - - {t("konnect.goodbye.message.close", "You can close this window now.")} - -
- ))} - {renderIf(hello !== null && hello.state === true)(() => ( -
- - {t("konnect.goodbye.confirm.headline", "Hello {{displayName}}", { displayName: hello.displayName })} - - - {t("konnect.goodbye.confirm.subHeader", "please confirm sign out")} - - - - {t("konnect.goodbye.message.confirm", "Press the button below, to sign out from your account now.")} - - - -
- -
-
-
- ))} -
- ); - } - - logoff(event) { - event.preventDefault(); - - this.props.dispatch(executeLogoff()).then((response) => { - const { history } = this.props; - - if (response.success) { - this.props.dispatch(executeHello()); - history.push('/goodbye'); - } - }); - } -} - -Goodbyescreen.propTypes = { - classes: PropTypes.object.isRequired, - t: PropTypes.func.isRequired, - - branding: PropTypes.object, - hello: PropTypes.object, - - dispatch: PropTypes.func.isRequired, - history: PropTypes.object.isRequired -}; - -const mapStateToProps = (state) => { - const { branding, hello } = state.common; - - return { - branding, - hello - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Goodbyescreen))); diff --git a/identifier/src/containers/Goodbye/Goodbyescreen.tsx b/identifier/src/containers/Goodbye/Goodbyescreen.tsx new file mode 100644 index 00000000..5ac41411 --- /dev/null +++ b/identifier/src/containers/Goodbye/Goodbyescreen.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { withTranslation } from 'react-i18next'; + +import renderIf from 'render-if'; + +import { createStyles, Theme, WithStyles, withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import DialogActions from '@material-ui/core/DialogActions'; + +import ResponsiveScreen from '../../components/ResponsiveScreen'; +import { executeHello, executeLogoff } from '../../actions/common'; +import { PromiseDispatch, RootState } from '../../store'; +import { TFunction } from 'i18next'; +import { History } from 'history'; +import { ObjectType } from '../../types'; + +const styles = (theme: Theme) => createStyles({ + subHeader: { + marginBottom: theme.spacing(5) + }, + wrapper: { + marginTop: theme.spacing(5), + position: 'relative', + display: 'inline-block' + }, + button: {} +}); + +interface GoodbyescreenProps extends WithStyles { + t: TFunction, + branding?: { bannerLogo: string, locales: string[] } | null, + hello?: ObjectType | null, + dispatch: PromiseDispatch, + history?: History +} + +const Goodbyescreen: React.FC = ({ classes, t, branding, hello, dispatch, history }) => { + + const logoff = (event: React.MouseEvent) => { + event.preventDefault(); + + dispatch(executeLogoff()).then((response) => { + if (response?.success) { + dispatch(executeHello()); + history?.push('/goodbye'); + } + }); + } + + React.useEffect(() => { + dispatch(executeHello()); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const loading = hello === null; + + + + return ( + + {renderIf(hello !== null && !hello?.state)(() => ( +
+ + {t("konnect.goodbye.headline", "Goodbye")} + + + {t("konnect.goodbye.subHeader", "you have been signed out from your account")} + + + {t("konnect.goodbye.message.close", "You can close this window now.")} + +
+ ))} + {renderIf(hello !== null && hello?.state === true)(() => ( +
+ + {t("konnect.goodbye.confirm.headline", "Hello {{displayName}}", { displayName: hello?.displayName })} + + + {t("konnect.goodbye.confirm.subHeader", "please confirm sign out")} + + + + {t("konnect.goodbye.message.confirm", "Press the button below, to sign out from your account now.")} + + + +
+ +
+
+
+ ))} +
+ ); +} + + +const mapStateToProps = (state: RootState) => { + const { branding, hello } = state.common; + + return { + branding, + hello: hello as ObjectType + }; +}; + +export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Goodbyescreen))); diff --git a/identifier/src/containers/Goodbye/index.js b/identifier/src/containers/Goodbye/index.tsx similarity index 100% rename from identifier/src/containers/Goodbye/index.js rename to identifier/src/containers/Goodbye/index.tsx diff --git a/identifier/src/containers/Login/Chooseaccount.jsx b/identifier/src/containers/Login/Chooseaccount.jsx deleted file mode 100644 index cb3cfded..00000000 --- a/identifier/src/containers/Login/Chooseaccount.jsx +++ /dev/null @@ -1,150 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { withTranslation } from 'react-i18next'; - -import { withStyles } from '@material-ui/core/styles'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import Avatar from '@material-ui/core/Avatar'; -import Typography from '@material-ui/core/Typography'; -import DialogContent from '@material-ui/core/DialogContent'; - -import { executeLogonIfFormValid, advanceLogonFlow } from '../../actions/login'; -import { ErrorMessage } from '../../errors'; - -const styles = theme => ({ - content: { - overflowY: 'visible', - }, - subHeader: { - marginBottom: theme.spacing(2) - }, - message: { - marginTop: theme.spacing(2) - }, - accountList: { - marginLeft: theme.spacing(-5), - marginRight: theme.spacing(-5) - }, - accountListItem: { - paddingLeft: theme.spacing(5), - paddingRight: theme.spacing(5) - } -}); - -class Chooseaccount extends React.PureComponent { - componentDidMount() { - const { hello, history } = this.props; - if ((!hello || !hello.state) && history.action !== 'PUSH') { - history.replace(`/identifier${history.location.search}${history.location.hash}`); - } - } - - render() { - const { loading, errors, classes, hello, t } = this.props; - - let errorMessage = null; - if (errors.http) { - errorMessage = - - ; - } - - let username = ''; - if (hello && hello.state) { - username = hello.username; - } - - return ( - - - {t("konnect.chooseaccount.headline", "Choose an account")} - - - {t("konnect.chooseaccount.subHeader", "to sign in")} - - -
this.logon(event)}> - - this.logon(event)} - >{username.substr(0, 1)} - - - this.logoff(event)} - > - - - {t("konnect.chooseaccount.useOther.persona.label", "?")} - - - - - - - {errorMessage} -
-
- ); - } - - logon(event) { - event.preventDefault(); - - const { hello, dispatch, history } = this.props; - dispatch(executeLogonIfFormValid(hello.username, '', true)).then((response) => { - if (response.success) { - dispatch(advanceLogonFlow(response.success, history)); - } - }); - } - - logoff(event) { - event.preventDefault(); - - const { history} = this.props; - history.push(`/identifier${history.location.search}${history.location.hash}`); - } -} - -Chooseaccount.propTypes = { - classes: PropTypes.object.isRequired, - t: PropTypes.func.isRequired, - - loading: PropTypes.string.isRequired, - errors: PropTypes.object.isRequired, - hello: PropTypes.object, - - dispatch: PropTypes.func.isRequired, - history: PropTypes.object.isRequired -}; - -const mapStateToProps = (state) => { - const { loading, errors } = state.login; - const { hello } = state.common; - - return { - loading, - errors, - hello - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Chooseaccount))); diff --git a/identifier/src/containers/Login/Chooseaccount.tsx b/identifier/src/containers/Login/Chooseaccount.tsx new file mode 100644 index 00000000..93d61a7e --- /dev/null +++ b/identifier/src/containers/Login/Chooseaccount.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { History } from 'history'; + +import { withTranslation } from 'react-i18next'; + +import { createStyles, Theme, WithStyles, withStyles } from '@material-ui/core/styles'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import Avatar from '@material-ui/core/Avatar'; +import Typography from '@material-ui/core/Typography'; +import DialogContent from '@material-ui/core/DialogContent'; + +import { executeLogonIfFormValid, advanceLogonFlow } from '../../actions/login'; +import { ErrorMessage, ErrorType } from '../../errors'; +import { TFunction } from 'i18next'; +import { ObjectType } from '../../types'; +import { PromiseDispatch, RootState } from '../../store'; + +const styles = (theme: Theme) => createStyles({ + content: { + overflowY: 'visible', + }, + subHeader: { + marginBottom: theme.spacing(2) + }, + message: { + marginTop: theme.spacing(2) + }, + accountList: { + marginLeft: theme.spacing(-5), + marginRight: theme.spacing(-5) + }, + accountListItem: { + paddingLeft: theme.spacing(5), + paddingRight: theme.spacing(5) + } +}); + +interface ChooseaccountProps extends WithStyles { + t: TFunction, + errors: ErrorType, + hello?: ObjectType | null, + dispatch: PromiseDispatch, + history: History, + loading: string, +} + + +const Chooseaccount: React.FC = ({ t, errors, hello, dispatch, history, loading, classes }) => { + + const logon = (event: React.MouseEvent | React.FormEvent) => { + event.preventDefault(); + dispatch(executeLogonIfFormValid(hello?.username as string, '', true)).then((response) => { + if (response.success) { + dispatch(advanceLogonFlow(response.success as boolean, history)); + } + }); + } + + const logoff = (event: React.MouseEvent) => { + event.preventDefault(); + history.push(`/identifier${history.location.search}${history.location.hash}`); + } + + + React.useEffect(() => { + if ((!hello || !hello.state) && history.action !== 'PUSH') { + history.replace(`/identifier${history.location.search}${history.location.hash}`); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + let errorMessage = null; + if (errors?.http) { + errorMessage = + + ; + } + + let username = ''; + if (hello && hello.state) { + username = (hello.username as string); + } + + return ( + + + {t("konnect.chooseaccount.headline", "Choose an account")} + + + {t("konnect.chooseaccount.subHeader", "to sign in")} + + +
logon(event)}> + + logon(event)} + >{username.substr(0, 1)} + + + logoff(event)} + > + + + {t("konnect.chooseaccount.useOther.persona.label", "?")} + + + + + + + {errorMessage} +
+
+ ); +} + + +const mapStateToProps = (state: RootState) => { + const { loading, errors } = state.login; + const { hello } = state.common; + + return { + loading, + errors, + hello: hello as ObjectType + }; +}; + +export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Chooseaccount))); diff --git a/identifier/src/containers/Login/Consent.jsx b/identifier/src/containers/Login/Consent.jsx deleted file mode 100644 index 857344d0..00000000 --- a/identifier/src/containers/Login/Consent.jsx +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { withTranslation, Trans } from 'react-i18next'; - -import renderIf from 'render-if'; - -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import BaseTooltip from '@material-ui/core/Tooltip'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import green from '@material-ui/core/colors/green'; -import Typography from '@material-ui/core/Typography'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; - -import { executeConsent, advanceLogonFlow, receiveValidateLogon } from '../../actions/login'; -import { ErrorMessage } from '../../errors'; -import { REQUEST_CONSENT_ALLOW } from '../../actions/types'; -import ClientDisplayName from '../../components/ClientDisplayName'; -import ScopesList from '../../components/ScopesList'; - -const styles = theme => ({ - button: { - margin: theme.spacing(1), - minWidth: 100 - }, - buttonProgress: { - color: green[500], - position: 'absolute', - top: '50%', - left: '50%', - marginTop: -12, - marginLeft: -12 - }, - subHeader: { - marginBottom: theme.spacing(2) - }, - scopesList: { - marginBottom: theme.spacing(2) - }, - wrapper: { - marginTop: theme.spacing(2), - position: 'relative', - display: 'inline-block' - }, - message: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2) - } -}); - -const Tooltip = ({children, ...other } = {}) => { - // Ensures that there is only a single child for the tooltip element to - // make it compatible with the Trans component. - return {children}; -} - -Tooltip.propTypes = { - children: PropTypes.node.isRequired, -} - -class Consent extends React.PureComponent { - componentDidMount() { - const { dispatch, hello, history, client } = this.props; - if ((!hello || !hello.state || !client) && history.action !== 'PUSH') { - history.replace(`/identifier${history.location.search}${history.location.hash}`); - } - - dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors. - } - - action = (allow=false, scopes={}) => (event) => { - event.preventDefault(); - - if (allow === undefined) { - return; - } - - // Convert all scopes which are true to a scope value. - const scope = Object.keys(scopes).filter(scope => { - return !!scopes[scope]; - }).join(' '); - - const { dispatch, history } = this.props; - dispatch(executeConsent(allow, scope)).then((response) => { - if (response.success) { - dispatch(advanceLogonFlow(response.success, history, true, {konnect: response.state})); - } - }); - } - - render() { - const { classes, loading, hello, errors, client, t } = this.props; - - const scopes = hello.details.scopes || {}; - const meta = hello.details.meta || {}; - - return ( - - - {t("konnect.consent.headline", "Hi {{displayName}}", { displayName: hello.displayName })} - - - {hello.username} - - - - - - - wants to - - - - - - - Allow to do this? - - - - {t("konnect.consent.consequence", "By clicking Allow, you allow this app to use your information.")} - - -
- -
- - {(loading && loading !== REQUEST_CONSENT_ALLOW) && - } -
-
- - {loading === REQUEST_CONSENT_ALLOW && } -
-
- - {renderIf(errors.http)(() => ( - - - - ))} -
-
- ); - } -} - -Consent.propTypes = { - classes: PropTypes.object.isRequired, - t: PropTypes.func.isRequired, - - loading: PropTypes.string.isRequired, - errors: PropTypes.object.isRequired, - hello: PropTypes.object, - client: PropTypes.object.isRequired, - - dispatch: PropTypes.func.isRequired, - history: PropTypes.object.isRequired -}; - -const mapStateToProps = (state) => { - const { hello } = state.common; - const { loading, errors } = state.login; - - return { - loading: loading, - errors, - hello, - client: hello.details.client || {} - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Consent))); diff --git a/identifier/src/containers/Login/Consent.tsx b/identifier/src/containers/Login/Consent.tsx new file mode 100644 index 00000000..00abf209 --- /dev/null +++ b/identifier/src/containers/Login/Consent.tsx @@ -0,0 +1,190 @@ +import React, { ReactNode } from 'react'; +import { connect } from 'react-redux'; + +import { withTranslation, Trans } from 'react-i18next'; + +import renderIf from 'render-if'; + +import { createStyles, Theme, WithStyles, withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import BaseTooltip, { TooltipProps } from '@material-ui/core/Tooltip'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import green from '@material-ui/core/colors/green'; +import Typography from '@material-ui/core/Typography'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; + +import { executeConsent, advanceLogonFlow, receiveValidateLogon } from '../../actions/login'; +import { ErrorMessage, ErrorType } from '../../errors'; +import { REQUEST_CONSENT_ALLOW } from '../../actions/types'; +import ClientDisplayName from '../../components/ClientDisplayName'; +import ScopesList from '../../components/ScopesList'; +import { PromiseDispatch, RootState } from '../../store'; +import { MappingType, ObjectType } from '../../types'; +import { TFunction } from 'i18next'; +import { History } from 'history'; + +const styles = (theme: Theme) => createStyles({ + button: { + margin: theme.spacing(1), + minWidth: 100 + }, + buttonProgress: { + color: green[500], + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12 + }, + subHeader: { + marginBottom: theme.spacing(2) + }, + scopesList: { + marginBottom: theme.spacing(2) + }, + wrapper: { + marginTop: theme.spacing(2), + position: 'relative', + display: 'inline-block' + }, + message: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2) + } +}); + +const Tooltip: React.FC<{ children: ReactNode } & TooltipProps> = ({ children, ...other }) => { + // Ensures that there is only a single child for the tooltip element to + // make it compatible with the Trans component. + return {children}; +} + + +interface ConsentProps extends WithStyles { + t: TFunction + hello?: ObjectType | null, + dispatch: PromiseDispatch, + history?: History, + loading: string, + errors: ErrorType, + client: { [key: string]: string } +} + +const Consent: React.FC = ({ t, hello, dispatch, history, loading, errors, client, classes }) => { + + React.useEffect(() => { + if ((!hello || !hello.state || !client) && history?.action !== 'PUSH') { + history?.replace(`/identifier${history?.location?.search}${history?.location?.hash}`); + } + dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors. + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + + const action = (allow = false, scopes: ObjectType = {}) => (event: React.FormEvent | React.MouseEvent | undefined) => { + event?.preventDefault(); + + if (allow === undefined) { + return; + } + + // Convert all scopes which are true to a scope value. + const scope = Object.keys(scopes).filter(scope => { + return !!scopes[scope]; + }).join(' '); + + dispatch(executeConsent(allow, scope)).then((response) => { + if (response.success) { + dispatch(advanceLogonFlow(response.success as boolean, history, true, { konnect: (response as ObjectType).state })); + } + }); + } + + + + const scopes = (hello?.details as ObjectType)?.scopes || {}; + const meta = (hello?.details as ObjectType)?.meta as { scopes: MappingType } || {}; + + return ( + + + {t("konnect.consent.headline", "Hi {{displayName}}", { displayName: hello?.displayName })} + + + {hello?.username} + + + + + + + wants to + + + + + + + Allow to do this? + + + + {t("konnect.consent.consequence", "By clicking Allow, you allow this app to use your information.")} + + +
action(undefined, scopes as ObjectType)}> + +
+ + {(loading && loading !== REQUEST_CONSENT_ALLOW) && + } +
+
+ + {loading === REQUEST_CONSENT_ALLOW && } +
+
+ + {renderIf(errors?.http)(() => ( + + + + ))} +
+
+ ); +} + +const mapStateToProps = (state: RootState) => { + const { hello } = state.common; + const { loading, errors } = state.login; + + return { + loading: loading, + errors, + hello: hello as ObjectType, + client: (hello?.details as ObjectType)?.client as { [key: string]: string } || {} + }; +}; + +export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Consent))); diff --git a/identifier/src/containers/Login/Login.jsx b/identifier/src/containers/Login/Login.tsx similarity index 68% rename from identifier/src/containers/Login/Login.jsx rename to identifier/src/containers/Login/Login.tsx index ee5966e9..6af62ae3 100644 --- a/identifier/src/containers/Login/Login.jsx +++ b/identifier/src/containers/Login/Login.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, useMemo, useRef } from 'react'; -import PropTypes from 'prop-types'; +import React, { useEffect, useMemo } from 'react'; import { connect } from 'react-redux'; import { useTranslation } from 'react-i18next'; @@ -7,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import renderIf from 'render-if'; import { isEmail } from 'validator'; -import { withStyles } from '@material-ui/core/styles'; +import { createStyles, Theme, WithStyles, withStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; import CircularProgress from '@material-ui/core/CircularProgress'; import green from '@material-ui/core/colors/green'; @@ -17,13 +16,17 @@ import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import { updateInput, executeLogonIfFormValid, advanceLogonFlow } from '../../actions/login'; -import { ErrorMessage } from '../../errors'; +import { ErrorMessage, ErrorType } from '../../errors'; import IconButton from '@material-ui/core/IconButton'; import InputAdornment from '@material-ui/core/InputAdornment' import Visibility from '@material-ui/icons/Visibility'; import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import { PromiseDispatch, RootState } from '../../store'; +import { ObjectType } from '../../types'; +import { History } from "history"; +import { ParsedQuery } from 'query-string'; -const styles = theme => ({ +const styles = (theme: Theme) => createStyles({ button: { margin: theme.spacing(1), minWidth: 100 @@ -56,63 +59,73 @@ const styles = theme => ({ }, }); -function Login(props) { - const { - hello, - branding, - query, - dispatch, - history, - loading, - errors, - classes, - username, - password, - } = props; +interface LoginProps extends WithStyles { + branding?: { bannerLogo: string, locales: string[], signinPageText?: string, usernameHintText?: string } | null, + hello?: ObjectType | null, + dispatch: PromiseDispatch, + history?: History, + loading: string, + username: string, + password: string, + errors: ErrorType, + query: ParsedQuery, +} + +const Login: React.FC = ({ hello, + branding, + query, + dispatch, + history, + loading, + errors, + classes, + username, + password, }) => { + const { t } = useTranslation(); const [showPassword, setShowPassword] = React.useState(false); - const passwordInputRef = useRef(); + const passwordInputRef = React.useRef(); useEffect(() => { - if (hello && hello.state && history.action !== 'PUSH') { + if (hello && hello.state && history?.action !== 'PUSH') { if (!query.prompt || query.prompt.indexOf('select_account') === -1) { dispatch(advanceLogonFlow(true, history)); return; } - history.replace(`/chooseaccount${history.location.search}${history.location.hash}`); + history?.replace(`/chooseaccount${history?.location.search}${history?.location.hash}`); return; } // If login_hint is an email, set it into the username field automatically. if (query && query.login_hint) { - if (isEmail(query.login_hint) || isEmail(`${query.login_hint}@example.com`)) { - dispatch(updateInput("username", query.login_hint)); + if (isEmail(query.login_hint as string) || isEmail(`${query.login_hint}@example.com`)) { + dispatch(updateInput("username", query.login_hint as string)); setTimeout(() => { - passwordInputRef.current.focus(); + passwordInputRef.current?.focus(); }, 0); } } - }, [ /* no dependencies */ ]); // eslint-disable-line react-hooks/exhaustive-deps + }, [ /* no dependencies */]); // eslint-disable-line react-hooks/exhaustive-deps - const handleChange = (name) => (event) => { + const handleChange = (name: string) => (event: React.ChangeEvent) => { dispatch(updateInput(name, event.target.value)); }; - const handleNextClick = (event) => { + const handleNextClick = (event: React.MouseEvent) => { event.preventDefault(); dispatch(executeLogonIfFormValid(username, password, false)).then((response) => { if (response.success) { - dispatch(advanceLogonFlow(response.success, history)); + dispatch(advanceLogonFlow(response.success as boolean, history)); } }); }; const usernamePlaceHolder = useMemo(() => { - if (branding?.usernameHintText ) { + if (branding?.usernameHintText) { switch (branding.usernameHintText) { case "Username": break; @@ -134,11 +147,11 @@ function Login(props) { {t("konnect.login.headline", "Sign in")} -
this.logon(event)}> + } + error={!!errors?.username} + helperText={} fullWidth autoFocus inputProps={{ @@ -155,8 +168,8 @@ function Login(props) { inputRef={passwordInputRef} type={showPassword ? "text" : "password"} label={t("konnect.login.passwordField.label", "Password")} - error={!!errors.password} - helperText={} + error={!!errors?.password} + helperText={} fullWidth onChange={handleChange('password')} autoComplete="kopano-account current-password" @@ -192,9 +205,9 @@ function Login(props) { - {renderIf(errors.http)(() => ( + {renderIf(errors?.http)(() => ( - + ))} @@ -204,23 +217,9 @@ function Login(props) { ); } -Login.propTypes = { - classes: PropTypes.object.isRequired, - - loading: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - password: PropTypes.string.isRequired, - errors: PropTypes.object.isRequired, - branding: PropTypes.object, - hello: PropTypes.object, - query: PropTypes.object.isRequired, - - dispatch: PropTypes.func.isRequired, - history: PropTypes.object.isRequired -}; -const mapStateToProps = (state) => { - const { loading, username, password, errors} = state.login; +const mapStateToProps = (state: RootState) => { + const { loading, username, password, errors } = state.login; const { branding, hello, query } = state.common; return { @@ -229,7 +228,7 @@ const mapStateToProps = (state) => { password, errors, branding, - hello, + hello: hello as ObjectType, query }; }; diff --git a/identifier/src/containers/Login/Loginscreen.jsx b/identifier/src/containers/Login/Loginscreen.jsx deleted file mode 100644 index ab849c8d..00000000 --- a/identifier/src/containers/Login/Loginscreen.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { Route, Switch } from 'react-router-dom'; - -import { withStyles } from '@material-ui/core/styles'; - -import ResponsiveScreen from '../../components/ResponsiveScreen'; -import RedirectWithQuery from '../../components/RedirectWithQuery'; -import { executeHello } from '../../actions/common'; - -import Login from './Login'; -import Chooseaccount from './Chooseaccount'; -import Consent from './Consent'; - -const styles = () => ({ -}); - -class Loginscreen extends React.PureComponent { - componentDidMount() { - this.props.dispatch(executeHello()); - } - - render() { - const { branding, hello } = this.props; - - const loading = hello === null; - return ( - - - - - - - - - ); - } -} - -Loginscreen.propTypes = { - classes: PropTypes.object.isRequired, - - branding: PropTypes.object, - hello: PropTypes.object, - - dispatch: PropTypes.func.isRequired -}; - -const mapStateToProps = (state) => { - const { branding, hello } = state.common; - - return { - branding, - hello - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(Loginscreen)); diff --git a/identifier/src/containers/Login/Loginscreen.tsx b/identifier/src/containers/Login/Loginscreen.tsx new file mode 100644 index 00000000..d4db252f --- /dev/null +++ b/identifier/src/containers/Login/Loginscreen.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { Route, Switch } from 'react-router-dom'; + +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; + +import ResponsiveScreen from '../../components/ResponsiveScreen'; +import RedirectWithQuery from '../../components/RedirectWithQuery'; +import { executeHello } from '../../actions/common'; + +import Login from './Login'; +import Chooseaccount from './Chooseaccount'; +import Consent from './Consent'; +import { PromiseDispatch, RootState } from '../../store'; +import { ObjectType } from '../../types'; + +const styles = () => createStyles({ +}); + +interface LoginscreenProps extends WithStyles { + branding?: { bannerLogo: string, locales: string[] } | null, + hello?: ObjectType | null, + dispatch: PromiseDispatch, +} + +const Loginscreen: React.FC = ({ branding, hello, dispatch }) => { + + React.useEffect(() => { + dispatch(executeHello()); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const loading = hello === null; + + return ( + + + + + + + + + ); +} + + +const mapStateToProps = (state: RootState) => { + const { branding, hello } = state.common; + + return { + branding, + hello: hello as ObjectType + }; +}; + +export default connect(mapStateToProps)(withStyles(styles)(Loginscreen)); diff --git a/identifier/src/containers/Login/index.js b/identifier/src/containers/Login/index.ts similarity index 100% rename from identifier/src/containers/Login/index.js rename to identifier/src/containers/Login/index.ts diff --git a/identifier/src/containers/Welcome/Welcomescreen.jsx b/identifier/src/containers/Welcome/Welcomescreen.jsx deleted file mode 100644 index f20a195a..00000000 --- a/identifier/src/containers/Welcome/Welcomescreen.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { withTranslation } from 'react-i18next'; - -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import DialogActions from '@material-ui/core/DialogActions'; - -import ResponsiveScreen from '../../components/ResponsiveScreen'; -import { executeLogoff } from '../../actions/common'; - -const styles = theme => ({ - button: { - margin: theme.spacing(1), - minWidth: 100 - }, - subHeader: { - marginBottom: theme.spacing(5) - } -}); - -class Welcomescreen extends React.PureComponent { - render() { - const { classes, branding, hello, t } = this.props; - - const loading = hello === null; - return ( - - - {t("konnect.welcome.headline", "Welcome {{displayName}}", {displayName: hello.displayName})} - - - {hello.username} - - - - {t("konnect.welcome.message", "You are signed in - awesome!")} - - - - - - - ); - } - - logoff(event) { - event.preventDefault(); - - this.props.dispatch(executeLogoff()).then((response) => { - const { history } = this.props; - - if (response.success) { - history.push('/identifier'); - } - }); - } -} - -Welcomescreen.propTypes = { - classes: PropTypes.object.isRequired, - t: PropTypes.func.isRequired, - - branding: PropTypes.object, - hello: PropTypes.object, - - dispatch: PropTypes.func.isRequired, - history: PropTypes.object.isRequired -}; - -const mapStateToProps = (state) => { - const { branding, hello } = state.common; - - return { - branding, - hello - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Welcomescreen))); diff --git a/identifier/src/containers/Welcome/Welcomescreen.tsx b/identifier/src/containers/Welcome/Welcomescreen.tsx new file mode 100644 index 00000000..826bde12 --- /dev/null +++ b/identifier/src/containers/Welcome/Welcomescreen.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { History } from 'history'; + +import { withTranslation } from 'react-i18next'; + +import { createStyles, Theme, WithStyles, withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import DialogActions from '@material-ui/core/DialogActions'; + +import ResponsiveScreen from '../../components/ResponsiveScreen'; +import { executeLogoff } from '../../actions/common'; +import { PromiseDispatch, RootState } from '../../store'; +import { ObjectType } from '../../types'; +import { TFunction } from 'i18next'; + +const styles = (theme: Theme) => createStyles({ + button: { + margin: theme.spacing(1), + minWidth: 100 + }, + subHeader: { + marginBottom: theme.spacing(5) + } +}); + + +interface WelcomescreenProps extends WithStyles { + t: TFunction, + branding?: { bannerLogo: string, locales: string[] } | null, + hello?: ObjectType | null, + dispatch: PromiseDispatch, + history?: History +} + +const Welcomescreen: React.FC = ({ t, classes, branding, hello, dispatch, history }) => { + + const loading = hello === null; + + const logoff = (event: React.MouseEvent) => { + event.preventDefault(); + + dispatch(executeLogoff()).then((response) => { + + if (response?.success) { + history?.push('/identifier'); + } + }); + } + + return ( + + + {t("konnect.welcome.headline", "Welcome {{displayName}}", { displayName: hello?.displayName })} + + + {hello?.username} + + + + {t("konnect.welcome.message", "You are signed in - awesome!")} + + + + + + + ); +} + + +const mapStateToProps = (state: RootState) => { + const { branding, hello } = state.common; + + return { + branding, + hello: hello as ObjectType + }; +}; + +export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Welcomescreen))); diff --git a/identifier/src/containers/Welcome/index.js b/identifier/src/containers/Welcome/index.ts similarity index 100% rename from identifier/src/containers/Welcome/index.js rename to identifier/src/containers/Welcome/index.ts diff --git a/identifier/src/errors/index.js b/identifier/src/errors/index.ts similarity index 73% rename from identifier/src/errors/index.js rename to identifier/src/errors/index.ts index f7ca6a97..11164c32 100644 --- a/identifier/src/errors/index.js +++ b/identifier/src/errors/index.ts @@ -1,5 +1,7 @@ /* eslint react/prop-types: 0 */ +import { AxiosResponse } from 'axios'; +import { TFunction } from 'i18next'; import { withTranslation } from 'react-i18next'; export const ERROR_LOGIN_VALIDATE_MISSINGUSERNAME = 'konnect.error.login.validate.missingUsername'; @@ -11,9 +13,9 @@ export const ERROR_HTTP_UNEXPECTED_RESPONSE_STATE = 'konnect.error.http.unexpect // Error with values. export class ExtendedError extends Error { - values = undefined; + values: { [key: string]: string | null | undefined | Error | boolean | object | (string | null)[] } | null | undefined | object | AxiosResponse = undefined; - constructor(message, values) { + constructor(message: string, values?: { [key: string]: string | null | undefined | Error | boolean } | null | AxiosResponse) { super(message); if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, ExtendedError); @@ -22,8 +24,10 @@ export class ExtendedError extends Error { } } +export type ErrorType = { id?: null | string, message?: string | null, values?: { [key: string] : string | boolean | Error, }, [key: string]: string | null | undefined | { [key: string] : string | boolean | Error, } } | null ; + // Component to translate error text with values. -function ErrorMessageComponent(props) { +function ErrorMessageComponent(props: { error?: ErrorType | null, t: TFunction, values?: {[key:string]: string | boolean} }): JSX.Element | null { const { error, t, values } = props; if (!error) { @@ -57,7 +61,7 @@ function ErrorMessageComponent(props) { } const f = t; - return f(messageDescriptor.defaultMessage, messageDescriptor.values); + return f(messageDescriptor.defaultMessage ?? "", messageDescriptor.values); } export const ErrorMessage = withTranslation()(ErrorMessageComponent); diff --git a/identifier/src/models/hello.js b/identifier/src/models/hello.ts similarity index 73% rename from identifier/src/models/hello.js rename to identifier/src/models/hello.ts index dab2ad09..f043147b 100644 --- a/identifier/src/models/hello.js +++ b/identifier/src/models/hello.ts @@ -1,12 +1,16 @@ -export function newHelloRequest(flow, query) { - const r = {}; +export type HelloQuery = { scope?: string, client_id?: string, redirect_uri: string, id_token_hint?: string, max_age?: string,claims_scope?: string, prompt?: string } + + + +export function newHelloRequest(flow: string, query: HelloQuery) { + const r:{[key: string]: string} = {}; if (query.prompt) { // TODO(longsleep): Validate prompt values? r.prompt = query.prompt; } - - let selectedFlow = flow; + + let selectedFlow: string | null = flow; switch (flow) { case 'oauth': case 'consent': diff --git a/identifier/src/reducers/common.js b/identifier/src/reducers/common.ts similarity index 64% rename from identifier/src/reducers/common.js rename to identifier/src/reducers/common.ts index 58b49f64..8cddd09a 100644 --- a/identifier/src/reducers/common.js +++ b/identifier/src/reducers/common.ts @@ -5,6 +5,7 @@ import { SERVICE_WORKER_NEW_CONTENT } from '../actions/types'; import queryString from 'query-string'; +import { ErrorType } from '../errors'; const query = queryString.parse(document.location.search); const flow = query.flow || ''; @@ -20,7 +21,17 @@ const defaultPathPrefix = (() => { return pathPrefix; })(); -const defaultState = { +type commonStateType = { + hello: { [key: string]: string | boolean | undefined | null | {[key: string]: string | boolean | string[] | undefined | null } } | null, + branding: { bannerLogo: string, locales: string[] } | null, + error: ErrorType, + flow: string | (string | null)[], + query: queryString.ParsedQuery, + pathPrefix: string, + updateAvailable: boolean +} + +const defaultState:commonStateType = { hello: null, branding: null, error: null, @@ -30,7 +41,8 @@ const defaultState = { pathPrefix: defaultPathPrefix }; -function commonReducer(state = defaultState, action) { + +function commonReducer(state = defaultState, action: {type: string, error?: ErrorType | null, state: commonStateType | undefined, username: string | undefined, displayName: string | undefined, hello: null | undefined | {[key: string]: commonStateType | string | boolean}}) { switch (action.type) { case RECEIVE_ERROR: return Object.assign({}, state, { @@ -51,7 +63,7 @@ function commonReducer(state = defaultState, action) { displayName: action.displayName, details: action.hello }, - branding: action.hello.branding ? action.hello.branding : state.branding + branding: action.hello?.branding ? action.hello.branding : state.branding }); case SERVICE_WORKER_NEW_CONTENT: diff --git a/identifier/src/reducers/index.js b/identifier/src/reducers/index.ts similarity index 100% rename from identifier/src/reducers/index.js rename to identifier/src/reducers/index.ts diff --git a/identifier/src/reducers/login.js b/identifier/src/reducers/login.ts similarity index 66% rename from identifier/src/reducers/login.js rename to identifier/src/reducers/login.ts index 8ad64786..750fe238 100644 --- a/identifier/src/reducers/login.js +++ b/identifier/src/reducers/login.ts @@ -8,13 +8,22 @@ import { RECEIVE_CONSENT, UPDATE_INPUT } from '../actions/types'; +import { ErrorType } from '../errors'; -function loginReducer(state = { + +type loginState = { + loading: string, + username: string, + password: string, + errors: ErrorType +} + +function loginReducer(state:loginState = { loading: '', username: '', password: '', errors: {} -}, action) { +}, action: {errors?: Error | { [key: string]: string | Error | boolean | null }, type: string, success?: boolean, name?: string, value?: string | null}) { switch (action.type) { case RECEIVE_VALIDATE_LOGON: return Object.assign({}, state, { @@ -47,10 +56,17 @@ function loginReducer(state = { }); case UPDATE_INPUT: - delete state.errors[action.name]; - return Object.assign({}, state, { + if(action.name){ + if(state.errors){ + delete state.errors[action.name]; + } + return Object.assign({}, state, { [action.name]: action.value - }); + }); + } + else{ + return state; + } default: return state; diff --git a/identifier/src/render-if.d.ts b/identifier/src/render-if.d.ts new file mode 100644 index 00000000..8636ef39 --- /dev/null +++ b/identifier/src/render-if.d.ts @@ -0,0 +1 @@ +declare module 'render-if'; \ No newline at end of file diff --git a/identifier/src/store.js b/identifier/src/store.js deleted file mode 100644 index 1fde7bfd..00000000 --- a/identifier/src/store.js +++ /dev/null @@ -1,24 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunkMiddleware from 'redux-thunk'; -import { createLogger } from 'redux-logger'; - -import rootReducer from './reducers'; - -const middlewares = [ - thunkMiddleware -]; - -if (process.env.NODE_ENV !== 'development') { // eslint-disable-line no-undef - middlewares.push(createLogger()); // must be last middleware in the chain. -} - -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - -const store = createStore( - rootReducer, - composeEnhancers(applyMiddleware( - ...middlewares, - )) -); - -export default store; diff --git a/identifier/src/store.ts b/identifier/src/store.ts new file mode 100644 index 00000000..1c9c8695 --- /dev/null +++ b/identifier/src/store.ts @@ -0,0 +1,36 @@ +import { createStore, applyMiddleware, compose, AnyAction, Action, EmptyObject } from 'redux'; +import thunkMiddleware, { ThunkDispatch, ThunkMiddleware } from 'redux-thunk'; +import { createLogger } from 'redux-logger'; + +import rootReducer from './reducers'; + +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; + } +} + +type IAppState = ReturnType; + +const middlewares: ThunkMiddleware[] = [ + thunkMiddleware +]; + +if (process.env.NODE_ENV !== 'development') { // eslint-disable-line no-undef + middlewares.push(createLogger()); // must be last middleware in the chain. +} + +const composeEnhancers = (window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] as typeof compose) || compose || compose; + +const store = createStore( + rootReducer, + composeEnhancers(applyMiddleware( + ...middlewares, + )) +); + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch; +export type PromiseDispatch = ThunkDispatch; + +export default store; diff --git a/identifier/src/theme.js b/identifier/src/theme.ts similarity index 97% rename from identifier/src/theme.js rename to identifier/src/theme.ts index d86783a4..1b482ee4 100644 --- a/identifier/src/theme.js +++ b/identifier/src/theme.ts @@ -29,7 +29,6 @@ const theme = createMuiTheme({ }, typography: { fontSize: 12, - useNextVariants: true, button: { textTransform: 'none', }, diff --git a/identifier/src/types.ts b/identifier/src/types.ts new file mode 100644 index 00000000..177ed320 --- /dev/null +++ b/identifier/src/types.ts @@ -0,0 +1,8 @@ +export type StringObject = {[key: string] : string}; +export type ResponseType = {[key: string] : string | null | undefined | boolean}; +export type ObjectType = {[key: string] : undefined | null | string | boolean | { [key: string]: string | object | string[] } | (string | null)[] }; +export type ModelResponseType = {success: boolean , errors: { http: Error }} +export type MappingType = { + mapping: string[], + definitions: { [key: string]: { [key: string]: string } } +} diff --git a/identifier/src/utils.js b/identifier/src/utils.js deleted file mode 100644 index dcbfaaed..00000000 --- a/identifier/src/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function withClientRequestState(obj) { - obj.state = Math.random().toString(36).substring(7); - - return obj; -} diff --git a/identifier/src/utils.ts b/identifier/src/utils.ts new file mode 100644 index 00000000..b8f1d5fe --- /dev/null +++ b/identifier/src/utils.ts @@ -0,0 +1,5 @@ +export function withClientRequestState(obj: {[key: string]: string | string[] | {[key: string]: string | object} | boolean | (string | null)[] } ) { + obj.state = Math.random().toString(36).substring(7); + + return obj; +} diff --git a/identifier/src/version.js b/identifier/src/version.ts similarity index 100% rename from identifier/src/version.js rename to identifier/src/version.ts diff --git a/identifier/yarn.lock b/identifier/yarn.lock index 7d89b3cf..789d6f39 100644 --- a/identifier/yarn.lock +++ b/identifier/yarn.lock @@ -2728,12 +2728,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.19.0": - version: 7.19.4 - resolution: "@babel/runtime@npm:7.19.4" +"@babel/runtime@npm:^7.7.6": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" dependencies: - regenerator-runtime: "npm:^0.13.4" - checksum: 680df8a00a43534c584c0e4cf626071fc7be58749eb693a1cecc137d2e4dbd8da9ce0d2889c9e274dfe0ed42039c03350a7665406c46b8eaee39a4f19f7f3178 + regenerator-runtime: "npm:^0.14.0" + checksum: 9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 languageName: node linkType: hard @@ -3859,6 +3859,13 @@ __metadata: languageName: node linkType: hard +"@types/history@npm:^4.7.11": + version: 4.7.11 + resolution: "@types/history@npm:4.7.11" + checksum: 1da529a3485f3015daf794effa3185493bf7dd2551c26932389c614f5a0aab76ab97645897d1eef9c74ead216a3848fcaa019f165bbd6e4b71da6eff164b4c68 + languageName: node + linkType: hard + "@types/hoist-non-react-statics@npm:^3.3.0": version: 3.3.1 resolution: "@types/hoist-non-react-statics@npm:3.3.1" @@ -4004,6 +4011,27 @@ __metadata: languageName: node linkType: hard +"@types/react-router-dom@npm:^5.3.3": + version: 5.3.3 + resolution: "@types/react-router-dom@npm:5.3.3" + dependencies: + "@types/history": "npm:^4.7.11" + "@types/react": "npm:*" + "@types/react-router": "npm:*" + checksum: 28c4ea48909803c414bf5a08502acbb8ba414669b4b43bb51297c05fe5addc4df0b8fd00e0a9d1e3535ec4073ef38aaafac2c4a2b95b787167d113bc059beff3 + languageName: node + linkType: hard + +"@types/react-router@npm:*": + version: 5.1.20 + resolution: "@types/react-router@npm:5.1.20" + dependencies: + "@types/history": "npm:^4.7.11" + "@types/react": "npm:*" + checksum: 72d78d2f4a4752ec40940066b73d7758a0824c4d0cbeb380ae24c8b1cdacc21a6fc835a99d6849b5b295517a3df5466fc28be038f1040bd870f8e39e5ded43a4 + languageName: node + linkType: hard + "@types/react-transition-group@npm:^4.2.0": version: 4.4.3 resolution: "@types/react-transition-group@npm:4.4.3" @@ -7497,6 +7525,15 @@ __metadata: languageName: node linkType: hard +"history@npm:^5.3.0": + version: 5.3.0 + resolution: "history@npm:5.3.0" + dependencies: + "@babel/runtime": "npm:^7.7.6" + checksum: 52ba685b842ca6438ff11ef459951eb13d413ae715866a8dc5f7c3b1ea0cdeb8db6aabf7254551b85f56abc205e6e2d7e1d5afb36b711b401cdaff4f2cf187e9 + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -7597,12 +7634,12 @@ __metadata: languageName: node linkType: hard -"i18next-browser-languagedetector@npm:^6.1.8": - version: 6.1.8 - resolution: "i18next-browser-languagedetector@npm:6.1.8" +"i18next-browser-languagedetector@npm:^8.0.2": + version: 8.0.2 + resolution: "i18next-browser-languagedetector@npm:8.0.2" dependencies: - "@babel/runtime": "npm:^7.19.0" - checksum: a55e3fb432bbc361c7b37760d3a5496bfe54429d71c65802c7358570d03c04b9788650e377ba551d97f6ed4640b925f674a14164174e17fad035b25958f17cfa + "@babel/runtime": "npm:^7.23.2" + checksum: 2d994fbec7d106da7592d3b22a058f03be59a67e0ef7436bc1b5500dca778dc5eb77df34b4e0a054123d5538ef5f8b5c19cd1d7b862dc3403c4571b48c9cabe8 languageName: node linkType: hard @@ -7711,6 +7748,7 @@ __metadata: "@types/react": "npm:^17.0.70" "@types/react-dom": "npm:^17.0.23" "@types/react-redux": "npm:^7.1.25" + "@types/react-router-dom": "npm:^5.3.3" "@types/redux-logger": "npm:^3.0.12" "@types/validator": "npm:^13" "@typescript-eslint/eslint-plugin": "npm:^6.11.0" @@ -7725,8 +7763,9 @@ __metadata: eslint-config-react-app-bump: "npm:^1.0.16" eslint-plugin-i18next: "npm:^5.2.1" glob: "npm:^8.1.0" + history: "npm:^5.3.0" i18next: "npm:^21.10.0" - i18next-browser-languagedetector: "npm:^6.1.8" + i18next-browser-languagedetector: "npm:^8.0.2" i18next-conv: "npm:^12.1.1" i18next-http-backend: "npm:^1.4.5" i18next-parser: "npm:^5.4.0"