diff --git a/app/javascript/packages/document-capture/components/document-capture-not-ready.tsx b/app/javascript/packages/document-capture/components/document-capture-not-ready.tsx
new file mode 100644
index 00000000000..07c5923ab46
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/document-capture-not-ready.tsx
@@ -0,0 +1,53 @@
+import { Button } from '@18f/identity-components';
+import { useI18n } from '@18f/identity-react-i18n';
+import { useContext } from 'react';
+import FlowContext from '@18f/identity-verify-flow/context/flow-context';
+import { addSearchParams, forceRedirect, Navigate } from '@18f/identity-url';
+import { getConfigValue } from '@18f/identity-config';
+import AnalyticsContext from '../context/analytics';
+import { ServiceProviderContext } from '../context';
+
+export interface DocumentCaptureNotReadyProps {
+ navigate?: Navigate;
+}
+
+function DocumentCaptureNotReady({ navigate }: DocumentCaptureNotReadyProps) {
+ const { t } = useI18n();
+ const { trackEvent } = useContext(AnalyticsContext);
+ const { currentStep } = useContext(FlowContext);
+ const { name: spName, failureToProofURL } = useContext(ServiceProviderContext);
+ const appName = getConfigValue('appName');
+ const handleExit = () => {
+ trackEvent('IdV: docauth not ready link clicked');
+ forceRedirect(
+ addSearchParams(spName ? failureToProofURL : '/account', {
+ step: currentStep,
+ location: 'not_ready',
+ }),
+ navigate,
+ );
+ };
+
+ return (
+ <>
+
{t('doc_auth.not_ready.header')}
+
+ {spName
+ ? t('doc_auth.not_ready.content_sp', {
+ sp_name: spName,
+ app_name: appName,
+ })
+ : t('doc_auth.not_ready.content_nosp', {
+ app_name: appName,
+ })}
+
+
+ >
+ );
+}
+
+export default DocumentCaptureNotReady;
diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
index 44dc1a798eb..89a3a07e50c 100644
--- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
@@ -1,3 +1,4 @@
+import { useContext } from 'react';
import { PageHeading } from '@18f/identity-components';
import {
FormStepError,
@@ -10,6 +11,8 @@ import { useI18n } from '@18f/identity-react-i18n';
import UnknownError from './unknown-error';
import TipList from './tip-list';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
+import DocumentCaptureNotReady from './document-capture-not-ready';
+import { FeatureFlagContext } from '../context';
interface DocumentCaptureReviewIssuesProps {
isFailedDocType: boolean;
@@ -43,6 +46,7 @@ function DocumentCaptureReviewIssues({
hasDismissed,
}: DocumentCaptureReviewIssuesProps) {
const { t } = useI18n();
+ const { notReadySectionEnabled } = useContext(FeatureFlagContext);
return (
<>
{t('doc_auth.headings.review_issues')}
@@ -78,6 +82,7 @@ function DocumentCaptureReviewIssues({
/>
))}
+ {notReadySectionEnabled && }
>
);
diff --git a/app/javascript/packages/document-capture/components/documents-step.jsx b/app/javascript/packages/document-capture/components/documents-step.jsx
index ccb2763f600..16db622b7be 100644
--- a/app/javascript/packages/document-capture/components/documents-step.jsx
+++ b/app/javascript/packages/document-capture/components/documents-step.jsx
@@ -8,6 +8,8 @@ import DocumentSideAcuantCapture from './document-side-acuant-capture';
import DeviceContext from '../context/device';
import UploadContext from '../context/upload';
import TipList from './tip-list';
+import DocumentCaptureNotReady from './document-capture-not-ready';
+import { FeatureFlagContext } from '../context';
/**
* @typedef {'front'|'back'} DocumentSide
@@ -43,7 +45,7 @@ function DocumentsStep({
const { isMobile } = useContext(DeviceContext);
const { isLastStep } = useContext(FormStepsContext);
const { flowPath } = useContext(UploadContext);
-
+ const { notReadySectionEnabled } = useContext(FeatureFlagContext);
return (
<>
{flowPath === 'hybrid' && }
@@ -70,7 +72,7 @@ function DocumentsStep({
/>
))}
{isLastStep ? : }
-
+ {notReadySectionEnabled && }
>
);
diff --git a/app/javascript/packages/document-capture/context/feature-flag.tsx b/app/javascript/packages/document-capture/context/feature-flag.tsx
new file mode 100644
index 00000000000..26048b8bcbd
--- /dev/null
+++ b/app/javascript/packages/document-capture/context/feature-flag.tsx
@@ -0,0 +1,17 @@
+import { createContext } from 'react';
+
+export interface FeatureFlagContextProps {
+ /**
+ * Specify whether to show the not-ready section on doc capture screen.
+ * Populated from backend configuration
+ */
+ notReadySectionEnabled: boolean;
+}
+
+const FeatureFlagContext = createContext({
+ notReadySectionEnabled: false,
+});
+
+FeatureFlagContext.displayName = 'FeatureFlagContext';
+
+export default FeatureFlagContext;
diff --git a/app/javascript/packages/document-capture/context/index.ts b/app/javascript/packages/document-capture/context/index.ts
index 128326c3ff3..dfbcbdf47bc 100644
--- a/app/javascript/packages/document-capture/context/index.ts
+++ b/app/javascript/packages/document-capture/context/index.ts
@@ -16,3 +16,4 @@ export {
} from './failed-capture-attempts';
export type { DeviceContextValue } from './device';
export { default as InPersonContext } from './in-person';
+export { default as FeatureFlagContext } from './feature-flag';
diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx
index 9d9125f96db..b50b0a5c2f6 100644
--- a/app/javascript/packs/document-capture.tsx
+++ b/app/javascript/packs/document-capture.tsx
@@ -10,6 +10,7 @@ import {
FailedCaptureAttemptsContextProvider,
MarketingSiteContextProvider,
InPersonContext,
+ FeatureFlagContext,
} from '@18f/identity-document-capture';
import { isCameraCapableMobile } from '@18f/identity-device';
import { FlowContext } from '@18f/identity-verify-flow';
@@ -33,6 +34,7 @@ interface AppRootData {
exitUrl: string;
idvInPersonUrl?: string;
securityAndPrivacyHowItWorksUrl: string;
+ uiNotReadySectionEnabled: string;
}
const appRoot = document.getElementById('document-capture-form')!;
@@ -99,6 +101,7 @@ const {
inPersonOutageExpectedUpdateDate,
usStatesTerritories = '',
phoneWithCamera = '',
+ uiNotReadySectionEnabled = '',
} = appRoot.dataset as DOMStringMap & AppRootData;
let parsedUsStatesTerritories = [];
@@ -175,6 +178,14 @@ const App = composeComponents(
maxSubmissionAttemptsBeforeNativeCamera: Number(maxSubmissionAttemptsBeforeNativeCamera),
},
],
+ [
+ FeatureFlagContext.Provider,
+ {
+ value: {
+ notReadySectionEnabled: String(uiNotReadySectionEnabled) === 'true',
+ },
+ },
+ ],
[
DocumentCapture,
{
diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb
index 170ee611e79..142f552a8e0 100644
--- a/app/views/idv/shared/_document_capture.html.erb
+++ b/app/views/idv/shared/_document_capture.html.erb
@@ -37,6 +37,7 @@
in_person_outage_expected_update_date: IdentityConfig.store.in_person_outage_expected_update_date,
us_states_territories: us_states_territories,
doc_auth_selfie_capture: IdentityConfig.store.doc_auth_selfie_capture,
+ ui_not_ready_section_enabled: IdentityConfig.store.doc_auth_not_ready_section_enabled,
} %>
<%= simple_form_for(
:doc_auth,
diff --git a/config/application.yml.default b/config/application.yml.default
index 04818829f1e..a360f77bdff 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -85,6 +85,7 @@ doc_auth_error_sharpness_threshold: 40
doc_auth_max_attempts: 5
doc_auth_max_capture_attempts_before_native_camera: 3
doc_auth_max_submission_attempts_before_native_camera: 3
+doc_auth_not_ready_section_enabled: false
doc_auth_selfie_capture: '{"enabled":false}'
doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]'
doc_capture_request_valid_for_minutes: 15
@@ -380,11 +381,12 @@ development:
database_worker_jobs_username: ''
database_worker_jobs_host: ''
database_worker_jobs_password: ''
- doc_auth_selfie_capture: '{"enabled":false}'
+ doc_auth_selfie_capture: '{"enabled":false}'
doc_auth_vendor: 'mock'
doc_auth_vendor_randomize: false
doc_auth_vendor_randomize_percent: 0
doc_auth_vendor_randomize_alternate_vendor: ''
+ doc_auth_not_ready_section_enabled: true
domain_name: localhost:3000
enable_rate_limiting: false
hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c
@@ -523,7 +525,7 @@ test:
doc_auth_vendor_randomize: false
doc_auth_vendor_randomize_percent: 0
doc_auth_vendor_randomize_alternate_vendor: ''
- doc_auth_selfie_capture: '{"enabled":false}'
+ doc_auth_selfie_capture: '{"enabled":false}'
doc_capture_polling_enabled: false
domain_name: www.example.com
hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index a20678ea2e1..191589bbb74 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -279,6 +279,15 @@ en:
- 'Verify by mail: We’ll mail a letter to your home
address. This takes 5 to 10 days.'
welcome: 'You will need your:'
+ not_ready:
+ button_nosp: Cancel and return to your profile
+ button_sp: Exit %{app_name} and return to %{sp_name}
+ content_nosp: If you exit %{app_name} now, you will not have verified your
+ identity. You can return later to finish this process.
+ content_sp: If you exit %{app_name} now and return to %{sp_name}, you will not
+ have verified your identity. You can return later to finish this
+ process.
+ header: Not ready to add photos?
phone_question:
do_not_have: I don’t have a phone
tips:
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index ec8aac78177..8f7b03244e5 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -321,6 +321,15 @@ es:
- 'Verificar por correo: Le enviaremos una carta a su
domicilio. Esto tarda entre 5 y 10 días.'
welcome: 'Necesitará su:'
+ not_ready:
+ button_nosp: Cancelar y volver a su perfil
+ button_sp: Salir de %{app_name} y volver a %{sp_name}
+ content_nosp: Si sale ahora de %{app_name}, no habrá verificado su identidad.
+ Puede volver más tarde para completar este proceso.
+ content_sp: Si sale ahora de %{app_name} y regresa a %{sp_name}, no habrá
+ verificado su identidad. Puede volver más tarde para completar este
+ proceso.
+ header: ¿No está listo para enviar las fotos?
phone_question:
do_not_have: No tengo teléfono
tips:
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index a3e6bb40593..06c97bf7484 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -333,6 +333,15 @@ fr:
lettre à votre adresse personnelle. Cela prend 5 à 10
jours.'
welcome: 'Vous aurez besoin de votre:'
+ not_ready:
+ button_nosp: Annuler et revenir à votre profil
+ button_sp: Quittez %{app_name} et retournez à %{sp_name}
+ content_nosp: Si vous quittez %{app_name}, votre identité n’aura pas été
+ vérifiée. Vous pourrez revenir plus tard pour terminer ce processus.
+ content_sp: Si vous quittez %{app_name} maintenant et revenez sur %{sp_name},
+ votre identité n’aura pas été vérifiée. Vous pourrez revenir plus tard
+ pour terminer ce processus.
+ header: Vous n’êtes pas prêt à ajouter des photos?
phone_question:
do_not_have: Je n’ai pas de téléphone
tips:
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index a9d884e4237..5e4c20eb141 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -180,6 +180,7 @@ def self.build_store(config_map)
config.add(:doc_auth_error_dpi_threshold, type: :integer)
config.add(:doc_auth_error_glare_threshold, type: :integer)
config.add(:doc_auth_error_sharpness_threshold, type: :integer)
+ config.add(:doc_auth_not_ready_section_enabled, type: :boolean)
config.add(:doc_auth_max_attempts, type: :integer)
config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer)
config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer)
diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb
index eedb3fef6bc..314ca89f7e6 100644
--- a/spec/features/idv/doc_auth/document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_spec.rb
@@ -9,10 +9,12 @@
let(:user) { user_with_2fa }
let(:fake_analytics) { FakeAnalytics.new }
let(:sp_name) { 'Test SP' }
+ let(:enable_not_ready) { true }
before do
allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
allow_any_instance_of(ServiceProviderSession).to receive(:sp_name).and_return(sp_name)
-
+ allow(IdentityConfig.store).to receive(:doc_auth_not_ready_section_enabled).
+ and_return(enable_not_ready)
visit_idp_from_oidc_sp_with_ial2
sign_in_and_2fa_user(user)
@@ -134,6 +136,16 @@
expect(DocAuthLog.find_by(user_id: user.id).state).to be_nil
end
+ context 'not ready section' do
+ it 'renders not ready section when enabled' do
+ expect(page).to have_content(
+ I18n.t(
+ 'doc_auth.not_ready.content_sp', sp_name: sp_name,
+ app_name: APP_NAME
+ ),
+ )
+ end
+ end
end
context 'standard mobile flow' do
diff --git a/spec/javascript/packages/document-capture/components/document-capture-not-ready-spec.tsx b/spec/javascript/packages/document-capture/components/document-capture-not-ready-spec.tsx
new file mode 100644
index 00000000000..2f8dbbcf94f
--- /dev/null
+++ b/spec/javascript/packages/document-capture/components/document-capture-not-ready-spec.tsx
@@ -0,0 +1,127 @@
+import sinon from 'sinon';
+
+import { FlowContext } from '@18f/identity-verify-flow';
+import { I18nContext } from '@18f/identity-react-i18n';
+import { I18n } from '@18f/identity-i18n';
+import userEvent from '@testing-library/user-event';
+import type { Navigate } from '@18f/identity-url';
+import {
+ AnalyticsContextProvider,
+ ServiceProviderContextProvider,
+} from '@18f/identity-document-capture/context';
+import DocumentCaptureNotReady from '@18f/identity-document-capture/components/document-capture-not-ready';
+import { expect } from 'chai';
+import { render } from '../../../support/document-capture';
+
+describe('DocumentCaptureNotReady', () => {
+ beforeEach(() => {
+ const config = document.createElement('script');
+ config.id = 'test-config';
+ config.type = 'application/json';
+ config.setAttribute('data-config', '');
+ config.textContent = JSON.stringify({ appName: 'Login.gov' });
+ document.body.append(config);
+ });
+ const trackEvent = sinon.spy();
+ const navigateSpy: Navigate = sinon.spy();
+ context('with service provider', () => {
+ const spName = 'testSP';
+ it('renders, track event and redirect', async () => {
+ const { getByRole } = render(
+
+ '',
+ }}
+ >
+
+
+
+
+
+
+ ,
+ );
+ // header
+ expect(getByRole('heading', { name: 'header text', level: 2 })).to.be.ok();
+
+ // content and exit link
+ const exitLink = getByRole('button', { name: 'Exit Login.gov and return to testSP' });
+ expect(exitLink).to.be.ok();
+ await userEvent.click(exitLink);
+ expect(navigateSpy).to.be.called.calledWithMatch(
+ /failure-to-proof\?step=document_capture&location=not_ready/,
+ );
+ expect(trackEvent).to.be.calledWithMatch(/IdV: docauth not ready link clicked/);
+ });
+ });
+
+ context('without service provider', () => {
+ it('renders, track event and redirect', async () => {
+ const { getByRole } = render(
+
+ '',
+ }}
+ >
+
+
+
+
+
+
+ ,
+ );
+ // header
+ expect(getByRole('heading', { name: 'header text', level: 2 })).to.be.ok();
+
+ // content and exit link
+ const exitLink = getByRole('button', { name: 'Cancel and return to your profile' });
+ expect(exitLink).to.be.ok();
+ await userEvent.click(exitLink);
+ expect(navigateSpy).to.be.called.calledWithMatch(
+ /account\?step=document_capture&location=not_ready/,
+ );
+ expect(trackEvent).to.be.calledWithMatch(/IdV: docauth not ready link clicked/);
+ });
+ });
+});
diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
index d160d95c5b4..44b2e570d37 100644
--- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
@@ -1,12 +1,15 @@
import userEvent from '@testing-library/user-event';
import sinon from 'sinon';
+import { expect } from 'chai';
import { t } from '@18f/identity-i18n';
import {
DeviceContext,
UploadContextProvider,
FailedCaptureAttemptsContextProvider,
+ FeatureFlagContext,
} from '@18f/identity-document-capture';
import DocumentsStep from '@18f/identity-document-capture/components/documents-step';
+import { composeComponents } from '@18f/identity-compose-components';
import { render } from '../../../support/document-capture';
import { getFixtureFile } from '../../../support/file';
@@ -82,4 +85,39 @@ describe('document-capture/components/documents-step', () => {
expect(queryByText(notExpectedText)).to.not.exist();
});
+
+ context('not ready section', () => {
+ it('is rendered when enabled', () => {
+ const App = composeComponents(
+ [
+ FeatureFlagContext.Provider,
+ {
+ value: {
+ notReadySectionEnabled: true,
+ },
+ },
+ ],
+ [DocumentsStep],
+ );
+ const { getByRole } = render();
+ expect(getByRole('heading', { name: 'doc_auth.not_ready.header', level: 2 })).to.be.ok();
+ const button = getByRole('button', { name: 'doc_auth.not_ready.button_nosp' });
+ expect(button).to.be.ok();
+ });
+ it('is not rendered when disabled', () => {
+ const App = composeComponents(
+ [
+ FeatureFlagContext.Provider,
+ {
+ value: {
+ notReadySectionEnabled: false,
+ },
+ },
+ ],
+ [DocumentsStep],
+ );
+ const { queryByRole } = render();
+ expect(queryByRole('heading', { name: 'doc_auth.not_ready.header', level: 2 })).to.be.null();
+ });
+ });
});