Merge pull request #527 from edx/bseverino/idv-redirect

[MST-1104] Redirect user to original location after IDV
This commit is contained in:
Michael Roytman
2021-10-20 14:33:29 -04:00
committed by GitHub
9 changed files with 110 additions and 57 deletions

View File

@@ -50,6 +50,7 @@
"formdata-polyfill": "4.0.10",
"history": "4.10.1",
"jslib-html5-camera-photo": "3.1.8",
"lodash.camelcase": "^4.3.0",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
"lodash.get": "4.4.2",

View File

@@ -69,7 +69,7 @@ function NameChangeModal({
useEffect(() => {
if (saveState === 'complete') {
handleClose();
push('/id-verification');
push(`/id-verification?next=${encodeURIComponent('account/settings')}`);
}
}, [saveState]);

View File

@@ -29,3 +29,27 @@ export function useAsyncCall(asyncFunc) {
return data;
}
// Redirect the user to their original location based on session storage
export function useRedirect() {
const [redirect, setRedirect] = useState({
location: 'dashboard',
text: 'id.verification.return.dashboard',
});
useEffect(() => {
if (sessionStorage.getItem('courseId')) {
setRedirect({
location: `courses/${sessionStorage.getItem('courseId')}`,
text: 'id.verification.return.course',
});
} else if (sessionStorage.getItem('next')) {
setRedirect({
location: sessionStorage.getItem('next'),
text: 'id.verification.return.generic',
});
}
}, []);
return redirect;
}

View File

@@ -701,6 +701,11 @@ const messages = defineMessages({
defaultMessage: 'Return to Course',
description: 'Return to the course which ID verification was accessed from.',
},
'id.verification.return.generic': {
id: 'id.verification.return.generic',
defaultMessage: 'Return',
description: 'Button to return to the user\'s original location.',
},
'id.verification.photo.upload.help.title': {
id: 'id.verification.photo.upload.help.title',
defaultMessage: 'Upload a Photo Instead',

View File

@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import {
Route, Switch, Redirect, useRouteMatch, useLocation,
} from 'react-router-dom';
import camelCase from 'lodash.camelcase';
import qs from 'qs';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Modal, Button } from '@edx/paragon';
@@ -32,16 +33,16 @@ function IdVerificationPage(props) {
const [isModalOpen, setIsModalOpen] = useState(false);
// Course run key is passed as a query string
// Save query params in order to route back to the correct location later
useEffect(() => {
if (search) {
const parsed = qs.parse(search, {
const parsedQueryParams = qs.parse(search, {
ignoreQueryPrefix: true,
interpretNumericEntities: true,
});
if (Object.prototype.hasOwnProperty.call(parsed, 'course_id') && parsed.course_id) {
sessionStorage.setItem('courseRunKey', parsed.course_id);
}
Object.entries(parsedQueryParams).forEach(([key, value]) => {
sessionStorage.setItem(camelCase(key), value);
});
}
}, [search]);

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { 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';
import { useRedirect } from '../../hooks';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import IdVerificationContext, { MEDIA_ACCESS } from '../IdVerificationContext';
@@ -14,8 +15,7 @@ import { UnsupportedCameraDirectionsPanel } from './UnsupportedCameraDirectionsP
import messages from '../IdVerification.messages';
function RequestCameraAccessPanel(props) {
const [returnUrl, setReturnUrl] = useState('dashboard');
const [returnText, setReturnText] = useState('id.verification.return.dashboard');
const { location: returnUrl, text: returnText } = useRedirect();
const panelSlug = 'request-camera-access';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const {
@@ -38,15 +38,6 @@ function RequestCameraAccessPanel(props) {
}
}, [mediaAccess, userId]);
// If the user accessed IDV through a course,
// link back to that course rather than the dashboard
useEffect(() => {
if (sessionStorage.getItem('courseRunKey')) {
setReturnUrl(`courses/${sessionStorage.getItem('courseRunKey')}`);
setReturnText('id.verification.return.course');
}
}, []);
const getTitle = () => {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return props.intl.formatMessage(messages['id.verification.camera.access.title.success']);
@@ -57,7 +48,7 @@ function RequestCameraAccessPanel(props) {
return props.intl.formatMessage(messages['id.verification.camera.access.title']);
};
const returnToDashboardLink = (
const returnLink = (
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}>
{props.intl.formatMessage(messages[returnText])}
</a>
@@ -114,7 +105,7 @@ function RequestCameraAccessPanel(props) {
</p>
<EnableCameraDirectionsPanel browserName={browserName} intl={props.intl} />
<div className="action-row">
{optimizelyExperimentName ? nextButtonLink : returnToDashboardLink}
{optimizelyExperimentName ? nextButtonLink : returnLink}
</div>
</div>
)}
@@ -126,7 +117,7 @@ function RequestCameraAccessPanel(props) {
</p>
<UnsupportedCameraDirectionsPanel browserName={browserName} intl={props.intl} />
<div className="action-row">
{optimizelyExperimentName ? nextButtonLink : returnToDashboardLink}
{optimizelyExperimentName ? nextButtonLink : returnLink}
</div>
</div>
)}

View File

@@ -1,15 +1,18 @@
import React, { useEffect, useContext } from 'react';
import React, { useContext, useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import BasePanel from './BasePanel';
import { useRedirect } from '../../hooks';
import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
import BasePanel from './BasePanel';
function SubmittedPanel(props) {
const { userId } = useContext(IdVerificationContext);
const { location: returnUrl, text: returnText } = useRedirect();
const panelSlug = 'submitted';
useEffect(() => {
@@ -29,10 +32,10 @@ function SubmittedPanel(props) {
</p>
<a
className="btn btn-primary"
href={`${getConfig().LMS_BASE_URL}/dashboard`}
href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}
data-testid="return-button"
>
{props.intl.formatMessage(messages['id.verification.return.dashboard'])}
{props.intl.formatMessage(messages[returnText])}
</a>
</BasePanel>
);

View File

@@ -41,36 +41,8 @@ describe('IdVerificationPage', () => {
intl: {},
};
it('does not store irrelevant query params', async () => {
history.push('/?test=irrelevant');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
</Provider>
</IntlProvider>
</Router>
)));
expect(sessionStorage.setItem).toHaveBeenCalledTimes(0);
});
it('does not store empty course_id', async () => {
history.push('/?course_id=');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
</Provider>
</IntlProvider>
</Router>
)));
expect(sessionStorage.setItem).toHaveBeenCalledTimes(0);
});
it('decodes and stores course_id', async () => {
history.push('/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course');
history.push(`/?course_id=${encodeURIComponent('course-v1:edX+DemoX+Demo_Course')}`);
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
@@ -81,8 +53,25 @@ describe('IdVerificationPage', () => {
</Router>
)));
expect(sessionStorage.setItem).toHaveBeenCalledWith(
'courseRunKey',
'courseId',
'course-v1:edX+DemoX+Demo_Course',
);
});
it('stores `next` value', async () => {
history.push('/?next=dashboard');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
</Provider>
</IntlProvider>
</Router>
)));
expect(sessionStorage.setItem).toHaveBeenCalledWith(
'next',
'dashboard',
);
});
});

