Merge pull request #527 from edx/bseverino/idv-redirect
[MST-1104] Redirect user to original location after IDV
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -69,7 +69,7 @@ function NameChangeModal({
|
||||
useEffect(() => {
|
||||
if (saveState === 'complete') {
|
||||
handleClose();
|
||||
push('/id-verification');
|
||||
push(`/id-verification?next=${encodeURIComponent('account/settings')}`);
|
||||
}
|
||||
}, [saveState]);
|
||||
|
||||
|
||||
24
src/hooks.js
24
src/hooks.js
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user