diff --git a/src/id-verification/IdVerificationContextProvider.jsx b/src/id-verification/IdVerificationContextProvider.jsx index 027ab5a..a07a0f8 100644 --- a/src/id-verification/IdVerificationContextProvider.jsx +++ b/src/id-verification/IdVerificationContextProvider.jsx @@ -79,6 +79,10 @@ export default function IdVerificationContextProvider({ children }) { const [optimizelyExperimentName, setOptimizelyExperimentName] = useState(''); const [shouldUseCamera, setShouldUseCamera] = useState(false); + // If the user reaches the end of the flow and goes back to retake their photos, + // this flag ensures that they are directed straight back to the summary panel + const [reachedSummary, setReachedSummary] = useState(false); + const contextValue = { existingIdVerification, facePhotoFile, @@ -91,12 +95,14 @@ export default function IdVerificationContextProvider({ children }) { profileDataManager, optimizelyExperimentName, shouldUseCamera, + reachedSummary, setExistingIdVerification, setFacePhotoFile, setIdPhotoFile, setIdPhotoName, setOptimizelyExperimentName, setShouldUseCamera, + setReachedSummary, tryGetUserMedia: async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); diff --git a/src/id-verification/panels/SummaryPanel.jsx b/src/id-verification/panels/SummaryPanel.jsx index a11b466..efbc45a 100644 --- a/src/id-verification/panels/SummaryPanel.jsx +++ b/src/id-verification/panels/SummaryPanel.jsx @@ -1,4 +1,4 @@ -import React, { useState, useContext } from 'react'; +import React, { useState, useContext, useEffect } from 'react'; import { getConfig, history } from '@edx/frontend-platform'; import { Alert, Hyperlink, Input, Button, Spinner, @@ -27,11 +27,14 @@ function SummaryPanel(props) { idPhotoName, stopUserMedia, optimizelyExperimentName, + setReachedSummary, } = useContext(IdVerificationContext); const nameToBeUsed = idPhotoName || nameOnAccount || ''; const [isSubmitting, setIsSubmitting] = useState(false); const [submissionError, setSubmissionError] = useState(null); + useEffect(() => setReachedSummary(true), []); + function renderManagedProfileMessage() { if (!profileDataManager) { return null; diff --git a/src/id-verification/routing-utilities.js b/src/id-verification/routing-utilities.js index 71d7a7c..0218a51 100644 --- a/src/id-verification/routing-utilities.js +++ b/src/id-verification/routing-utilities.js @@ -2,56 +2,79 @@ import { useContext } from 'react'; import { useLocation } from 'react-router-dom'; import IdVerificationContext, { MEDIA_ACCESS } from './IdVerificationContext'; +const SLUGS = { + REVIEW_REQUIREMENTS: 'review-requirements', + CHOOSE_MODE: 'choose-mode', + REQUEST_CAMERA_ACCESS: 'request-camera-access', + PORTRAIT_PHOTO_CONTEXT: 'portrait-photo-context', + TAKE_PORTRAIT_PHOTO: 'take-portrait-photo', + ID_CONTEXT: 'id-context', + TAKE_ID_PHOTO: 'take-id-photo', + GET_NAME_ID: 'get-name-id', + SUMMARY: 'summary', + SUBMITTED: 'submitted', +}; + const panelSteps = [ - 'review-requirements', - 'choose-mode', - 'request-camera-access', - 'portrait-photo-context', - 'take-portrait-photo', - 'id-context', - 'take-id-photo', - 'get-name-id', - 'summary', - 'submitted', + SLUGS.REVIEW_REQUIREMENTS, + SLUGS.CHOOSE_MODE, + SLUGS.REQUEST_CAMERA_ACCESS, + SLUGS.PORTRAIT_PHOTO_CONTEXT, + SLUGS.TAKE_PORTRAIT_PHOTO, + SLUGS.ID_CONTEXT, + SLUGS.TAKE_ID_PHOTO, + SLUGS.GET_NAME_ID, + SLUGS.SUMMARY, + SLUGS.SUBMITTED, ]; // eslint-disable-next-line import/prefer-default-export export const useNextPanelSlug = (originSlug) => { // Go back to the summary view if that's where they came from const location = useLocation(); - const isFromSummary = location.state && location.state.fromSummary; const isFromPortrait = location.state && location.state.fromPortraitCapture; const isFromId = location.state && location.state.fromIdCapture; - const { shouldUseCamera, mediaAccess, optimizelyExperimentName } = useContext(IdVerificationContext); + const { + mediaAccess, + optimizelyExperimentName, + reachedSummary, + shouldUseCamera, + } = useContext(IdVerificationContext); - if (isFromSummary) { - return 'summary'; + const canRerouteToSummary = [ + SLUGS.TAKE_PORTRAIT_PHOTO, + SLUGS.TAKE_ID_PHOTO, + SLUGS.GET_NAME_ID, + ]; + + if (reachedSummary && canRerouteToSummary.includes(originSlug)) { + return SLUGS.SUMMARY; } // the following are used as part of an A/B experiment if (isFromPortrait) { if (mediaAccess === MEDIA_ACCESS.GRANTED) { - return 'portrait-photo-context'; + return SLUGS.PORTRAIT_PHOTO_CONTEXT; } - return 'take-portrait-photo'; + return SLUGS.TAKE_PORTRAIT_PHOTO; } if (isFromId) { if (mediaAccess === MEDIA_ACCESS.GRANTED) { - return 'id-context'; + return SLUGS.ID_CONTEXT; } - return 'take-id-photo'; + return SLUGS.TAKE_ID_PHOTO; } - if (originSlug === 'review-requirements' && !optimizelyExperimentName) { - return 'request-camera-access'; + if (originSlug === SLUGS.REVIEW_REQUIREMENTS && !optimizelyExperimentName) { + return SLUGS.REQUEST_CAMERA_ACCESS; } - if (originSlug === 'choose-mode' && !shouldUseCamera) { - return 'take-portrait-photo'; + if (originSlug === SLUGS.CHOOSE_MODE && !shouldUseCamera) { + return SLUGS.TAKE_PORTRAIT_PHOTO; } - if (originSlug === 'take-portrait-photo' && !shouldUseCamera) { - return 'take-id-photo'; + if (originSlug === SLUGS.TAKE_PORTRAIT_PHOTO && !shouldUseCamera) { + return SLUGS.TAKE_ID_PHOTO; } - if (originSlug === 'request-camera-access' && mediaAccess !== MEDIA_ACCESS.GRANTED) { - return 'take-portrait-photo'; + if (originSlug === SLUGS.REQUEST_CAMERA_ACCESS && mediaAccess !== MEDIA_ACCESS.GRANTED) { + return SLUGS.TAKE_PORTRAIT_PHOTO; } const nextIndex = panelSteps.indexOf(originSlug) + 1; @@ -63,16 +86,16 @@ export const useNextPanelSlug = (originSlug) => { export const useVerificationRedirectSlug = (slug) => { const { facePhotoFile, idPhotoFile, optimizelyExperimentName } = useContext(IdVerificationContext); const indexOfCurrentPanel = panelSteps.indexOf(slug); - if (!optimizelyExperimentName && slug === 'choose-mode') { - return 'review-requirements'; + if (!optimizelyExperimentName && slug === SLUGS.CHOOSE_MODE) { + return SLUGS.REVIEW_REQUIREMENTS; } if (!facePhotoFile) { - if (indexOfCurrentPanel > panelSteps.indexOf('take-portrait-photo')) { - return 'portrait-photo-context'; + if (indexOfCurrentPanel > panelSteps.indexOf(SLUGS.TAKE_PORTRAIT_PHOTO)) { + return SLUGS.PORTRAIT_PHOTO_CONTEXT; } } else if (!idPhotoFile) { - if (indexOfCurrentPanel > panelSteps.indexOf('take-id-photo')) { - return 'id-context'; + if (indexOfCurrentPanel > panelSteps.indexOf(SLUGS.TAKE_ID_PHOTO)) { + return SLUGS.ID_CONTEXT; } } diff --git a/src/id-verification/tests/panels/ChooseModePanel.test.jsx b/src/id-verification/tests/panels/ChooseModePanel.test.jsx index 11bcbe4..5446fa3 100644 --- a/src/id-verification/tests/panels/ChooseModePanel.test.jsx +++ b/src/id-verification/tests/panels/ChooseModePanel.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { - render, cleanup, act, screen, + render, cleanup, act, screen, fireEvent, } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; @@ -25,6 +25,7 @@ describe('ChooseModePanel', () => { const contextValue = { optimizelyExperimentName: 'test', shouldUseCamera: false, + reachedSummary: false, }; afterEach(() => { @@ -73,6 +74,23 @@ describe('ChooseModePanel', () => { expect(nextButton.getAttribute('href')).toEqual('/request-camera-access'); }); + it('reroutes correctly if reachedSummary is true', async () => { + contextValue.shouldUseCamera = true; + contextValue.reachedSummary = true; + await act(async () => render(( + + + + + + + + ))); + const nextButton = await screen.findByTestId('next-button'); + fireEvent.click(nextButton); + expect(history.location.pathname).toEqual('/request-camera-access'); + }); + it('redirects if user is not part of experiment', async () => { contextValue.optimizelyExperimentName = ''; diff --git a/src/id-verification/tests/panels/IdContextPanel.test.jsx b/src/id-verification/tests/panels/IdContextPanel.test.jsx index d2ba4f2..6df0ddc 100644 --- a/src/id-verification/tests/panels/IdContextPanel.test.jsx +++ b/src/id-verification/tests/panels/IdContextPanel.test.jsx @@ -26,13 +26,30 @@ describe('IdContextPanel', () => { const contextValue = { optimizelyExperimentName: '', facePhotoFile: 'test.jpg', + reachedSummary: false, }; afterEach(() => { cleanup(); }); - it('routes to TakeIdPhotoPanel', async () => { + it('routes to TakeIdPhotoPanel normally', async () => { + await act(async () => render(( + + + + + + + + ))); + const button = await screen.findByTestId('next-button'); + fireEvent.click(button); + expect(history.location.pathname).toEqual('/take-id-photo'); + }); + + it('routes to TakeIdPhotoPanel if reachedSummary is true', async () => { + contextValue.reachedSummary = true; await act(async () => render(( diff --git a/src/id-verification/tests/panels/PortraitPhotoContextPanel.test.jsx b/src/id-verification/tests/panels/PortraitPhotoContextPanel.test.jsx index 8a7c958..599430d 100644 --- a/src/id-verification/tests/panels/PortraitPhotoContextPanel.test.jsx +++ b/src/id-verification/tests/panels/PortraitPhotoContextPanel.test.jsx @@ -25,13 +25,30 @@ describe('PortraitPhotoContextPanel', () => { const contextValue = { optimizelyExperimentName: '', + reachedSummary: false, }; afterEach(() => { cleanup(); }); - it('routes to TakePortraitPhotoPanel', async () => { + it('routes to TakePortraitPhotoPanel normally', async () => { + await act(async () => render(( + + + + + + + + ))); + const button = await screen.findByTestId('next-button'); + fireEvent.click(button); + expect(history.location.pathname).toEqual('/take-portrait-photo'); + }); + + it('routes to TakePortraitPhotoPanel if reachedSummary is true', async () => { + contextValue.reachedSummary = true; await act(async () => render(( diff --git a/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx b/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx index fcbcec0..f817e1e 100644 --- a/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx +++ b/src/id-verification/tests/panels/RequestCameraAccessPanel.test.jsx @@ -26,6 +26,7 @@ describe('RequestCameraAccessPanel', () => { }; const contextValue = { + reachedSummary: false, tryGetUserMedia: jest.fn(), }; @@ -241,6 +242,46 @@ describe('RequestCameraAccessPanel', () => { expect(history.location.pathname).toEqual('/id-context'); }); + it('reroutes to portrait context when reachedSummary is true', async () => { + contextValue.mediaAccess = 'granted'; + contextValue.reachedSummary = true; + history.location.state = { fromPortraitCapture: true }; + + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } }); + await act(async () => render(( + + + + + + + + ))); + const button = await screen.findByTestId('next-button'); + fireEvent.click(button); + expect(history.location.pathname).toEqual('/portrait-photo-context'); + }); + + it('reroutes to ID context when reachedSummary is true', async () => { + contextValue.mediaAccess = 'granted'; + contextValue.reachedSummary = true; + history.location.state = { fromIdCapture: true }; + + Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } }); + await act(async () => render(( + + + + + + + + ))); + const button = await screen.findByTestId('next-button'); + fireEvent.click(button); + expect(history.location.pathname).toEqual('/id-context'); + }); + it('reroutes correctly to portrait context with no media access', async () => { contextValue.mediaAccess = 'denied'; contextValue.optimizelyExperimentName = 'test'; diff --git a/src/id-verification/tests/panels/SummaryPanel.test.jsx b/src/id-verification/tests/panels/SummaryPanel.test.jsx index e7a9627..7cfa84b 100644 --- a/src/id-verification/tests/panels/SummaryPanel.test.jsx +++ b/src/id-verification/tests/panels/SummaryPanel.test.jsx @@ -34,6 +34,7 @@ describe('SummaryPanel', () => { idPhotoName: 'test name', optimizelyExperimentName: 'test-experiment', stopUserMedia: jest.fn(), + setReachedSummary: jest.fn(), }; const getPanel = async () => { diff --git a/src/id-verification/tests/panels/TakeIdPhotoPanel.test.jsx b/src/id-verification/tests/panels/TakeIdPhotoPanel.test.jsx index ccbf0db..d681425 100644 --- a/src/id-verification/tests/panels/TakeIdPhotoPanel.test.jsx +++ b/src/id-verification/tests/panels/TakeIdPhotoPanel.test.jsx @@ -27,6 +27,7 @@ describe('TakeIdPhotoPanel', () => { const contextValue = { facePhotoFile: 'test.jpg', idPhotoFile: null, + reachedSummary: false, setIdPhotoFile: jest.fn(), }; @@ -67,7 +68,7 @@ describe('TakeIdPhotoPanel', () => { it('routes back to SummaryPanel if that was the source', async () => { contextValue.idPhotoFile = 'test.jpg'; - history.location.state = { fromSummary: true }; + contextValue.reachedSummary = true; await act(async () => render(( diff --git a/src/id-verification/tests/panels/TakePortraitPhotoPanel.test.jsx b/src/id-verification/tests/panels/TakePortraitPhotoPanel.test.jsx index 49bdf82..2f877bd 100644 --- a/src/id-verification/tests/panels/TakePortraitPhotoPanel.test.jsx +++ b/src/id-verification/tests/panels/TakePortraitPhotoPanel.test.jsx @@ -26,6 +26,8 @@ describe('TakePortraitPhotoPanel', () => { const contextValue = { facePhotoFile: null, + idPhotoFile: null, + reachedSummary: false, setFacePhotoFile: jest.fn(), setShouldUseCamera: jest.fn(), }; @@ -68,7 +70,8 @@ describe('TakePortraitPhotoPanel', () => { it('routes back to SummaryPanel if that was the source', async () => { contextValue.facePhotoFile = 'test.jpg'; - history.location.state = { fromSummary: true }; + contextValue.idPhotoFile = 'test.jpg'; + contextValue.reachedSummary = true; await act(async () => render((