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
@@ -1,6 +1,6 @@
import { useI18n } from '@18f/identity-react-i18n';
import { t } from '@18f/identity-i18n';
import AcuantCapture from './acuant-capture';
import FormErrorMessage, { CameraAccessDeclinedError } from './form-error-message';
import { FormError } from './form-steps';

/** @typedef {import('./form-steps').FormStepError<*>} FormStepError */
/** @typedef {import('./form-steps').RegisterFieldCallback} RegisterFieldCallback */
Expand All @@ -19,6 +19,17 @@ import FormErrorMessage, { CameraAccessDeclinedError } from './form-error-messag
* @prop {string=} className
*/

/**
* An error representing user declined access to camera.
*/
export class CameraAccessDeclinedError extends FormError {
get message() {
return this.isDetail
? t('doc_auth.errors.camera.blocked_detail')
: t('doc_auth.errors.camera.blocked');
}
}

/**
* @param {DocumentSideAcuantCaptureProps} props Props object.
*/
Expand All @@ -31,7 +42,6 @@ function DocumentSideAcuantCapture({
onError,
className,
}) {
const { t } = useI18n();
const error = errors.find(({ field }) => field === side)?.error;

return (
Expand All @@ -52,9 +62,9 @@ function DocumentSideAcuantCapture({
}
onCameraAccessDeclined={() => {
onError(new CameraAccessDeclinedError(), { field: side });
onError(new CameraAccessDeclinedError());
onError(new CameraAccessDeclinedError({ isDetail: true }));
}}
errorMessage={error ? <FormErrorMessage error={error} /> : undefined}
errorMessage={error ? error.message : undefined}
name={side}
className={className}
capture="environment"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,68 +0,0 @@
import { useI18n } from '@18f/identity-react-i18n';
import { UploadFormEntryError } from '../services/upload';
import { BackgroundEncryptedUploadError } from '../higher-order/with-background-encrypted-upload';

/** @typedef {import('react').ReactNode} ReactNode */

/**
* @typedef FormErrorMessageProps
*
* @prop {Error} error Error for which message should be generated.
* @prop {boolean=} isDetail Whether to use an extended description for the error, if available.
*/

/**
* Non-breaking space (`&nbsp;`) represented as unicode escape sequence, which React will more
* happily tolerate than an HTML entity.
*
* @type {string}
*/
const NBSP_UNICODE = '\u00A0';

/**
* An error representing a state where a required form value is missing.
*/
export class RequiredValueMissingError extends Error {}

/**
* An error representing user declined access to camera.
*/
export class CameraAccessDeclinedError extends Error {}

/**
* @param {FormErrorMessageProps} props Props object.
*/
function FormErrorMessage({ error, isDetail = false }) {
const { t } = useI18n();

if (error instanceof RequiredValueMissingError) {
return <>{t('simple_form.required.text')}</>;
}

if (error instanceof CameraAccessDeclinedError) {
return (
<>
{isDetail
? t('doc_auth.errors.camera.blocked_detail')
: t('doc_auth.errors.camera.blocked')}
</>
);
}

if (error instanceof UploadFormEntryError) {
return <>{error.message}</>;
}

if (error instanceof BackgroundEncryptedUploadError) {
return (
<>
{t('doc_auth.errors.upload_error')}{' '}
{t('errors.messages.try_again').split(' ').join(NBSP_UNICODE)}
</>
);
}

return null;
}

export default FormErrorMessage;
23 changes: 19 additions & 4 deletions app/javascript/packages/document-capture/components/form-steps.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { useEffect, useRef, useState, createContext, useContext } from 'react';
import type { RefCallback, FormEventHandler, FC } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { t } from '@18f/identity-i18n';
import { Alert, Button } from '@18f/identity-components';
import { useDidUpdateEffect, useIfStillMounted } from '@18f/identity-react-hooks';
import FormErrorMessage, { RequiredValueMissingError } from './form-error-message';
import PromptOnNavigate from './prompt-on-navigate';
import useHistoryParam from '../hooks/use-history-param';
import useForceRender from '../hooks/use-force-render';

export class FormError extends Error {
isDetail: boolean;

constructor(options?: { isDetail: boolean }) {
super();

this.isDetail = Boolean(options?.isDetail);
}
}

/**
* An error representing a state where a required form value is missing.
*/
export class RequiredValueMissingError extends FormError {
message = t('simple_form.required.text');
}

export interface FormStepError<V> {
/**
* Name of field for which error occurred.
Expand Down Expand Up @@ -311,7 +327,7 @@ function FormSteps({
{Object.keys(values).length > 0 && <PromptOnNavigate />}
{stepErrors.map((error) => (
<Alert key={error.message} type="error" className="margin-bottom-4">
<FormErrorMessage error={error} isDetail />
{error.message}
</Alert>
))}
<FormStepsContext.Provider value={{ isLastStep, canContinueToNextStep, onPageTransition }}>
Expand Down Expand Up @@ -358,7 +374,6 @@ function FormSteps({

export function FormStepsContinueButton() {
const { canContinueToNextStep, isLastStep } = useContext(FormStepsContext);
const { t } = useI18n();

return (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import DeviceContext from '../context/device';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import AcuantCapture from './acuant-capture';
import SelfieCapture from './selfie-capture';
import FormErrorMessage from './form-error-message';
import ServiceProviderContext from '../context/service-provider';
import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload';
import DocumentCaptureTroubleshootingOptions from './document-capture-troubleshooting-options';
Expand Down Expand Up @@ -133,15 +132,15 @@ function ReviewIssuesStep({
onChange={(nextSelfie) => onChange({ selfie: nextSelfie })}
allowUpload={false}
className="document-capture-review-issues-step__input"
errorMessage={selfieError ? <FormErrorMessage error={selfieError} /> : undefined}
errorMessage={selfieError?.message}
name="selfie"
/>
) : (
<SelfieCapture
ref={registerField('selfie', { isRequired: true })}
value={value.selfie}
onChange={(nextSelfie) => onChange({ selfie: nextSelfie })}
errorMessage={selfieError ? <FormErrorMessage error={selfieError} /> : undefined}
errorMessage={selfieError?.message}
className={[
'document-capture-review-issues-step__input',
!value.selfie && 'document-capture-review-issues-step__input--unconstrained-width',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { FormStepsContinueButton } from './form-steps';
import DeviceContext from '../context/device';
import AcuantCapture from './acuant-capture';
import SelfieCapture from './selfie-capture';
import FormErrorMessage from './form-error-message';
import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload';
import PageHeading from './page-heading';
import StartOverOrCancel from './start-over-or-cancel';
Expand Down Expand Up @@ -55,15 +54,15 @@ function SelfieStep({
value={value.selfie}
onChange={(nextSelfie) => onChange({ selfie: nextSelfie })}
allowUpload={false}
errorMessage={error ? <FormErrorMessage error={error} /> : undefined}
errorMessage={error?.message}
name="selfie"
/>
) : (
<SelfieCapture
ref={registerField('selfie', { isRequired: true })}
value={value.selfie}
onChange={(nextSelfie) => onChange({ selfie: nextSelfie })}
errorMessage={error ? <FormErrorMessage error={error} /> : undefined}
errorMessage={error?.message}
/>
)}
<FormStepsContinueButton />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
import { useContext } from 'react';
import { t } from '@18f/identity-i18n';
import UploadContext from '../context/upload';
import AnalyticsContext from '../context/analytics';
import { FormError } from '../components/form-steps';

/**
* @typedef {import('../components/form-steps').FormStepComponentProps<V>} FormStepComponentProps
* @template V
*/

/**
* Non-breaking space (`&nbsp;`) represented as unicode escape sequence, which React will more
* happily tolerate than an HTML entity.
*/
const NBSP_UNICODE = '\u00A0';

/**
* Returns a new string from the given string, replacing spaces with non-breaking spaces.
*
* @param {string} string Original string.
*
* @return String with non-breaking spaces.
*/
const nonBreaking = (string) => string.split(' ').join(NBSP_UNICODE);

/**
* An error representing a failure to complete encrypted upload of image.
*/
export class BackgroundEncryptedUploadError extends Error {
export class BackgroundEncryptedUploadError extends FormError {
baseField = '';

/** @type {string[]} */
fields = [];

message = `${t('doc_auth.errors.upload_error')} ${nonBreaking(t('errors.messages.try_again'))}`;
}

/**
Expand Down
15 changes: 13 additions & 2 deletions app/javascript/packages/document-capture/services/upload.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { FormError } from '../components/form-steps';

/** @typedef {import('../context/upload').UploadSuccessResponse} UploadSuccessResponse */
/** @typedef {import('../context/upload').UploadErrorResponse} UploadErrorResponse */
/** @typedef {import('../context/upload').UploadFieldError} UploadFieldError */

export class UploadFormEntryError extends Error {
export class UploadFormEntryError extends FormError {
/** @type {string} */
field = '';

/**
* @param {string} message
*/
constructor(message) {
super();

this.message = message;
}
}

export class UploadFormEntriesError extends Error {
export class UploadFormEntriesError extends FormError {
/** @type {UploadFormEntryError[]} */
formEntryErrors = [];

Expand Down

This file was deleted.