Skip to content

Commit

Permalink
feat(meetings): add-refresh-captcha-flow
Browse files Browse the repository at this point in the history
  • Loading branch information
ShreyasSharma28 authored and Shreyas281299 committed Jul 4, 2024
1 parent b354ab4 commit f673183
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 26 deletions.
53 changes: 49 additions & 4 deletions src/components/WebexInMeeting/WebexInMeeting.jsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,29 +26,63 @@ 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,
desktop: width >= DESKTOP && width < DESKTOP_LARGE,
'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 (
<div ref={meetingRef} className={cssClasses} style={style}>
{passwordRequired && state === NOT_JOINED && (
<Modal
onClose={onCloseHandler}
otherClassName={[sc('authentication')]}
onBack={authModal === 'host' && (() => setAuthModal('guest'))}
ariaLabel={authModal === 'guest' ? 'Meeting guest authentication' : 'Meeting host authentication'}
>
{
authModal === 'guest'
? (
<WebexMeetingGuestAuthentication
meetingID={meetingID}
className={sc('authentication-guest')}
switchToHostModal={() => setAuthModal('host')}
/>
)
: <WebexMeetingHostAuthentication meetingID={meetingID} className={sc('authentication-host')} />
}
</Modal>
)}
<div style={{maxWidth}} className={sc('media-container')}>
<WebexRemoteMedia className={sc('remote-media-in-meeting')} layout={layout} meetingID={meetingID} />
<WebexLocalMedia className={sc('local-media-in-meeting')} meetingID={meetingID} mediaType={localMediaType} />
Expand All @@ -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,
};
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
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';

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.',
};

/**
* 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) {
Expand All @@ -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();
Expand All @@ -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) => {
Expand All @@ -98,7 +103,7 @@ export default function WebexMeetingGuestAuthentication({
};

const refreshCaptcha = () => {
adapter.meetingsAdapter.refreshCaptcha();
adapter.meetingsAdapter.refreshCaptcha(ID);
};

const handleHostClick = (event) => {
Expand Down Expand Up @@ -141,7 +146,19 @@ export default function WebexMeetingGuestAuthentication({
/>
{requiredCaptcha && requiredCaptcha.verificationImageURL && (
<div className={sc('captcha-image')} aria-label={HINTS.captchaImage}>
<img src={requiredCaptcha.verificationImageURL} alt="captcha" />
<div className={sc('captcha-buttons')}>
<img src={requiredCaptcha.verificationImageURL} alt="captcha" />
<Button
type="primary"
className={sc('captcha-refresh-button')}
size={28}
onClick={refreshCaptcha}
ariaLabel={HINTS.captchaRefresh}
tabIndex={103}
>
<Icon name="refresh" />
</Button>
</div>
<CaptchaInput
className={sc('input')}
type="captcha"
Expand All @@ -151,7 +168,7 @@ export default function WebexMeetingGuestAuthentication({
error={captchaError}
label="Enter Captcha"
ariaLabel={HINTS.captcha}
tabIndex={103}
tabIndex={104}
/>
</div>
)}
Expand All @@ -162,7 +179,7 @@ export default function WebexMeetingGuestAuthentication({
onClick={joinMeeting}
isDisabled={isStartButtonDisabled}
ariaLabel={HINTS.buttonHint}
tabIndex={104}
tabIndex={105}
>
{isJoining && <Spinner className={sc('start-button-spinner')} size={18} />}
{isJoining ? 'Starting meeting...' : 'Start meeting'}
Expand All @@ -172,7 +189,7 @@ export default function WebexMeetingGuestAuthentication({
Hosting the meeting?
{' '}
{/* eslint-disable-next-line */}
<a href="#" tabIndex={105} className={sc('host-hyperlink')} onClick={handleHostClick} aria-label={HINTS.hostLink}>Enter host key.</a>
<a href="#" tabIndex={106} className={sc('host-hyperlink')} onClick={handleHostClick} aria-label={HINTS.hostLink}>Enter host key.</a>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Array [
className="wxc-input-field__form-control"
>
<input
aria-errormessage=""
aria-invalid="false"
aria-label="Your name appears in the participant list. Skip this optional field to use the name provided by the system."
aria-labelledby="wxc-0-label"
Expand All @@ -62,9 +63,11 @@ Array [
required={false}
tabIndex={101}
type="text"
value=""
/>
</div>
</div>

</div>
<div
className="wxc wxc-label wxc wxc-input-field wxc wxc-password-input wxc-meeting-guest-authentication__input wxc-input-field--has-right-icon"
Expand All @@ -89,7 +92,7 @@ Array [
<input
aria-errormessage=""
aria-invalid="false"
aria-label="The password is provided in the invitation for a scheduled meeting, or from the host."
aria-label="The password is provided in the invitation for a scheduled meeting, or from the host."
aria-labelledby="wxc-0-label"
autoFocus={false}
className="wxc-input-field__input"
Expand All @@ -116,7 +119,7 @@ Array [
"height": 28,
}
}
tabIndex={104}
tabIndex={105}
type="button"
wxc-disabled="true"
>
Expand All @@ -134,7 +137,7 @@ Array [
className="wxc-meeting-guest-authentication__host-hyperlink"
href="#"
onClick={[Function]}
tabIndex={105}
tabIndex={106}
>
Enter host key.
</a>
Expand Down
2 changes: 2 additions & 0 deletions src/components/generic/Icon/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
PtoPresenceIcon,
QuietHoursPresenceIcon,
RecentsPresenceIcon,
RefreshIcon,
RemoteMediaErrorIcon,
SettingsIcon,
ShareScreenFilledIcon,
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions src/components/icons/RefreshIcon.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg width={size || 24} height={size || 24} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className={`wxc-icon ${className}`} style={style}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M17.65 6.35A7.95 7.95 0 0 0 12 4a8 8 0 1 0 7.9 9.9h-2.1a6 6 0 1 1-1.45-6.45L13 11h7V4l-2.35 2.35z" fill="#FFFFFF" />
</svg>
);
}

RefreshIcon.propTypes = {
size: PropTypes.number,
className: PropTypes.string,
style: PropTypes.shape(),
};

RefreshIcon.defaultProps = {
size: 24,
className: '',
style: {},
};
1 change: 1 addition & 0 deletions src/components/icons/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit f673183

Please sign in to comment.