Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useState, useMemo, useContext } from 'react';
import { Alert } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import { FormSteps, PromptOnNavigate } from '@18f/identity-form-steps';
import { FlowContext } from '@18f/identity-verify-flow';
import { FlowContext, VerifyFlowStepIndicator } from '@18f/identity-verify-flow';
import type { FormStep } from '@18f/identity-form-steps';
import { UploadFormEntriesError } from '../services/upload';
import DocumentsStep from './documents-step';
import SelfieStep from './selfie-step';
Expand All @@ -20,20 +21,15 @@ import SuspenseErrorBoundary from './suspense-error-boundary';
import SubmissionInterstitial from './submission-interstitial';
import withProps from '../higher-order/with-props';

/** @typedef {import('react').ReactNode} ReactNode */
/** @typedef {import('@18f/identity-form-steps').FormStep} FormStep */

/**
* Returns a new object with specified keys removed.
*
* @template {Record<string,any>} T
*
* @param {T} object Original object.
* @param {...string} keys Keys to remove.
* @param object Original object.
* @param keys Keys to remove.
*
* @return {Partial<T>} Object with keys removed.
* @return Object with keys removed.
*/
export const except = (object, ...keys) =>
export const except = <T extends Record<string, any>>(object: T, ...keys: string[]): Partial<T> =>
Object.entries(object).reduce((result, [key, value]) => {
if (!keys.includes(key)) {
result[key] = value;
Expand All @@ -42,19 +38,21 @@ export const except = (object, ...keys) =>
return result;
}, {});

/**
* @typedef DocumentCaptureProps
*
* @prop {boolean=} isAsyncForm Whether submission should poll for async response.
* @prop {()=>void=} onStepChange Callback triggered on step change.
*/
interface DocumentCaptureProps {
/**
* Whether submission should poll for async response.
*/
isAsyncForm?: boolean;

/**
* @param {DocumentCaptureProps} props
*/
function DocumentCapture({ isAsyncForm = false, onStepChange }) {
const [formValues, setFormValues] = useState(/** @type {Record<string,any>?} */ (null));
const [submissionError, setSubmissionError] = useState(/** @type {Error=} */ (undefined));
/**
* Callback triggered on step change.
*/
onStepChange?: () => void;
}

function DocumentCapture({ isAsyncForm = false, onStepChange }: DocumentCaptureProps) {
const [formValues, setFormValues] = useState<Record<string, any> | null>(null);
const [submissionError, setSubmissionError] = useState<Error | undefined>(undefined);
const { t } = useI18n();
const serviceProvider = useContext(ServiceProviderContext);
const { flowPath } = useContext(UploadContext);
Expand All @@ -63,9 +61,9 @@ function DocumentCapture({ isAsyncForm = false, onStepChange }) {
/**
* Clears error state and sets form values for submission.
*
* @param {Record<string,any>} nextFormValues Submitted form values.
* @param nextFormValues Submitted form values.
*/
function submitForm(nextFormValues) {
function submitForm(nextFormValues: Record<string, any>) {
setSubmissionError(undefined);
setFormValues(nextFormValues);
}
Expand Down Expand Up @@ -98,10 +96,10 @@ function DocumentCapture({ isAsyncForm = false, onStepChange }) {
}
}

const inPersonSteps =
const inPersonSteps: FormStep[] =
inPersonURL === undefined
? []
: /** @type {FormStep[]} */ ([
: ([
{
name: 'location',
form: InPersonLocationStep,
Expand All @@ -114,70 +112,72 @@ function DocumentCapture({ isAsyncForm = false, onStepChange }) {
name: 'switch_back',
form: InPersonSwitchBackStep,
},
]).filter(Boolean);
].filter(Boolean) as FormStep[]);

/** @type {FormStep[]} */
const steps = submissionError
? /** @type {FormStep[]} */ ([
{
name: 'review',
form:
submissionError instanceof UploadFormEntriesError
? withProps({
remainingAttempts: submissionError.remainingAttempts,
isFailedResult: submissionError.isFailedResult,
captureHints: submissionError.hints,
pii: submissionError.pii,
})(ReviewIssuesStep)
: ReviewIssuesStep,
},
])
.concat(inPersonSteps)
.filter(Boolean)
: /** @type {FormStep[]} */ (
const steps: FormStep[] = submissionError
? (
[
{
name: 'documents',
form: DocumentsStep,
},
serviceProvider.isLivenessRequired && {
name: 'selfie',
form: SelfieStep,
name: 'review',
form:
submissionError instanceof UploadFormEntriesError
? withProps({
remainingAttempts: submissionError.remainingAttempts,
isFailedResult: submissionError.isFailedResult,
captureHints: submissionError.hints,
pii: submissionError.pii,
})(ReviewIssuesStep)
: ReviewIssuesStep,
},
].filter(Boolean)
);
] as FormStep[]
).concat(inPersonSteps)
: ([
{
name: 'documents',
form: DocumentsStep,
},
serviceProvider.isLivenessRequired && {
name: 'selfie',
form: SelfieStep,
},
].filter(Boolean) as FormStep[]);

return submissionFormValues &&
(!submissionError || submissionError instanceof RetrySubmissionError) ? (
<>
<SubmissionInterstitial autoFocus />
<SuspenseErrorBoundary
fallback={<PromptOnNavigate />}
onError={setSubmissionError}
handledError={submissionError}
>
{submissionError instanceof RetrySubmissionError ? (
<SubmissionStatus />
) : (
<Submission payload={submissionFormValues} />
)}
</SuspenseErrorBoundary>
</>
) : (
return (
<>
{submissionError && !(submissionError instanceof UploadFormEntriesError) && (
<Alert type="error" className="margin-bottom-4">
{t('doc_auth.errors.general.network_error')}
</Alert>
<VerifyFlowStepIndicator currentStep="document_capture" />
{submissionFormValues &&
(!submissionError || submissionError instanceof RetrySubmissionError) ? (
<>
<SubmissionInterstitial autoFocus />
<SuspenseErrorBoundary
fallback={<PromptOnNavigate />}
onError={setSubmissionError}
handledError={submissionError}
>
{submissionError instanceof RetrySubmissionError ? (
<SubmissionStatus />
) : (
<Submission payload={submissionFormValues} />
)}
</SuspenseErrorBoundary>
</>
) : (
<>
{submissionError && !(submissionError instanceof UploadFormEntriesError) && (
<Alert type="error" className="margin-bottom-4">
{t('doc_auth.errors.general.network_error')}
</Alert>
)}
<FormSteps
steps={steps}
initialValues={initialValues}
initialActiveErrors={initialActiveErrors}
onComplete={submitForm}
onStepChange={onStepChange}
autoFocus={!!submissionError}
/>
</>
)}
<FormSteps
steps={steps}
initialValues={initialValues}
initialActiveErrors={initialActiveErrors}
onComplete={submitForm}
onStepChange={onStepChange}
autoFocus={!!submissionError}
/>
</>
);
}
Expand Down
1 change: 1 addition & 0 deletions app/javascript/packages/verify-flow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { default as FlowContext } from './context/flow-context';
export { SecretsContextProvider } from './context/secrets-context';
export { default as Cancel } from './cancel';
export { default as VerifyFlow } from './verify-flow';
export { default as VerifyFlowStepIndicator } from './verify-flow-step-indicator';

export { default as personalKeyStep } from './steps/personal-key';
export { default as personalKeyConfirmStep } from './steps/personal-key-confirm';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type VerifyFlowStepIndicatorStep =
* Mapping of flow form steps to corresponding step indicator step.
*/
const FLOW_STEP_STEP_MAPPING: Record<string, VerifyFlowStepIndicatorStep> = {
document_capture: 'verify_id',
password_confirm: 'secure_account',
personal_key: 'secure_account',
personal_key_confirm: 'secure_account',
Expand Down
3 changes: 2 additions & 1 deletion app/services/flow/flow_state_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ def call_optional_show_step(optional_step)
end

def step_indicator_params
return if !flow.class.const_defined?('STEP_INDICATOR_STEPS')
handler = flow.step_handler(current_step)
return if !flow.class.const_defined?('STEP_INDICATOR_STEPS') || !handler
return if !handler || !handler.const_defined?('STEP_INDICATOR_STEP')
{
steps: flow.class::STEP_INDICATOR_STEPS,
current_step: handler::STEP_INDICATOR_STEP,
Expand Down
2 changes: 0 additions & 2 deletions app/services/idv/steps/document_capture_step.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module Idv
module Steps
class DocumentCaptureStep < DocAuthBaseStep
STEP_INDICATOR_STEP = :verify_id

IMAGE_UPLOAD_PARAM_NAMES = %i[
front_image back_image selfie_image
].freeze
Expand Down
8 changes: 0 additions & 8 deletions spec/controllers/idv/capture_doc_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@
:flow_session,
step_template: 'idv/capture_doc/document_capture',
flow_namespace: 'idv',
step_indicator: hash_including(
:steps,
current_step: :verify_id,
),
),
).and_call_original

Expand All @@ -107,10 +103,6 @@
:flow_session,
step_template: 'idv/capture_doc/capture_complete',
flow_namespace: 'idv',
step_indicator: hash_including(
:steps,
current_step: :verify_id,
),
),
).and_call_original

Expand Down
4 changes: 0 additions & 4 deletions spec/controllers/idv/doc_auth_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@
:flow_session,
step_template: 'idv/doc_auth/document_capture',
flow_namespace: 'idv',
step_indicator: hash_including(
:steps,
current_step: :verify_id,
),
),
).and_call_original

Expand Down