diff --git a/src/components/WebexInMeeting/WebexInMeeting.jsx b/src/components/WebexInMeeting/WebexInMeeting.jsx index c85ada97a..073effb4f 100644 --- a/src/components/WebexInMeeting/WebexInMeeting.jsx +++ b/src/components/WebexInMeeting/WebexInMeeting.jsx @@ -1,12 +1,21 @@ -import React, {useEffect, useState} from 'react'; +import React, {useContext, useEffect, useState} from 'react'; import PropTypes from 'prop-types'; +import {MeetingState} from '@webex/component-adapter-interfaces'; import Banner from '../generic/Banner/Banner'; import WebexLocalMedia from '../WebexLocalMedia/WebexLocalMedia'; import WebexRemoteMedia from '../WebexRemoteMedia/WebexRemoteMedia'; import webexComponentClasses from '../helpers'; -import {useElementDimensions, useMeeting, useRef} from '../hooks'; +import { + AdapterContext, + useElementDimensions, + useMeeting, + useRef, +} from '../hooks'; import {TABLET, DESKTOP, DESKTOP_LARGE} from '../breakpoints'; +import {Modal} from '../generic'; +import WebexMeetingGuestAuthentication from '../WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication'; +import WebexMeetingHostAuthentication from '../WebexMeetingHostAuthentication/WebexMeetingHostAuthentication'; /** * Webex In-Meeting component displays the remote stream plus @@ -17,15 +26,23 @@ import {TABLET, DESKTOP, DESKTOP_LARGE} from '../breakpoints'; * @param {string} props.layout Layout to apply on remote video * @param {string} props.meetingID ID of the meeting for which to show media * @param {object} props.style Custom style to apply + * @param {object} props.setEscapedAuthentication Set true if user escapes authentication * @returns {object} JSX of the component */ export default function WebexInMeeting({ - className, layout, meetingID, style, + className, layout, meetingID, style, setEscapedAuthentication, }) { - const {remoteShare, localShare} = useMeeting(meetingID); + const adapter = useContext(AdapterContext); + const { + remoteShare, + localShare, + passwordRequired, + state, + } = useMeeting(meetingID); const meetingRef = useRef(); const {width, height} = useElementDimensions(meetingRef); const [maxWidth, setMaxWidth] = useState('none'); + const [authModal, setAuthModal] = useState('guest'); const localMediaType = localShare?.stream ? 'screen' : 'video'; const [cssClasses, sc] = webexComponentClasses('in-meeting', className, { tablet: width >= TABLET && width < DESKTOP, @@ -33,13 +50,39 @@ export default function WebexInMeeting({ 'desktop-xl': width >= DESKTOP_LARGE, 'remote-sharing': remoteShare !== null, }); + const {NOT_JOINED} = MeetingState; useEffect(() => { setMaxWidth(height ? (height * 16) / 9 : 'none'); }, [height]); + const onCloseHandler = () => { + adapter.meetingsAdapter.clearPasswordRequiredFlag(meetingID); + setEscapedAuthentication(true); + }; + return (
+ {passwordRequired && state === NOT_JOINED && ( + setAuthModal('guest'))} + ariaLabel={authModal === 'guest' ? 'Meeting guest authentication' : 'Meeting host authentication'} + > + { + authModal === 'guest' + ? ( + setAuthModal('host')} + /> + ) + : + } + + )}
@@ -54,10 +97,12 @@ WebexInMeeting.propTypes = { layout: PropTypes.string, meetingID: PropTypes.string.isRequired, style: PropTypes.shape(), + setEscapedAuthentication: PropTypes.func, }; WebexInMeeting.defaultProps = { className: '', layout: undefined, style: undefined, + setEscapedAuthentication: undefined, }; diff --git a/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.jsx b/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.jsx index 251162d11..b050b59e3 100644 --- a/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.jsx +++ b/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.jsx @@ -1,10 +1,10 @@ -import React, {useState, useContext} from 'react'; +import React, {useState, useContext, useRef} from 'react'; import PropTypes from 'prop-types'; import webexComponentClasses from '../helpers'; -import {Button} from '../generic'; +import {Button, Icon} from '../generic'; import {PasswordInput, TextInput} from '../inputs'; import {PHONE_LARGE} from '../breakpoints'; -import {useElementDimensions, useMeeting, useRef} from '../hooks'; +import {useElementDimensions, useMeeting} from '../hooks'; import {AdapterContext} from '../hooks/contexts'; import Spinner from '../generic/Spinner/Spinner'; import CaptchaInput from '../inputs/CaptchaInput/CaptchaInput'; @@ -12,9 +12,10 @@ import CaptchaInput from '../inputs/CaptchaInput/CaptchaInput'; const HINTS = { logo: 'Webex by Cisco logo', name: 'Your name appears in the participant list. Skip this optional field to use the name provided by the system.', - password: 'The password is provided in the invitation for a scheduled meeting, or from the host.', + password: 'The password is provided in the invitation for a scheduled meeting, or from the host.', captcha: 'The captcha is required', captchaImage: 'Captcha Image', + captchaRefresh: 'Click to refresh the captcha image', buttonHint: 'Start meeting. Start the meeting after entering the required information.', hostLink: 'Click to go to a new screen where the meeting host can enter the host key.', }; @@ -22,7 +23,7 @@ const HINTS = { /** * Helper function for checking name format * - * @param {string} name Input value + * @param {string} name Input value * @returns {string} returns the error if exists */ function getNameError(name) { @@ -38,21 +39,23 @@ function getNameError(name) { /** * Webex Meeting Guest Authentication component * - * @param {object} props Data passed to the component - * @param {string} props.className Custom CSS class to apply - * @param {string} props.meetingID ID of the meeting - * @param {object} props.style Custom style to apply - * @param {Function} props.switchToHostModal A callback function to switch from guest form to host form + * @param {object} props Data passed to the component + * @param {string} props.className Custom CSS class to apply + * @param {string} props.meetingID ID of the meeting + * @param {object} props.style Custom style to apply + * @param {Function} props.switchToHostModal A callback function to switch from guest form to host form * @returns {object} JSX of the component */ export default function WebexMeetingGuestAuthentication({ className, meetingID, style, switchToHostModal, }) { - const [name, setName] = useState(); + const [name, setName] = useState(''); const [password, setPassword] = useState(''); - const [nameError, setNameError] = useState(); + const [nameError, setNameError] = useState(''); const [captcha, setCaptcha] = useState(''); - const {ID, failureReason, invalidPassword, requiredCaptcha} = useMeeting(meetingID); + const { + ID, failureReason, invalidPassword, requiredCaptcha, + } = useMeeting(meetingID); const [isJoining, setIsJoining] = useState(false); const adapter = useContext(AdapterContext); const ref = useRef(); @@ -79,7 +82,9 @@ export default function WebexMeetingGuestAuthentication({ const joinMeeting = () => { setIsJoining(true); - adapter.meetingsAdapter.joinMeeting(ID, {name, password, captcha}).finally(() => setIsJoining(false)); + adapter.meetingsAdapter + .joinMeeting(ID, {name, password, captcha}) + .finally(() => setIsJoining(false)); }; const handleNameChange = (value) => { @@ -98,7 +103,7 @@ export default function WebexMeetingGuestAuthentication({ }; const refreshCaptcha = () => { - adapter.meetingsAdapter.refreshCaptcha(); + adapter.meetingsAdapter.refreshCaptcha(ID); }; const handleHostClick = (event) => { @@ -141,7 +146,19 @@ export default function WebexMeetingGuestAuthentication({ /> {requiredCaptcha && requiredCaptcha.verificationImageURL && (
- captcha +
+ captcha + +
)} @@ -162,7 +179,7 @@ export default function WebexMeetingGuestAuthentication({ onClick={joinMeeting} isDisabled={isStartButtonDisabled} ariaLabel={HINTS.buttonHint} - tabIndex={104} + tabIndex={105} > {isJoining && } {isJoining ? 'Starting meeting...' : 'Start meeting'} @@ -172,7 +189,7 @@ export default function WebexMeetingGuestAuthentication({ Hosting the meeting? {' '} {/* eslint-disable-next-line */} - Enter host key. + Enter host key.
); diff --git a/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.scss b/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.scss index 4cfaca411..aef5a4ea5 100644 --- a/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.scss +++ b/src/components/WebexMeetingGuestAuthentication/WebexMeetingGuestAuthentication.scss @@ -49,6 +49,15 @@ $C: #{$WEBEX_COMPONENTS_CLASS_PREFIX}-meeting-guest-authentication; align-items: center; } + .#{$C}__captcha-refresh-button { + align-items: center; + } + + .#{$C}__captcha-buttons { + display: flex; + flex-direction: row; + } + .#{$C}__start-button-spinner { margin-right: 0.5rem; } diff --git a/src/components/WebexMeetingGuestAuthentication/__snapshots__/WebexMeetingGuestAuthentication.stories.storyshot b/src/components/WebexMeetingGuestAuthentication/__snapshots__/WebexMeetingGuestAuthentication.stories.storyshot index 3b9c46390..5e2041781 100644 --- a/src/components/WebexMeetingGuestAuthentication/__snapshots__/WebexMeetingGuestAuthentication.stories.storyshot +++ b/src/components/WebexMeetingGuestAuthentication/__snapshots__/WebexMeetingGuestAuthentication.stories.storyshot @@ -49,6 +49,7 @@ Array [ className="wxc-input-field__form-control" > +
@@ -134,7 +137,7 @@ Array [ className="wxc-meeting-guest-authentication__host-hyperlink" href="#" onClick={[Function]} - tabIndex={105} + tabIndex={106} > Enter host key. diff --git a/src/components/generic/Icon/Icon.jsx b/src/components/generic/Icon/Icon.jsx index def483fe0..44c77a800 100644 --- a/src/components/generic/Icon/Icon.jsx +++ b/src/components/generic/Icon/Icon.jsx @@ -30,6 +30,7 @@ import { PtoPresenceIcon, QuietHoursPresenceIcon, RecentsPresenceIcon, + RefreshIcon, RemoteMediaErrorIcon, SettingsIcon, ShareScreenFilledIcon, @@ -72,6 +73,7 @@ const icons = { 'pto-presence': PtoPresenceIcon, 'quiet-hours-presence': QuietHoursPresenceIcon, 'recents-presence': RecentsPresenceIcon, + refresh: RefreshIcon, settings: SettingsIcon, 'share-screen-presence-stroke': ShareScreenIcon, 'share-screen-filled': ShareScreenFilledIcon, diff --git a/src/components/icons/RefreshIcon.jsx b/src/components/icons/RefreshIcon.jsx new file mode 100644 index 000000000..bdfcfc0d8 --- /dev/null +++ b/src/components/icons/RefreshIcon.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * Refresh SVG Icon + * + * @param {object} props Data passed to the component + * @param {number} props.size Width and height of the icon + * @param {string} props.className Additional className for the component + * @param {object} props.style Inline style object for the component + * @returns {object} JSX of the icon + * + */ +export default function RefreshIcon({size, className, style}) { + return ( + + + + + ); +} + +RefreshIcon.propTypes = { + size: PropTypes.number, + className: PropTypes.string, + style: PropTypes.shape(), +}; + +RefreshIcon.defaultProps = { + size: 24, + className: '', + style: {}, +}; diff --git a/src/components/icons/index.jsx b/src/components/icons/index.jsx index 8105ec3c5..946ce5249 100644 --- a/src/components/icons/index.jsx +++ b/src/components/icons/index.jsx @@ -27,6 +27,7 @@ export {default as ParticipantListFilledIcon} from './ParticipantListFilledIcon' export {default as PtoPresenceIcon} from './PtoPresenceIcon'; export {default as QuietHoursPresenceIcon} from './QuietHoursPresenceIcon'; export {default as RecentsPresenceIcon} from './RecentsPresenceIcon'; +export {default as RefreshIcon} from './RefreshIcon'; export {default as RemoteMediaErrorIcon} from './RemoteMediaErrorIcon'; export {default as SettingsIcon} from './SettingsIcon'; export {default as ShareScreenFilledIcon} from './ShareScreenFilledIcon';