diff --git a/package-lock.json b/package-lock.json index 893550a..0819858 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7329,6 +7329,11 @@ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz", "integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==" }, + "bowser": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.10.0.tgz", + "integrity": "sha512-OCsqTQboTEWWsUjcp5jLSw2ZHsBiv2C105iFs61bOT0Hnwi9p7/uuXdd7mu8RYcarREfdjNN+8LitmEHATsLYg==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index 5c4fd1a..cf4ecd0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@fortawesome/free-solid-svg-icons": "5.8.2", "@fortawesome/react-fontawesome": "0.1.11", "babel-polyfill": "6.26.0", + "bowser": "^2.10.0", "classnames": "2.2.6", "font-awesome": "4.7.0", "form-urlencoded": "4.0.1", diff --git a/src/id-verification/IdVerification.messages.js b/src/id-verification/IdVerification.messages.js index 9dab397..b9db0fe 100644 --- a/src/id-verification/IdVerification.messages.js +++ b/src/id-verification/IdVerification.messages.js @@ -131,6 +131,131 @@ const messages = defineMessages({ defaultMessage: 'It looks like we\'re unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.', description: 'Text indicating that the camera could not be accessed.', }, + 'id.verification.camera.access.failure.temporary.chrome': { + id: 'id.verification.camera.access.failure.temporary.chrome', + defaultMessage: 'To enable camera access in Chrome:', + description: 'Description for the directions on enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.chrome.step1': { + id: 'id.verification.camera.access.failure.temporary.chrome.step1', + defaultMessage: 'Open Chrome.', + description: 'Text for step one of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.chrome.step2': { + id: 'id.verification.camera.access.failure.temporary.chrome.step2', + defaultMessage: 'Navigate to More > Settings.', + description: 'Text for step two of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.chrome.step2.windows': { + id: 'id.verification.camera.access.failure.temporary.chrome.step2.windows', + defaultMessage: 'For Windows: Alt+F, Alt+E, or F10 followed by the spacebar', + description: 'Text for Windows keyboard shortcut in chrome.', + }, + 'id.verification.camera.access.failure.temporary.chrome.step2.mac': { + id: 'id.verification.camera.access.failure.temporary.chrome.step2.mac', + defaultMessage: 'For Mac: Command+,', + description: 'Text for Mac keyboard shortcut in chrome.', + }, + 'id.verification.camera.access.failure.temporary.chrome.step3': { + id: 'id.verification.camera.access.failure.temporary.chrome.step3', + defaultMessage: 'Under the "Privacy and security" tab, select "Site Settings" and then "Camera."', + description: 'Text for step three of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.chrome.step4': { + id: 'id.verification.camera.access.failure.temporary.chrome.step4', + defaultMessage: 'Under "Blocked," find "edx.org" and select it.', + description: 'Text for step four of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.chrome.step5': { + id: 'id.verification.camera.access.failure.temporary.chrome.step5', + defaultMessage: 'In the "Permissions" section, update the camera permissions to "Allow."', + description: 'Text for step five of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.ie11': { + id: 'id.verification.camera.access.failure.temporary.ie11', + defaultMessage: 'To enable camera access in Internet Explorer:', + description: 'Description for the directions on enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.ie11.step1': { + id: 'id.verification.camera.access.failure.temporary.ie11.step1', + defaultMessage: 'Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.', + description: 'Text for step one of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.ie11.step2': { + id: 'id.verification.camera.access.failure.temporary.ie11.step2', + defaultMessage: 'Select the "Camera and Mic" tab, and then select the "Camera and Microphone Settings by Site" button.', + description: 'Text for step two of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.ie11.step3': { + id: 'id.verification.camera.access.failure.temporary.ie11.step3', + defaultMessage: 'Choose "edx.org" from the list of websites and change the permissions by selecting "Allow" in the dropdown menu.', + description: 'Text for step three of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox': { + id: 'id.verification.camera.access.failure.temporary.firefox', + defaultMessage: 'To enable camera access in Firefox:', + description: 'Description for the directions on enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox.step1': { + id: 'id.verification.camera.access.failure.temporary.firefox.step1', + defaultMessage: 'Open Firefox.', + description: 'Text for step one of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox.step2': { + id: 'id.verification.camera.access.failure.temporary.firefox.step2', + defaultMessage: 'Enter "about:preferences" in the URL bar.', + description: 'Text for step two of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox.step3': { + id: 'id.verification.camera.access.failure.temporary.firefox.step3', + defaultMessage: 'Select the "Privacy & Security" tab, and navigate to the "Permissions" section.', + description: 'Text for step three of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox.step4': { + id: 'id.verification.camera.access.failure.temporary.firefox.step4', + defaultMessage: 'Next to "Camera," select the "Settings…" button.', + description: 'Text for step four of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox.step5': { + id: 'id.verification.camera.access.failure.temporary.firefox.step5', + defaultMessage: 'In the search bar, enter "edx.org."', + description: 'Text for step five of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox.step6': { + id: 'id.verification.camera.access.failure.temporary.firefox.step6', + defaultMessage: 'In the status column for "edx.org," select "Allow" from the drop down.', + description: 'Text for step six of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.firefox.step7': { + id: 'id.verification.camera.access.failure.temporary.firefox.step7', + defaultMessage: 'Select "Save Changes."', + description: 'Text for step seven of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.safari': { + id: 'id.verification.camera.access.failure.temporary.safari', + defaultMessage: 'To enable camera access in Safari:', + description: 'Description for the directions on enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.safari.step1': { + id: 'id.verification.camera.access.failure.temporary.safari.step1', + defaultMessage: 'Open Safari.', + description: 'Text for step one of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.safari.step2': { + id: 'id.verification.camera.access.failure.temporary.safari.step2', + defaultMessage: 'Click on the Safari app menu, then select "Preferences." You can also use Command+, as a keyboard shortcut.', + description: 'Text for step two of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.safari.step3': { + id: 'id.verification.camera.access.failure.temporary.safari.step3', + defaultMessage: 'Select the "Websites" tab and then select "Camera."', + description: 'Text for step three of enabling camera access.', + }, + 'id.verification.camera.access.failure.temporary.safari.step4': { + id: 'id.verification.camera.access.failure.temporary.safari.step4', + defaultMessage: 'Select "edx.org" and change the camera permissions to "Allow."', + description: 'Text for step four of enabling camera access.', + }, 'id.verification.photo.tips.title': { id: 'id.verification.photo.tips.title', defaultMessage: 'Helpful Photo Tips', diff --git a/src/id-verification/panels/EnableCameraDirectionsPanel.jsx b/src/id-verification/panels/EnableCameraDirectionsPanel.jsx new file mode 100644 index 0000000..100749a --- /dev/null +++ b/src/id-verification/panels/EnableCameraDirectionsPanel.jsx @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import messages from '../IdVerification.messages'; + +export function EnableCameraDirectionsPanel(props) { + if (props.browserName === 'Internet Explorer') { + return ( + <> +
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11'])}
+
    +
  1. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step1'])}
  2. +
  3. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step2'])}
  4. +
  5. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step3'])}
  6. +
+ + ); + } else if (props.browserName === 'Chrome') { + return ( + <> +
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome'])}
+
    +
  1. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step1'])}
  2. +
  3. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2'])}
  4. + +
  5. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step3'])}
  6. +
  7. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step4'])}
  8. +
  9. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step5'])}
  10. +
+ + ); + } else if (props.browserName === 'Firefox') { + return ( + <> +
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox'])}
+
    +
  1. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step1'])}
  2. +
  3. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step2'])}
  4. +
  5. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step3'])}
  6. +
  7. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step4'])}
  8. +
  9. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step5'])}
  10. +
  11. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step6'])}
  12. +
  13. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step7'])}
  14. +
+ + ); + } else if (props.browserName === 'Safari') { + return ( + <> +
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari'])}
+
    +
  1. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step1'])}
  2. +
  3. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step2'])}
  4. +
  5. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step3'])}
  6. +
  7. {props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step4'])}
  8. +
+ + ); + } + return <>; +} + +EnableCameraDirectionsPanel.propTypes = { + intl: intlShape.isRequired, + browserName: PropTypes.string.isRequired, +}; + +export default injectIntl(EnableCameraDirectionsPanel); diff --git a/src/id-verification/panels/RequestCameraAccessPanel.jsx b/src/id-verification/panels/RequestCameraAccessPanel.jsx index 753d047..9eb0116 100644 --- a/src/id-verification/panels/RequestCameraAccessPanel.jsx +++ b/src/id-verification/panels/RequestCameraAccessPanel.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useContext } from 'react'; import { Link } from 'react-router-dom'; +import Bowser from 'bowser'; import { getConfig } from '@edx/frontend-platform'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; @@ -7,6 +8,7 @@ import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/ import { useNextPanelSlug } from '../routing-utilities'; import BasePanel from './BasePanel'; import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext'; +import { EnableCameraDirectionsPanel } from './EnableCameraDirectionsPanel'; import messages from '../IdVerification.messages'; @@ -16,6 +18,7 @@ function RequestCameraAccessPanel(props) { const panelSlug = 'request-camera-access'; const nextPanelSlug = useNextPanelSlug(panelSlug); const { tryGetUserMedia, mediaAccess, userId } = useContext(IdVerificationContext); + const browserName = Bowser.parse(window.navigator.userAgent).browser.name; useEffect(() => { if (mediaAccess === MEDIA_ACCESS.UNSUPPORTED) { @@ -89,10 +92,11 @@ function RequestCameraAccessPanel(props) { )} {[MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess) && ( -
+

{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary'])}

+
{props.intl.formatMessage(messages[returnText])} diff --git a/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx b/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx index 17d36ee..18842bb 100644 --- a/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx +++ b/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Router } from 'react-router-dom'; +import Bowser from 'bowser'; import { createMemoryHistory } from 'history'; import { render, screen, cleanup, act, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; @@ -11,6 +12,8 @@ jest.mock('@edx/frontend-platform/analytics', () => ({ sendTrackEvent: jest.fn(), })); +jest.mock('bowser'); + const history = createMemoryHistory(); const IntlRequestCameraAccessPanel = injectIntl(RequestCameraAccessPanel); @@ -30,6 +33,7 @@ describe('RequestCameraAccessPanel', () => { it('renders correctly with media access pending', async () => { contextValue.mediaAccess = 'pending'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } }); await act(async () => render(( @@ -45,6 +49,7 @@ describe('RequestCameraAccessPanel', () => { it('renders correctly with media access granted and routes to PortraitPhotoContextPanel', async () => { contextValue.mediaAccess = 'granted'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } }); await act(async () => render(( @@ -63,6 +68,7 @@ describe('RequestCameraAccessPanel', () => { it('renders correctly with media access denied', async () => { contextValue.mediaAccess = 'denied'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } }); await act(async () => render(( @@ -78,6 +84,7 @@ describe('RequestCameraAccessPanel', () => { it('renders correctly with media access unsupported', async () => { contextValue.mediaAccess = 'unsupported'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } }); await act(async () => render(( @@ -90,4 +97,68 @@ describe('RequestCameraAccessPanel', () => { const text = await screen.findByTestId('camera-access-failure'); expect(text).toHaveTextContent(/It looks like we're unable to access your camera./); }); + + it('renders correct directions for Chrome with media access denied', async () => { + contextValue.mediaAccess = 'denied'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Chrome' } }); + await act(async () => render(( + + + + + + + + ))); + const text = await screen.findByTestId('camera-failure-instructions'); + expect(text).toHaveTextContent(/Open Chrome./); + }); + + it('renders correct directions for Firefox with media access denied', async () => { + contextValue.mediaAccess = 'denied'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Firefox' } }); + await act(async () => render(( + + + + + + + + ))); + const text = await screen.findByTestId('camera-failure-instructions'); + expect(text).toHaveTextContent(/Open Firefox./); + }); + + it('renders correct directions for Safari with media access denied', async () => { + contextValue.mediaAccess = 'denied'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Safari' } }); + await act(async () => render(( + + + + + + + + ))); + const text = await screen.findByTestId('camera-failure-instructions'); + expect(text).toHaveTextContent(/Open Safari./); + }); + + it('renders correct directions for IE11 with media access denied', async () => { + contextValue.mediaAccess = 'denied'; + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Internet Explorer' } }); + await act(async () => render(( + + + + + + + + ))); + const text = await screen.findByTestId('camera-failure-instructions'); + expect(text).toHaveTextContent(/Open the Flash Player/); + }); });