View File

@@ -28,14 +28,20 @@ describe('SubmittedPanel', () => {
};
beforeEach(() => {
global.sessionStorage.getItem = jest.fn();
const mockStorage = {};
global.Storage.prototype.setItem = jest.fn((key, value) => {
mockStorage[key] = value;
});
global.Storage.prototype.getItem = jest.fn(key => mockStorage[key]);
});
afterEach(() => {
global.Storage.prototype.setItem.mockReset();
global.Storage.prototype.getItem.mockReset();
cleanup();
});
it('links to dashboard without courseRunKey', async () => {
it('links to dashboard without courseId or next value', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
@@ -47,5 +53,38 @@ describe('SubmittedPanel', () => {
)));
const button = await screen.findByTestId('return-button');
expect(button).toHaveTextContent(/Return to Your Dashboard/);
expect(button).toHaveAttribute('href', `${process.env.LMS_BASE_URL}/dashboard`);
});
it('links to course when courseId is stored', async () => {
sessionStorage.setItem('courseId', 'course-v1:edX+DemoX+Demo_Course');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSubmittedPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('return-button');
expect(button).toHaveTextContent(/Return to Course/);
expect(button).toHaveAttribute('href', `${process.env.LMS_BASE_URL}/courses/course-v1:edX+DemoX+Demo_Course`);
});
it('links to specified page when `next` value is provided', async () => {
sessionStorage.setItem('next', 'some_page');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSubmittedPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('return-button');
expect(button).toHaveTextContent(/Return/);
expect(button).toHaveAttribute('href', `${process.env.LMS_BASE_URL}/some_page`);
});
});