@aws-amplify/ui-react-liveness 3.0.8 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ import '@tensorflow-models/face-detection';
9
9
  import '@tensorflow/tfjs-backend-wasm';
10
10
  import '@tensorflow/tfjs-backend-cpu';
11
11
  import '@aws-amplify/core/internals/utils';
12
- import { drawStaticOval } from '../service/utils/liveness.mjs';
12
+ import { drawStaticOval, clearOvalCanvas } from '../service/utils/liveness.mjs';
13
13
  import 'aws-amplify/auth';
14
14
  import '@aws-sdk/client-rekognitionstreaming';
15
15
  import '@aws-sdk/util-format-url';
@@ -38,6 +38,7 @@ const showMatchIndicatorStates = [
38
38
  FaceMatchState.TOO_FAR,
39
39
  FaceMatchState.CANT_IDENTIFY,
40
40
  FaceMatchState.FACE_IDENTIFIED,
41
+ FaceMatchState.OFF_CENTER,
41
42
  ];
42
43
  /**
43
44
  * For now we want to memoize the HOC for MatchIndicator because to optimize renders
@@ -65,6 +66,7 @@ const LivenessCameraModule = (props) => {
65
66
  const isCheckingCamera = state.matches('cameraCheck');
66
67
  const isWaitingForCamera = state.matches('waitForDOMAndCameraDetails');
67
68
  const isStartView = state.matches('start') || state.matches('userCancel');
69
+ const isDetectFaceBeforeStart = state.matches('detectFaceBeforeStart');
68
70
  const isRecording = state.matches('recording');
69
71
  const isCheckSucceeded = state.matches('checkSucceeded');
70
72
  const isFlashingFreshness = state.matches({
@@ -125,6 +127,11 @@ const LivenessCameraModule = (props) => {
125
127
  setAspectRatio(videoRef.current.videoWidth / videoRef.current.videoHeight);
126
128
  }
127
129
  }, [send, videoRef, isCameraReady, isMobileScreen]);
130
+ React__default.useEffect(() => {
131
+ if (isDetectFaceBeforeStart) {
132
+ clearOvalCanvas({ canvas: canvasRef.current });
133
+ }
134
+ }, [isDetectFaceBeforeStart]);
128
135
  const photoSensitivityWarning = React__default.useMemo(() => {
129
136
  return (React__default.createElement(View, { style: { visibility: isStartView ? 'visible' : 'hidden' } },
130
137
  React__default.createElement(PhotosensitiveWarning, { bodyText: instructionDisplayText.photosensitivityWarningBodyText, headingText: instructionDisplayText.photosensitivityWarningHeadingText, infoText: instructionDisplayText.photosensitivityWarningInfoText, labelText: instructionDisplayText.photosensitivityWarningLabelText })));
@@ -159,31 +166,35 @@ const LivenessCameraModule = (props) => {
159
166
  React__default.createElement(Loader, { size: "large", className: LivenessClassNames.Loader, "data-testid": "centered-loader", position: "unset" }),
160
167
  React__default.createElement(Text, { fontSize: "large", fontWeight: "bold", "data-testid": "waiting-camera-permission", className: `${LivenessClassNames.StartScreenCameraWaiting}__text` }, cameraDisplayText.waitingCameraPermissionText)));
161
168
  }
162
- const isRecordingOnMobile = isMobileScreen && !isStartView && !isWaitingForCamera && isRecording;
169
+ // We don't show full screen camera on the pre check screen (isStartView/isWaitingForCamera)
170
+ const shouldShowFullScreenCamera = isMobileScreen && !isStartView && !isWaitingForCamera;
163
171
  return (React__default.createElement(React__default.Fragment, null,
164
172
  photoSensitivityWarning,
165
- React__default.createElement(Flex, { className: classNames(LivenessClassNames.CameraModule, isRecordingOnMobile && `${LivenessClassNames.CameraModule}--mobile`), "data-testid": testId, gap: "zero" },
173
+ React__default.createElement(Flex, { className: classNames(LivenessClassNames.CameraModule, shouldShowFullScreenCamera &&
174
+ `${LivenessClassNames.CameraModule}--mobile`), "data-testid": testId, gap: "zero" },
166
175
  !isCameraReady && centeredLoader,
176
+ React__default.createElement(Overlay, { horizontal: "center", vertical: isRecording && !isFlashingFreshness ? 'start' : 'space-between', className: LivenessClassNames.InstructionOverlay },
177
+ isRecording && (React__default.createElement(DefaultRecordingIcon, { recordingIndicatorText: recordingIndicatorText })),
178
+ !isStartView && !isWaitingForCamera && !isCheckSucceeded && (React__default.createElement(DefaultCancelButton, { cancelLivenessCheckText: cancelLivenessCheckText })),
179
+ React__default.createElement(Flex, { className: classNames(LivenessClassNames.Hint, shouldShowFullScreenCamera && `${LivenessClassNames.Hint}--mobile`) },
180
+ React__default.createElement(Hint, { hintDisplayText: hintDisplayText })),
181
+ errorState && (React__default.createElement(ErrorView, { onRetry: () => {
182
+ send({ type: 'CANCEL' });
183
+ }, displayText: errorDisplayText }, renderErrorModal({
184
+ errorState,
185
+ overrideErrorDisplayText: errorDisplayText,
186
+ }))),
187
+ isRecording &&
188
+ !isFlashingFreshness &&
189
+ showMatchIndicatorStates.includes(faceMatchState) ? (React__default.createElement(MemoizedMatchIndicator, { percentage: Math.ceil(faceMatchPercentage) })) : null),
167
190
  React__default.createElement(View, { as: "canvas", ref: freshnessColorRef, className: LivenessClassNames.FreshnessCanvas, hidden: true }),
168
191
  React__default.createElement(View, { className: LivenessClassNames.VideoAnchor, style: {
169
192
  aspectRatio: `${aspectRatio}`,
170
193
  } },
171
194
  React__default.createElement("video", { ref: videoRef, muted: true, autoPlay: true, playsInline: true, width: mediaWidth, height: mediaHeight, onCanPlay: handleMediaPlay, "data-testid": "video", className: LivenessClassNames.Video, "aria-label": cameraDisplayText.a11yVideoLabelText }),
172
- React__default.createElement(Flex, { className: classNames(LivenessClassNames.OvalCanvas, isRecordingOnMobile && `${LivenessClassNames.OvalCanvas}--mobile`, isRecordingStopped && LivenessClassNames.FadeOut) },
195
+ React__default.createElement(Flex, { className: classNames(LivenessClassNames.OvalCanvas, shouldShowFullScreenCamera &&
196
+ `${LivenessClassNames.OvalCanvas}--mobile`, isRecordingStopped && LivenessClassNames.FadeOut) },
173
197
  React__default.createElement(View, { as: "canvas", ref: canvasRef })),
174
- isRecording && (React__default.createElement(DefaultRecordingIcon, { recordingIndicatorText: recordingIndicatorText })),
175
- !isStartView && !isWaitingForCamera && !isCheckSucceeded && (React__default.createElement(DefaultCancelButton, { cancelLivenessCheckText: cancelLivenessCheckText })),
176
- React__default.createElement(Overlay, { horizontal: "center", vertical: isRecording && !isFlashingFreshness ? 'start' : 'space-between', className: LivenessClassNames.InstructionOverlay },
177
- React__default.createElement(Hint, { hintDisplayText: hintDisplayText }),
178
- errorState && (React__default.createElement(ErrorView, { onRetry: () => {
179
- send({ type: 'CANCEL' });
180
- }, displayText: errorDisplayText }, renderErrorModal({
181
- errorState,
182
- overrideErrorDisplayText: errorDisplayText,
183
- }))),
184
- isRecording &&
185
- !isFlashingFreshness &&
186
- showMatchIndicatorStates.includes(faceMatchState) ? (React__default.createElement(MemoizedMatchIndicator, { percentage: Math.ceil(faceMatchPercentage) })) : null),
187
198
  isStartView &&
188
199
  !isMobileScreen &&
189
200
  selectableDevices &&
@@ -1,4 +1,5 @@
1
1
  const defaultErrorDisplayText = {
2
+ errorLabelText: 'Error',
2
3
  timeoutHeaderText: 'Time out',
3
4
  timeoutMessageText: "Face didn't fit inside oval in time limit. Try again and completely fill the oval with face in it.",
4
5
  faceDistanceHeaderText: 'Forward movement detected',
@@ -24,6 +25,8 @@ const defaultLivenessDisplayText = {
24
25
  goodFitCaptionText: 'Good fit',
25
26
  goodFitAltText: "Ilustration of a person's face, perfectly fitting inside of an oval.",
26
27
  hintCenterFaceText: 'Center your face',
28
+ hintCenterFaceInstructionText: 'Instruction: Before starting the check, make sure your camera is at the center top of your screen and center your face to the camera. When the check starts an oval will show up in the center. You will be prompted to move forward into the oval and then prompted to hold still. After holding still for a few seconds, you should hear check complete.',
29
+ hintFaceOffCenterText: 'Face is not in the oval, center your face to the camera.',
27
30
  hintMoveFaceFrontOfCameraText: 'Move face in front of camera',
28
31
  hintTooManyFacesText: 'Ensure only one face is in front of camera',
29
32
  hintFaceDetectedText: 'Face detected',
@@ -32,10 +35,12 @@ const defaultLivenessDisplayText = {
32
35
  hintTooFarText: 'Move closer',
33
36
  hintConnectingText: 'Connecting...',
34
37
  hintVerifyingText: 'Verifying...',
38
+ hintCheckCompleteText: 'Check complete',
35
39
  hintIlluminationTooBrightText: 'Move to dimmer area',
36
40
  hintIlluminationTooDarkText: 'Move to brighter area',
37
41
  hintIlluminationNormalText: 'Lighting conditions normal',
38
42
  hintHoldFaceForFreshnessText: 'Hold still',
43
+ hintMatchIndicatorText: '50% completed. Keep moving closer.',
39
44
  photosensitivityWarningBodyText: 'This check flashes different colors. Use caution if you are photosensitive.',
40
45
  photosensitivityWarningHeadingText: 'Photosensitivity warning',
41
46
  photosensitivityWarningInfoText: 'Some people may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition.',
@@ -18,6 +18,7 @@ var FaceMatchState;
18
18
  FaceMatchState["CANT_IDENTIFY"] = "CANNOT IDENTIFY";
19
19
  FaceMatchState["FACE_IDENTIFIED"] = "ONE FACE IDENTIFIED";
20
20
  FaceMatchState["TOO_MANY"] = "TOO MANY FACES";
21
+ FaceMatchState["OFF_CENTER"] = "OFF CENTER";
21
22
  })(FaceMatchState || (FaceMatchState = {}));
22
23
 
23
24
  export { FaceMatchState, IlluminationState };
@@ -162,6 +162,16 @@ function drawLivenessOvalInCanvas({ canvas, oval, scaleFactor, videoEl, isStartS
162
162
  throw new Error('Cannot find Canvas.');
163
163
  }
164
164
  }
165
+ function clearOvalCanvas({ canvas, }) {
166
+ const ctx = canvas.getContext('2d');
167
+ if (ctx) {
168
+ ctx.restore();
169
+ ctx.clearRect(0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
170
+ }
171
+ else {
172
+ throw new Error('Cannot find Canvas.');
173
+ }
174
+ }
165
175
  /**
166
176
  * Returns the state of the provided face with respect to the provided liveness oval.
167
177
  */
@@ -196,12 +206,17 @@ function getFaceMatchStateInLivenessOval(face, ovalDetails, initialFaceIntersect
196
206
  const faceMatchPercentage = Math.max(Math.min(1, (0.75 * (intersection - initialFaceIntersection)) /
197
207
  (intersectionThreshold - initialFaceIntersection) +
198
208
  0.25), 0) * 100;
209
+ const faceIsOutsideOvalToTheLeft = minOvalX > minFaceX && maxOvalX > maxFaceX;
210
+ const faceIsOutsideOvalToTheRight = minFaceX > minOvalX && maxFaceX > maxOvalX;
199
211
  if (intersection > intersectionThreshold &&
200
212
  Math.abs(minOvalX - minFaceX) < ovalMatchWidthThreshold &&
201
213
  Math.abs(maxOvalX - maxFaceX) < ovalMatchWidthThreshold &&
202
214
  Math.abs(maxOvalY - maxFaceY) < ovalMatchHeightThreshold) {
203
215
  faceMatchState = FaceMatchState.MATCHED;
204
216
  }
217
+ else if (faceIsOutsideOvalToTheLeft || faceIsOutsideOvalToTheRight) {
218
+ faceMatchState = FaceMatchState.OFF_CENTER;
219
+ }
205
220
  else if (minOvalY - minFaceY > faceDetectionHeightThreshold ||
206
221
  maxFaceY - maxOvalY > faceDetectionHeightThreshold ||
207
222
  (minOvalX - minFaceX > faceDetectionWidthThreshold &&
@@ -459,4 +474,4 @@ function getBoundingBox({ deviceHeight, deviceWidth, height, width, top, left, }
459
474
  };
460
475
  }
461
476
 
462
- export { drawLivenessOvalInCanvas, drawStaticOval, estimateIllumination, fillOverlayCanvasFractional, generateBboxFromLandmarks, getBoundingBox, getColorsSequencesFromSessionInformation, getFaceMatchState, getFaceMatchStateInLivenessOval, getIntersectionOverUnion, getOvalBoundingBox, getOvalDetailsFromSessionInformation, getRGBArrayFromColorString, getStaticLivenessOvalDetails, isCameraDeviceVirtual, isClientFreshnessColorSequence, isFaceDistanceBelowThreshold };
477
+ export { clearOvalCanvas, drawLivenessOvalInCanvas, drawStaticOval, estimateIllumination, fillOverlayCanvasFractional, generateBboxFromLandmarks, getBoundingBox, getColorsSequencesFromSessionInformation, getFaceMatchState, getFaceMatchStateInLivenessOval, getIntersectionOverUnion, getOvalBoundingBox, getOvalDetailsFromSessionInformation, getRGBArrayFromColorString, getStaticLivenessOvalDetails, isCameraDeviceVirtual, isClientFreshnessColorSequence, isFaceDistanceBelowThreshold };
@@ -23,7 +23,7 @@ import { LivenessClassNames } from '../types/classNames.mjs';
23
23
 
24
24
  const renderToastErrorModal = (props) => {
25
25
  const { error: errorState, displayText } = props;
26
- const { timeoutHeaderText, timeoutMessageText, faceDistanceHeaderText, faceDistanceMessageText, multipleFacesHeaderText, multipleFacesMessageText, clientHeaderText, clientMessageText, serverHeaderText, serverMessageText, } = displayText;
26
+ const { errorLabelText, timeoutHeaderText, timeoutMessageText, faceDistanceHeaderText, faceDistanceMessageText, multipleFacesHeaderText, multipleFacesMessageText, clientHeaderText, clientMessageText, serverHeaderText, serverMessageText, } = displayText;
27
27
  let heading;
28
28
  let message;
29
29
  switch (errorState) {
@@ -50,7 +50,7 @@ const renderToastErrorModal = (props) => {
50
50
  }
51
51
  return (React__default.createElement(React__default.Fragment, null,
52
52
  React__default.createElement(Flex, { className: LivenessClassNames.ErrorModal },
53
- React__default.createElement(AlertIcon, { ariaHidden: true, variation: "error" }),
53
+ React__default.createElement(AlertIcon, { ariaLabel: errorLabelText, role: "img", variation: "error" }),
54
54
  React__default.createElement(Text, { className: LivenessClassNames.ErrorModalHeading, id: "amplify-liveness-error-heading" }, heading)),
55
55
  React__default.createElement(Text, { id: "amplify-liveness-error-message" }, message)));
56
56
  };
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { View } from '@aws-amplify/ui-react';
2
+ import { VisuallyHidden, View } from '@aws-amplify/ui-react';
3
3
  import '../service/machine/index.mjs';
4
4
  import { FaceMatchState, IlluminationState } from '../service/types/liveness.mjs';
5
5
  import '@tensorflow/tfjs-core';
@@ -25,9 +25,10 @@ const selectFaceMatchState = createLivenessSelector((state) => state.context.fac
25
25
  const selectIlluminationState = createLivenessSelector((state) => state.context.faceMatchAssociatedParams.illuminationState);
26
26
  const selectIsFaceFarEnoughBeforeRecording = createLivenessSelector((state) => state.context.isFaceFarEnoughBeforeRecording);
27
27
  const selectFaceMatchStateBeforeStart = createLivenessSelector((state) => state.context.faceMatchStateBeforeStart);
28
- const defaultToast = (text, isInitial = false) => {
28
+ const selectFaceMatchPercentage = createLivenessSelector((state) => state.context.faceMatchAssociatedParams?.faceMatchPercentage);
29
+ const DefaultToast = ({ text, isInitial = false, }) => {
29
30
  return (React.createElement(Toast, { size: "large", variation: "primary", isInitial: isInitial },
30
- React.createElement(View, { "aria-live": "assertive", "aria-label": text }, text)));
31
+ React.createElement(View, { "aria-live": "assertive" }, text)));
31
32
  };
32
33
  const Hint = ({ hintDisplayText }) => {
33
34
  const [state] = useLivenessActor();
@@ -37,8 +38,11 @@ const Hint = ({ hintDisplayText }) => {
37
38
  const illuminationState = useLivenessSelector(selectIlluminationState);
38
39
  const faceMatchStateBeforeStart = useLivenessSelector(selectFaceMatchStateBeforeStart);
39
40
  const isFaceFarEnoughBeforeRecordingState = useLivenessSelector(selectIsFaceFarEnoughBeforeRecording);
40
- const isCheckFaceDetectedBeforeStart = state.matches('checkFaceDetectedBeforeStart');
41
- const isCheckFaceDistanceBeforeRecording = state.matches('checkFaceDistanceBeforeRecording');
41
+ const faceMatchPercentage = useLivenessSelector(selectFaceMatchPercentage);
42
+ const isCheckFaceDetectedBeforeStart = state.matches('checkFaceDetectedBeforeStart') ||
43
+ state.matches('detectFaceBeforeStart');
44
+ const isCheckFaceDistanceBeforeRecording = state.matches('checkFaceDistanceBeforeRecording') ||
45
+ state.matches('detectFaceDistanceBeforeRecording');
42
46
  const isStartView = state.matches('start') || state.matches('userCancel');
43
47
  const isRecording = state.matches('recording');
44
48
  const isNotRecording = state.matches('notRecording');
@@ -55,61 +59,74 @@ const Hint = ({ hintDisplayText }) => {
55
59
  [FaceMatchState.TOO_CLOSE]: hintDisplayText.hintTooCloseText,
56
60
  [FaceMatchState.TOO_FAR]: hintDisplayText.hintTooFarText,
57
61
  [FaceMatchState.MATCHED]: hintDisplayText.hintHoldFaceForFreshnessText,
62
+ [FaceMatchState.OFF_CENTER]: hintDisplayText.hintFaceOffCenterText,
58
63
  };
59
64
  const IlluminationStateStringMap = {
60
65
  [IlluminationState.BRIGHT]: hintDisplayText.hintIlluminationTooBrightText,
61
66
  [IlluminationState.DARK]: hintDisplayText.hintIlluminationTooDarkText,
62
67
  [IlluminationState.NORMAL]: hintDisplayText.hintIlluminationNormalText,
63
68
  };
64
- const getInstructionContent = () => {
65
- if (isStartView) {
66
- return defaultToast(hintDisplayText.hintCenterFaceText, true);
69
+ if (isStartView) {
70
+ return (React.createElement(React.Fragment, null,
71
+ React.createElement(VisuallyHidden, { role: "alert" }, hintDisplayText.hintCenterFaceInstructionText),
72
+ React.createElement(DefaultToast, { text: hintDisplayText.hintCenterFaceText, isInitial: true })));
73
+ }
74
+ if (errorState ?? (isCheckFailed || isCheckSuccessful)) {
75
+ return null;
76
+ }
77
+ if (!isRecording) {
78
+ if (isCheckFaceDetectedBeforeStart) {
79
+ if (faceMatchStateBeforeStart === FaceMatchState.TOO_MANY) {
80
+ return React.createElement(DefaultToast, { text: hintDisplayText.hintTooManyFacesText });
81
+ }
82
+ return (React.createElement(DefaultToast, { text: hintDisplayText.hintMoveFaceFrontOfCameraText }));
67
83
  }
68
- if (errorState ?? (isCheckFailed || isCheckSuccessful)) {
69
- return;
84
+ // Specifically checking for false here because initially the value is undefined and we do not want to show the instruction
85
+ if (isCheckFaceDistanceBeforeRecording &&
86
+ isFaceFarEnoughBeforeRecordingState === false) {
87
+ return React.createElement(DefaultToast, { text: hintDisplayText.hintTooCloseText });
70
88
  }
71
- if (!isRecording) {
72
- if (isCheckFaceDetectedBeforeStart) {
73
- if (faceMatchStateBeforeStart === FaceMatchState.TOO_MANY) {
74
- return defaultToast(FaceMatchStateStringMap[faceMatchStateBeforeStart]);
75
- }
76
- return defaultToast(hintDisplayText.hintMoveFaceFrontOfCameraText);
77
- }
78
- // Specifically checking for false here because initially the value is undefined and we do not want to show the instruction
79
- if (isCheckFaceDistanceBeforeRecording &&
80
- isFaceFarEnoughBeforeRecordingState === false) {
81
- return defaultToast(hintDisplayText.hintTooCloseText);
82
- }
83
- if (isNotRecording) {
84
- return (React.createElement(ToastWithLoader, { displayText: hintDisplayText.hintConnectingText }));
85
- }
86
- if (isUploading) {
87
- return (React.createElement(ToastWithLoader, { displayText: hintDisplayText.hintVerifyingText }));
88
- }
89
- if (illuminationState && illuminationState !== IlluminationState.NORMAL) {
90
- return defaultToast(IlluminationStateStringMap[illuminationState]);
91
- }
89
+ if (isNotRecording) {
90
+ return (React.createElement(ToastWithLoader, { displayText: hintDisplayText.hintConnectingText }));
92
91
  }
93
- if (isFlashingFreshness) {
94
- return defaultToast(hintDisplayText.hintHoldFaceForFreshnessText);
92
+ if (isUploading) {
93
+ return (React.createElement(React.Fragment, null,
94
+ React.createElement(VisuallyHidden, { "aria-live": "assertive" }, hintDisplayText.hintCheckCompleteText),
95
+ React.createElement(ToastWithLoader, { displayText: hintDisplayText.hintVerifyingText })));
95
96
  }
96
- if (isRecording && !isFlashingFreshness) {
97
- // During face matching, we want to only show the TOO_CLOSE or
98
- // TOO_FAR texts. If FaceMatchState matches TOO_CLOSE, we'll show
99
- // the TOO_CLOSE text, but for FACE_IDENTIFED, CANT_IDENTIFY, TOO_MANY
100
- // we are defaulting to the TOO_FAR text (for now).
101
- let resultHintString = FaceMatchStateStringMap[FaceMatchState.TOO_FAR];
102
- if (faceMatchState === FaceMatchState.TOO_CLOSE ||
103
- faceMatchState === FaceMatchState.MATCHED) {
104
- resultHintString = FaceMatchStateStringMap[faceMatchState];
105
- }
106
- return (React.createElement(Toast, { size: "large", variation: faceMatchState === FaceMatchState.TOO_CLOSE ? 'error' : 'primary' },
107
- React.createElement(View, { "aria-live": "assertive", "aria-label": resultHintString }, resultHintString)));
97
+ if (illuminationState && illuminationState !== IlluminationState.NORMAL) {
98
+ return (React.createElement(DefaultToast, { text: IlluminationStateStringMap[illuminationState] }));
108
99
  }
109
- return null;
110
- };
111
- const instructionContent = getInstructionContent();
112
- return instructionContent ? instructionContent : null;
100
+ }
101
+ if (isFlashingFreshness) {
102
+ return React.createElement(DefaultToast, { text: hintDisplayText.hintHoldFaceForFreshnessText });
103
+ }
104
+ if (isRecording && !isFlashingFreshness) {
105
+ // During face matching, we want to only show the TOO_CLOSE or
106
+ // TOO_FAR texts. If FaceMatchState matches TOO_CLOSE, we'll show
107
+ // the TOO_CLOSE text, but for FACE_IDENTIFED, CANT_IDENTIFY, TOO_MANY
108
+ // we are defaulting to the TOO_FAR text (for now).
109
+ let resultHintString = FaceMatchStateStringMap[FaceMatchState.TOO_FAR];
110
+ if (faceMatchState === FaceMatchState.TOO_CLOSE ||
111
+ faceMatchState === FaceMatchState.MATCHED) {
112
+ resultHintString = FaceMatchStateStringMap[faceMatchState];
113
+ }
114
+ // If the face is outside the oval set the aria-label to a string about centering face in oval
115
+ let a11yHintString = resultHintString;
116
+ if (faceMatchState === FaceMatchState.OFF_CENTER) {
117
+ a11yHintString = FaceMatchStateStringMap[faceMatchState];
118
+ }
119
+ else if (
120
+ // If the face match percentage reaches 50% append it to the a11y label
121
+ faceMatchState === FaceMatchState.TOO_FAR &&
122
+ faceMatchPercentage > 50) {
123
+ a11yHintString = hintDisplayText.hintMatchIndicatorText;
124
+ }
125
+ return (React.createElement(Toast, { size: "large", variation: faceMatchState === FaceMatchState.TOO_CLOSE ? 'error' : 'primary' },
126
+ React.createElement(VisuallyHidden, { "aria-live": "assertive" }, a11yHintString),
127
+ React.createElement(View, { "aria-label": a11yHintString }, resultHintString)));
128
+ }
129
+ return null;
113
130
  };
114
131
 
115
132
  export { Hint, selectErrorState, selectFaceMatchState, selectFaceMatchStateBeforeStart, selectIlluminationState, selectIsFaceFarEnoughBeforeRecording };
@@ -3,8 +3,8 @@ import { Flex, Loader, View } from '@aws-amplify/ui-react';
3
3
  import { LivenessClassNames } from '../types/classNames.mjs';
4
4
  import { Toast } from './Toast.mjs';
5
5
 
6
- const ToastWithLoader = ({ displayText, labelText, }) => {
7
- return (React.createElement(Toast, { "aria-live": "polite", "aria-label": labelText ?? displayText },
6
+ const ToastWithLoader = ({ displayText, }) => {
7
+ return (React.createElement(Toast, { "aria-live": "polite" },
8
8
  React.createElement(Flex, { className: LivenessClassNames.HintText },
9
9
  React.createElement(Loader, null),
10
10
  React.createElement(View, null, displayText))));
@@ -12,7 +12,7 @@ function getDisplayText(overrideDisplayText) {
12
12
  ...defaultLivenessDisplayText,
13
13
  ...overrideDisplayText,
14
14
  };
15
- const { a11yVideoLabelText, cameraMinSpecificationsHeadingText, cameraMinSpecificationsMessageText, cameraNotFoundHeadingText, cameraNotFoundMessageText, cancelLivenessCheckText, clientHeaderText, clientMessageText, hintCanNotIdentifyText, hintCenterFaceText, hintConnectingText, hintFaceDetectedText, hintHoldFaceForFreshnessText, hintIlluminationNormalText, hintIlluminationTooBrightText, hintIlluminationTooDarkText, hintMoveFaceFrontOfCameraText, hintTooManyFacesText, hintTooCloseText, hintTooFarText, hintVerifyingText, faceDistanceHeaderText, faceDistanceMessageText, goodFitCaptionText, goodFitAltText, landscapeHeaderText, landscapeMessageText, multipleFacesHeaderText, multipleFacesMessageText, photosensitivityWarningBodyText, photosensitivityWarningHeadingText, photosensitivityWarningInfoText, photosensitivityWarningLabelText, photosensitivyWarningBodyText, photosensitivyWarningHeadingText, photosensitivyWarningInfoText, photosensitivyWarningLabelText, portraitMessageText, retryCameraPermissionsText, recordingIndicatorText, serverHeaderText, serverMessageText, startScreenBeginCheckText, timeoutHeaderText, timeoutMessageText, tooFarCaptionText, tooFarAltText, tryAgainText, waitingCameraPermissionText, } = displayText;
15
+ const { a11yVideoLabelText, cameraMinSpecificationsHeadingText, cameraMinSpecificationsMessageText, cameraNotFoundHeadingText, cameraNotFoundMessageText, cancelLivenessCheckText, clientHeaderText, clientMessageText, errorLabelText, hintCanNotIdentifyText, hintCenterFaceText, hintCenterFaceInstructionText, hintFaceOffCenterText, hintConnectingText, hintFaceDetectedText, hintHoldFaceForFreshnessText, hintIlluminationNormalText, hintIlluminationTooBrightText, hintIlluminationTooDarkText, hintMoveFaceFrontOfCameraText, hintTooManyFacesText, hintTooCloseText, hintTooFarText, hintVerifyingText, hintCheckCompleteText, hintMatchIndicatorText, faceDistanceHeaderText, faceDistanceMessageText, goodFitCaptionText, goodFitAltText, landscapeHeaderText, landscapeMessageText, multipleFacesHeaderText, multipleFacesMessageText, photosensitivityWarningBodyText, photosensitivityWarningHeadingText, photosensitivityWarningInfoText, photosensitivityWarningLabelText, photosensitivyWarningBodyText, photosensitivyWarningHeadingText, photosensitivyWarningInfoText, photosensitivyWarningLabelText, portraitMessageText, retryCameraPermissionsText, recordingIndicatorText, serverHeaderText, serverMessageText, startScreenBeginCheckText, timeoutHeaderText, timeoutMessageText, tooFarCaptionText, tooFarAltText, tryAgainText, waitingCameraPermissionText, } = displayText;
16
16
  const hintDisplayText = {
17
17
  hintMoveFaceFrontOfCameraText,
18
18
  hintTooManyFacesText,
@@ -22,11 +22,15 @@ function getDisplayText(overrideDisplayText) {
22
22
  hintTooFarText,
23
23
  hintConnectingText,
24
24
  hintVerifyingText,
25
+ hintCheckCompleteText,
25
26
  hintIlluminationTooBrightText,
26
27
  hintIlluminationTooDarkText,
27
28
  hintIlluminationNormalText,
28
29
  hintHoldFaceForFreshnessText,
29
30
  hintCenterFaceText,
31
+ hintCenterFaceInstructionText,
32
+ hintFaceOffCenterText,
33
+ hintMatchIndicatorText,
30
34
  };
31
35
  const cameraDisplayText = {
32
36
  cameraMinSpecificationsHeadingText,
@@ -57,6 +61,7 @@ function getDisplayText(overrideDisplayText) {
57
61
  recordingIndicatorText,
58
62
  };
59
63
  const errorDisplayText = {
64
+ errorLabelText,
60
65
  timeoutHeaderText,
61
66
  timeoutMessageText,
62
67
  faceDistanceHeaderText,
@@ -1,3 +1,3 @@
1
- const VERSION = '3.0.8';
1
+ const VERSION = '3.0.9';
2
2
 
3
3
  export { VERSION };
package/dist/index.js CHANGED
@@ -78,6 +78,7 @@ var FaceMatchState;
78
78
  FaceMatchState["CANT_IDENTIFY"] = "CANNOT IDENTIFY";
79
79
  FaceMatchState["FACE_IDENTIFIED"] = "ONE FACE IDENTIFIED";
80
80
  FaceMatchState["TOO_MANY"] = "TOO MANY FACES";
81
+ FaceMatchState["OFF_CENTER"] = "OFF CENTER";
81
82
  })(FaceMatchState || (FaceMatchState = {}));
82
83
 
83
84
  /**
@@ -272,6 +273,16 @@ function drawLivenessOvalInCanvas({ canvas, oval, scaleFactor, videoEl, isStartS
272
273
  throw new Error('Cannot find Canvas.');
273
274
  }
274
275
  }
276
+ function clearOvalCanvas({ canvas, }) {
277
+ const ctx = canvas.getContext('2d');
278
+ if (ctx) {
279
+ ctx.restore();
280
+ ctx.clearRect(0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
281
+ }
282
+ else {
283
+ throw new Error('Cannot find Canvas.');
284
+ }
285
+ }
275
286
  /**
276
287
  * Returns the state of the provided face with respect to the provided liveness oval.
277
288
  */
@@ -306,12 +317,17 @@ function getFaceMatchStateInLivenessOval(face, ovalDetails, initialFaceIntersect
306
317
  const faceMatchPercentage = Math.max(Math.min(1, (0.75 * (intersection - initialFaceIntersection)) /
307
318
  (intersectionThreshold - initialFaceIntersection) +
308
319
  0.25), 0) * 100;
320
+ const faceIsOutsideOvalToTheLeft = minOvalX > minFaceX && maxOvalX > maxFaceX;
321
+ const faceIsOutsideOvalToTheRight = minFaceX > minOvalX && maxFaceX > maxOvalX;
309
322
  if (intersection > intersectionThreshold &&
310
323
  Math.abs(minOvalX - minFaceX) < ovalMatchWidthThreshold &&
311
324
  Math.abs(maxOvalX - maxFaceX) < ovalMatchWidthThreshold &&
312
325
  Math.abs(maxOvalY - maxFaceY) < ovalMatchHeightThreshold) {
313
326
  faceMatchState = FaceMatchState.MATCHED;
314
327
  }
328
+ else if (faceIsOutsideOvalToTheLeft || faceIsOutsideOvalToTheRight) {
329
+ faceMatchState = FaceMatchState.OFF_CENTER;
330
+ }
315
331
  else if (minOvalY - minFaceY > faceDetectionHeightThreshold ||
316
332
  maxFaceY - maxOvalY > faceDetectionHeightThreshold ||
317
333
  (minOvalX - minFaceX > faceDetectionWidthThreshold &&
@@ -771,7 +787,7 @@ class VideoRecorder {
771
787
  }
772
788
  }
773
789
 
774
- const VERSION = '3.0.8';
790
+ const VERSION = '3.0.9';
775
791
 
776
792
  const BASE_USER_AGENT = `ui-react-liveness/${VERSION}`;
777
793
  const getLivenessUserAgent = () => {
@@ -2530,8 +2546,8 @@ const Toast = ({ variation = 'default', size = 'medium', children, isInitial = f
2530
2546
  React__namespace.createElement(uiReact.Flex, { className: LivenessClassNames.ToastMessage, ...(isInitial ? { color: tokens.colors.font.primary } : {}) }, children))));
2531
2547
  };
2532
2548
 
2533
- const ToastWithLoader = ({ displayText, labelText, }) => {
2534
- return (React__namespace.createElement(Toast, { "aria-live": "polite", "aria-label": labelText ?? displayText },
2549
+ const ToastWithLoader = ({ displayText, }) => {
2550
+ return (React__namespace.createElement(Toast, { "aria-live": "polite" },
2535
2551
  React__namespace.createElement(uiReact.Flex, { className: LivenessClassNames.HintText },
2536
2552
  React__namespace.createElement(uiReact.Loader, null),
2537
2553
  React__namespace.createElement(uiReact.View, null, displayText))));
@@ -2542,9 +2558,10 @@ const selectFaceMatchState$1 = createLivenessSelector((state) => state.context.f
2542
2558
  const selectIlluminationState = createLivenessSelector((state) => state.context.faceMatchAssociatedParams.illuminationState);
2543
2559
  const selectIsFaceFarEnoughBeforeRecording = createLivenessSelector((state) => state.context.isFaceFarEnoughBeforeRecording);
2544
2560
  const selectFaceMatchStateBeforeStart = createLivenessSelector((state) => state.context.faceMatchStateBeforeStart);
2545
- const defaultToast = (text, isInitial = false) => {
2561
+ const selectFaceMatchPercentage$1 = createLivenessSelector((state) => state.context.faceMatchAssociatedParams?.faceMatchPercentage);
2562
+ const DefaultToast = ({ text, isInitial = false, }) => {
2546
2563
  return (React__namespace.createElement(Toast, { size: "large", variation: "primary", isInitial: isInitial },
2547
- React__namespace.createElement(uiReact.View, { "aria-live": "assertive", "aria-label": text }, text)));
2564
+ React__namespace.createElement(uiReact.View, { "aria-live": "assertive" }, text)));
2548
2565
  };
2549
2566
  const Hint = ({ hintDisplayText }) => {
2550
2567
  const [state] = useLivenessActor();
@@ -2554,8 +2571,11 @@ const Hint = ({ hintDisplayText }) => {
2554
2571
  const illuminationState = useLivenessSelector(selectIlluminationState);
2555
2572
  const faceMatchStateBeforeStart = useLivenessSelector(selectFaceMatchStateBeforeStart);
2556
2573
  const isFaceFarEnoughBeforeRecordingState = useLivenessSelector(selectIsFaceFarEnoughBeforeRecording);
2557
- const isCheckFaceDetectedBeforeStart = state.matches('checkFaceDetectedBeforeStart');
2558
- const isCheckFaceDistanceBeforeRecording = state.matches('checkFaceDistanceBeforeRecording');
2574
+ const faceMatchPercentage = useLivenessSelector(selectFaceMatchPercentage$1);
2575
+ const isCheckFaceDetectedBeforeStart = state.matches('checkFaceDetectedBeforeStart') ||
2576
+ state.matches('detectFaceBeforeStart');
2577
+ const isCheckFaceDistanceBeforeRecording = state.matches('checkFaceDistanceBeforeRecording') ||
2578
+ state.matches('detectFaceDistanceBeforeRecording');
2559
2579
  const isStartView = state.matches('start') || state.matches('userCancel');
2560
2580
  const isRecording = state.matches('recording');
2561
2581
  const isNotRecording = state.matches('notRecording');
@@ -2572,61 +2592,74 @@ const Hint = ({ hintDisplayText }) => {
2572
2592
  [FaceMatchState.TOO_CLOSE]: hintDisplayText.hintTooCloseText,
2573
2593
  [FaceMatchState.TOO_FAR]: hintDisplayText.hintTooFarText,
2574
2594
  [FaceMatchState.MATCHED]: hintDisplayText.hintHoldFaceForFreshnessText,
2595
+ [FaceMatchState.OFF_CENTER]: hintDisplayText.hintFaceOffCenterText,
2575
2596
  };
2576
2597
  const IlluminationStateStringMap = {
2577
2598
  [IlluminationState.BRIGHT]: hintDisplayText.hintIlluminationTooBrightText,
2578
2599
  [IlluminationState.DARK]: hintDisplayText.hintIlluminationTooDarkText,
2579
2600
  [IlluminationState.NORMAL]: hintDisplayText.hintIlluminationNormalText,
2580
2601
  };
2581
- const getInstructionContent = () => {
2582
- if (isStartView) {
2583
- return defaultToast(hintDisplayText.hintCenterFaceText, true);
2602
+ if (isStartView) {
2603
+ return (React__namespace.createElement(React__namespace.Fragment, null,
2604
+ React__namespace.createElement(uiReact.VisuallyHidden, { role: "alert" }, hintDisplayText.hintCenterFaceInstructionText),
2605
+ React__namespace.createElement(DefaultToast, { text: hintDisplayText.hintCenterFaceText, isInitial: true })));
2606
+ }
2607
+ if (errorState ?? (isCheckFailed || isCheckSuccessful)) {
2608
+ return null;
2609
+ }
2610
+ if (!isRecording) {
2611
+ if (isCheckFaceDetectedBeforeStart) {
2612
+ if (faceMatchStateBeforeStart === FaceMatchState.TOO_MANY) {
2613
+ return React__namespace.createElement(DefaultToast, { text: hintDisplayText.hintTooManyFacesText });
2614
+ }
2615
+ return (React__namespace.createElement(DefaultToast, { text: hintDisplayText.hintMoveFaceFrontOfCameraText }));
2584
2616
  }
2585
- if (errorState ?? (isCheckFailed || isCheckSuccessful)) {
2586
- return;
2617
+ // Specifically checking for false here because initially the value is undefined and we do not want to show the instruction
2618
+ if (isCheckFaceDistanceBeforeRecording &&
2619
+ isFaceFarEnoughBeforeRecordingState === false) {
2620
+ return React__namespace.createElement(DefaultToast, { text: hintDisplayText.hintTooCloseText });
2587
2621
  }
2588
- if (!isRecording) {
2589
- if (isCheckFaceDetectedBeforeStart) {
2590
- if (faceMatchStateBeforeStart === FaceMatchState.TOO_MANY) {
2591
- return defaultToast(FaceMatchStateStringMap[faceMatchStateBeforeStart]);
2592
- }
2593
- return defaultToast(hintDisplayText.hintMoveFaceFrontOfCameraText);
2594
- }
2595
- // Specifically checking for false here because initially the value is undefined and we do not want to show the instruction
2596
- if (isCheckFaceDistanceBeforeRecording &&
2597
- isFaceFarEnoughBeforeRecordingState === false) {
2598
- return defaultToast(hintDisplayText.hintTooCloseText);
2599
- }
2600
- if (isNotRecording) {
2601
- return (React__namespace.createElement(ToastWithLoader, { displayText: hintDisplayText.hintConnectingText }));
2602
- }
2603
- if (isUploading) {
2604
- return (React__namespace.createElement(ToastWithLoader, { displayText: hintDisplayText.hintVerifyingText }));
2605
- }
2606
- if (illuminationState && illuminationState !== IlluminationState.NORMAL) {
2607
- return defaultToast(IlluminationStateStringMap[illuminationState]);
2608
- }
2622
+ if (isNotRecording) {
2623
+ return (React__namespace.createElement(ToastWithLoader, { displayText: hintDisplayText.hintConnectingText }));
2609
2624
  }
2610
- if (isFlashingFreshness) {
2611
- return defaultToast(hintDisplayText.hintHoldFaceForFreshnessText);
2625
+ if (isUploading) {
2626
+ return (React__namespace.createElement(React__namespace.Fragment, null,
2627
+ React__namespace.createElement(uiReact.VisuallyHidden, { "aria-live": "assertive" }, hintDisplayText.hintCheckCompleteText),
2628
+ React__namespace.createElement(ToastWithLoader, { displayText: hintDisplayText.hintVerifyingText })));
2612
2629
  }
2613
- if (isRecording && !isFlashingFreshness) {
2614
- // During face matching, we want to only show the TOO_CLOSE or
2615
- // TOO_FAR texts. If FaceMatchState matches TOO_CLOSE, we'll show
2616
- // the TOO_CLOSE text, but for FACE_IDENTIFED, CANT_IDENTIFY, TOO_MANY
2617
- // we are defaulting to the TOO_FAR text (for now).
2618
- let resultHintString = FaceMatchStateStringMap[FaceMatchState.TOO_FAR];
2619
- if (faceMatchState === FaceMatchState.TOO_CLOSE ||
2620
- faceMatchState === FaceMatchState.MATCHED) {
2621
- resultHintString = FaceMatchStateStringMap[faceMatchState];
2622
- }
2623
- return (React__namespace.createElement(Toast, { size: "large", variation: faceMatchState === FaceMatchState.TOO_CLOSE ? 'error' : 'primary' },
2624
- React__namespace.createElement(uiReact.View, { "aria-live": "assertive", "aria-label": resultHintString }, resultHintString)));
2630
+ if (illuminationState && illuminationState !== IlluminationState.NORMAL) {
2631
+ return (React__namespace.createElement(DefaultToast, { text: IlluminationStateStringMap[illuminationState] }));
2625
2632
  }
2626
- return null;
2627
- };
2628
- const instructionContent = getInstructionContent();
2629
- return instructionContent ? instructionContent : null;
2633
+ }
2634
+ if (isFlashingFreshness) {
2635
+ return React__namespace.createElement(DefaultToast, { text: hintDisplayText.hintHoldFaceForFreshnessText });
2636
+ }
2637
+ if (isRecording && !isFlashingFreshness) {
2638
+ // During face matching, we want to only show the TOO_CLOSE or
2639
+ // TOO_FAR texts. If FaceMatchState matches TOO_CLOSE, we'll show
2640
+ // the TOO_CLOSE text, but for FACE_IDENTIFED, CANT_IDENTIFY, TOO_MANY
2641
+ // we are defaulting to the TOO_FAR text (for now).
2642
+ let resultHintString = FaceMatchStateStringMap[FaceMatchState.TOO_FAR];
2643
+ if (faceMatchState === FaceMatchState.TOO_CLOSE ||
2644
+ faceMatchState === FaceMatchState.MATCHED) {
2645
+ resultHintString = FaceMatchStateStringMap[faceMatchState];
2646
+ }
2647
+ // If the face is outside the oval set the aria-label to a string about centering face in oval
2648
+ let a11yHintString = resultHintString;
2649
+ if (faceMatchState === FaceMatchState.OFF_CENTER) {
2650
+ a11yHintString = FaceMatchStateStringMap[faceMatchState];
2651
+ }
2652
+ else if (
2653
+ // If the face match percentage reaches 50% append it to the a11y label
2654
+ faceMatchState === FaceMatchState.TOO_FAR &&
2655
+ faceMatchPercentage > 50) {
2656
+ a11yHintString = hintDisplayText.hintMatchIndicatorText;
2657
+ }
2658
+ return (React__namespace.createElement(Toast, { size: "large", variation: faceMatchState === FaceMatchState.TOO_CLOSE ? 'error' : 'primary' },
2659
+ React__namespace.createElement(uiReact.VisuallyHidden, { "aria-live": "assertive" }, a11yHintString),
2660
+ React__namespace.createElement(uiReact.View, { "aria-label": a11yHintString }, resultHintString)));
2661
+ }
2662
+ return null;
2630
2663
  };
2631
2664
 
2632
2665
  const MatchIndicator = ({ percentage, initialPercentage = 25, testId, }) => {
@@ -2662,6 +2695,7 @@ const RecordingIcon = ({ children }) => {
2662
2695
  };
2663
2696
 
2664
2697
  const defaultErrorDisplayText = {
2698
+ errorLabelText: 'Error',
2665
2699
  timeoutHeaderText: 'Time out',
2666
2700
  timeoutMessageText: "Face didn't fit inside oval in time limit. Try again and completely fill the oval with face in it.",
2667
2701
  faceDistanceHeaderText: 'Forward movement detected',
@@ -2687,6 +2721,8 @@ const defaultLivenessDisplayText = {
2687
2721
  goodFitCaptionText: 'Good fit',
2688
2722
  goodFitAltText: "Ilustration of a person's face, perfectly fitting inside of an oval.",
2689
2723
  hintCenterFaceText: 'Center your face',
2724
+ hintCenterFaceInstructionText: 'Instruction: Before starting the check, make sure your camera is at the center top of your screen and center your face to the camera. When the check starts an oval will show up in the center. You will be prompted to move forward into the oval and then prompted to hold still. After holding still for a few seconds, you should hear check complete.',
2725
+ hintFaceOffCenterText: 'Face is not in the oval, center your face to the camera.',
2690
2726
  hintMoveFaceFrontOfCameraText: 'Move face in front of camera',
2691
2727
  hintTooManyFacesText: 'Ensure only one face is in front of camera',
2692
2728
  hintFaceDetectedText: 'Face detected',
@@ -2695,10 +2731,12 @@ const defaultLivenessDisplayText = {
2695
2731
  hintTooFarText: 'Move closer',
2696
2732
  hintConnectingText: 'Connecting...',
2697
2733
  hintVerifyingText: 'Verifying...',
2734
+ hintCheckCompleteText: 'Check complete',
2698
2735
  hintIlluminationTooBrightText: 'Move to dimmer area',
2699
2736
  hintIlluminationTooDarkText: 'Move to brighter area',
2700
2737
  hintIlluminationNormalText: 'Lighting conditions normal',
2701
2738
  hintHoldFaceForFreshnessText: 'Hold still',
2739
+ hintMatchIndicatorText: '50% completed. Keep moving closer.',
2702
2740
  photosensitivityWarningBodyText: 'This check flashes different colors. Use caution if you are photosensitive.',
2703
2741
  photosensitivityWarningHeadingText: 'Photosensitivity warning',
2704
2742
  photosensitivityWarningInfoText: 'Some people may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition.',
@@ -2718,7 +2756,7 @@ const defaultLivenessDisplayText = {
2718
2756
 
2719
2757
  const renderToastErrorModal = (props) => {
2720
2758
  const { error: errorState, displayText } = props;
2721
- const { timeoutHeaderText, timeoutMessageText, faceDistanceHeaderText, faceDistanceMessageText, multipleFacesHeaderText, multipleFacesMessageText, clientHeaderText, clientMessageText, serverHeaderText, serverMessageText, } = displayText;
2759
+ const { errorLabelText, timeoutHeaderText, timeoutMessageText, faceDistanceHeaderText, faceDistanceMessageText, multipleFacesHeaderText, multipleFacesMessageText, clientHeaderText, clientMessageText, serverHeaderText, serverMessageText, } = displayText;
2722
2760
  let heading;
2723
2761
  let message;
2724
2762
  switch (errorState) {
@@ -2745,7 +2783,7 @@ const renderToastErrorModal = (props) => {
2745
2783
  }
2746
2784
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
2747
2785
  React__default["default"].createElement(uiReact.Flex, { className: LivenessClassNames.ErrorModal },
2748
- React__default["default"].createElement(internal.AlertIcon, { ariaHidden: true, variation: "error" }),
2786
+ React__default["default"].createElement(internal.AlertIcon, { ariaLabel: errorLabelText, role: "img", variation: "error" }),
2749
2787
  React__default["default"].createElement(uiReact.Text, { className: LivenessClassNames.ErrorModalHeading, id: "amplify-liveness-error-heading" }, heading)),
2750
2788
  React__default["default"].createElement(uiReact.Text, { id: "amplify-liveness-error-message" }, message)));
2751
2789
  };
@@ -2839,6 +2877,7 @@ const showMatchIndicatorStates = [
2839
2877
  FaceMatchState.TOO_FAR,
2840
2878
  FaceMatchState.CANT_IDENTIFY,
2841
2879
  FaceMatchState.FACE_IDENTIFIED,
2880
+ FaceMatchState.OFF_CENTER,
2842
2881
  ];
2843
2882
  /**
2844
2883
  * For now we want to memoize the HOC for MatchIndicator because to optimize renders
@@ -2866,6 +2905,7 @@ const LivenessCameraModule = (props) => {
2866
2905
  const isCheckingCamera = state.matches('cameraCheck');
2867
2906
  const isWaitingForCamera = state.matches('waitForDOMAndCameraDetails');
2868
2907
  const isStartView = state.matches('start') || state.matches('userCancel');
2908
+ const isDetectFaceBeforeStart = state.matches('detectFaceBeforeStart');
2869
2909
  const isRecording = state.matches('recording');
2870
2910
  const isCheckSucceeded = state.matches('checkSucceeded');
2871
2911
  const isFlashingFreshness = state.matches({
@@ -2926,6 +2966,11 @@ const LivenessCameraModule = (props) => {
2926
2966
  setAspectRatio(videoRef.current.videoWidth / videoRef.current.videoHeight);
2927
2967
  }
2928
2968
  }, [send, videoRef, isCameraReady, isMobileScreen]);
2969
+ React__default["default"].useEffect(() => {
2970
+ if (isDetectFaceBeforeStart) {
2971
+ clearOvalCanvas({ canvas: canvasRef.current });
2972
+ }
2973
+ }, [isDetectFaceBeforeStart]);
2929
2974
  const photoSensitivityWarning = React__default["default"].useMemo(() => {
2930
2975
  return (React__default["default"].createElement(uiReact.View, { style: { visibility: isStartView ? 'visible' : 'hidden' } },
2931
2976
  React__default["default"].createElement(PhotosensitiveWarning, { bodyText: instructionDisplayText.photosensitivityWarningBodyText, headingText: instructionDisplayText.photosensitivityWarningHeadingText, infoText: instructionDisplayText.photosensitivityWarningInfoText, labelText: instructionDisplayText.photosensitivityWarningLabelText })));
@@ -2960,31 +3005,35 @@ const LivenessCameraModule = (props) => {
2960
3005
  React__default["default"].createElement(uiReact.Loader, { size: "large", className: LivenessClassNames.Loader, "data-testid": "centered-loader", position: "unset" }),
2961
3006
  React__default["default"].createElement(uiReact.Text, { fontSize: "large", fontWeight: "bold", "data-testid": "waiting-camera-permission", className: `${LivenessClassNames.StartScreenCameraWaiting}__text` }, cameraDisplayText.waitingCameraPermissionText)));
2962
3007
  }
2963
- const isRecordingOnMobile = isMobileScreen && !isStartView && !isWaitingForCamera && isRecording;
3008
+ // We don't show full screen camera on the pre check screen (isStartView/isWaitingForCamera)
3009
+ const shouldShowFullScreenCamera = isMobileScreen && !isStartView && !isWaitingForCamera;
2964
3010
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
2965
3011
  photoSensitivityWarning,
2966
- React__default["default"].createElement(uiReact.Flex, { className: ui.classNames(LivenessClassNames.CameraModule, isRecordingOnMobile && `${LivenessClassNames.CameraModule}--mobile`), "data-testid": testId, gap: "zero" },
3012
+ React__default["default"].createElement(uiReact.Flex, { className: ui.classNames(LivenessClassNames.CameraModule, shouldShowFullScreenCamera &&
3013
+ `${LivenessClassNames.CameraModule}--mobile`), "data-testid": testId, gap: "zero" },
2967
3014
  !isCameraReady && centeredLoader,
3015
+ React__default["default"].createElement(Overlay, { horizontal: "center", vertical: isRecording && !isFlashingFreshness ? 'start' : 'space-between', className: LivenessClassNames.InstructionOverlay },
3016
+ isRecording && (React__default["default"].createElement(DefaultRecordingIcon, { recordingIndicatorText: recordingIndicatorText })),
3017
+ !isStartView && !isWaitingForCamera && !isCheckSucceeded && (React__default["default"].createElement(DefaultCancelButton, { cancelLivenessCheckText: cancelLivenessCheckText })),
3018
+ React__default["default"].createElement(uiReact.Flex, { className: ui.classNames(LivenessClassNames.Hint, shouldShowFullScreenCamera && `${LivenessClassNames.Hint}--mobile`) },
3019
+ React__default["default"].createElement(Hint, { hintDisplayText: hintDisplayText })),
3020
+ errorState && (React__default["default"].createElement(ErrorView, { onRetry: () => {
3021
+ send({ type: 'CANCEL' });
3022
+ }, displayText: errorDisplayText }, renderErrorModal({
3023
+ errorState,
3024
+ overrideErrorDisplayText: errorDisplayText,
3025
+ }))),
3026
+ isRecording &&
3027
+ !isFlashingFreshness &&
3028
+ showMatchIndicatorStates.includes(faceMatchState) ? (React__default["default"].createElement(MemoizedMatchIndicator, { percentage: Math.ceil(faceMatchPercentage) })) : null),
2968
3029
  React__default["default"].createElement(uiReact.View, { as: "canvas", ref: freshnessColorRef, className: LivenessClassNames.FreshnessCanvas, hidden: true }),
2969
3030
  React__default["default"].createElement(uiReact.View, { className: LivenessClassNames.VideoAnchor, style: {
2970
3031
  aspectRatio: `${aspectRatio}`,
2971
3032
  } },
2972
3033
  React__default["default"].createElement("video", { ref: videoRef, muted: true, autoPlay: true, playsInline: true, width: mediaWidth, height: mediaHeight, onCanPlay: handleMediaPlay, "data-testid": "video", className: LivenessClassNames.Video, "aria-label": cameraDisplayText.a11yVideoLabelText }),
2973
- React__default["default"].createElement(uiReact.Flex, { className: ui.classNames(LivenessClassNames.OvalCanvas, isRecordingOnMobile && `${LivenessClassNames.OvalCanvas}--mobile`, isRecordingStopped && LivenessClassNames.FadeOut) },
3034
+ React__default["default"].createElement(uiReact.Flex, { className: ui.classNames(LivenessClassNames.OvalCanvas, shouldShowFullScreenCamera &&
3035
+ `${LivenessClassNames.OvalCanvas}--mobile`, isRecordingStopped && LivenessClassNames.FadeOut) },
2974
3036
  React__default["default"].createElement(uiReact.View, { as: "canvas", ref: canvasRef })),
2975
- isRecording && (React__default["default"].createElement(DefaultRecordingIcon, { recordingIndicatorText: recordingIndicatorText })),
2976
- !isStartView && !isWaitingForCamera && !isCheckSucceeded && (React__default["default"].createElement(DefaultCancelButton, { cancelLivenessCheckText: cancelLivenessCheckText })),
2977
- React__default["default"].createElement(Overlay, { horizontal: "center", vertical: isRecording && !isFlashingFreshness ? 'start' : 'space-between', className: LivenessClassNames.InstructionOverlay },
2978
- React__default["default"].createElement(Hint, { hintDisplayText: hintDisplayText }),
2979
- errorState && (React__default["default"].createElement(ErrorView, { onRetry: () => {
2980
- send({ type: 'CANCEL' });
2981
- }, displayText: errorDisplayText }, renderErrorModal({
2982
- errorState,
2983
- overrideErrorDisplayText: errorDisplayText,
2984
- }))),
2985
- isRecording &&
2986
- !isFlashingFreshness &&
2987
- showMatchIndicatorStates.includes(faceMatchState) ? (React__default["default"].createElement(MemoizedMatchIndicator, { percentage: Math.ceil(faceMatchPercentage) })) : null),
2988
3037
  isStartView &&
2989
3038
  !isMobileScreen &&
2990
3039
  selectableDevices &&
@@ -3123,7 +3172,7 @@ function getDisplayText(overrideDisplayText) {
3123
3172
  ...defaultLivenessDisplayText,
3124
3173
  ...overrideDisplayText,
3125
3174
  };
3126
- const { a11yVideoLabelText, cameraMinSpecificationsHeadingText, cameraMinSpecificationsMessageText, cameraNotFoundHeadingText, cameraNotFoundMessageText, cancelLivenessCheckText, clientHeaderText, clientMessageText, hintCanNotIdentifyText, hintCenterFaceText, hintConnectingText, hintFaceDetectedText, hintHoldFaceForFreshnessText, hintIlluminationNormalText, hintIlluminationTooBrightText, hintIlluminationTooDarkText, hintMoveFaceFrontOfCameraText, hintTooManyFacesText, hintTooCloseText, hintTooFarText, hintVerifyingText, faceDistanceHeaderText, faceDistanceMessageText, goodFitCaptionText, goodFitAltText, landscapeHeaderText, landscapeMessageText, multipleFacesHeaderText, multipleFacesMessageText, photosensitivityWarningBodyText, photosensitivityWarningHeadingText, photosensitivityWarningInfoText, photosensitivityWarningLabelText, photosensitivyWarningBodyText, photosensitivyWarningHeadingText, photosensitivyWarningInfoText, photosensitivyWarningLabelText, portraitMessageText, retryCameraPermissionsText, recordingIndicatorText, serverHeaderText, serverMessageText, startScreenBeginCheckText, timeoutHeaderText, timeoutMessageText, tooFarCaptionText, tooFarAltText, tryAgainText, waitingCameraPermissionText, } = displayText;
3175
+ const { a11yVideoLabelText, cameraMinSpecificationsHeadingText, cameraMinSpecificationsMessageText, cameraNotFoundHeadingText, cameraNotFoundMessageText, cancelLivenessCheckText, clientHeaderText, clientMessageText, errorLabelText, hintCanNotIdentifyText, hintCenterFaceText, hintCenterFaceInstructionText, hintFaceOffCenterText, hintConnectingText, hintFaceDetectedText, hintHoldFaceForFreshnessText, hintIlluminationNormalText, hintIlluminationTooBrightText, hintIlluminationTooDarkText, hintMoveFaceFrontOfCameraText, hintTooManyFacesText, hintTooCloseText, hintTooFarText, hintVerifyingText, hintCheckCompleteText, hintMatchIndicatorText, faceDistanceHeaderText, faceDistanceMessageText, goodFitCaptionText, goodFitAltText, landscapeHeaderText, landscapeMessageText, multipleFacesHeaderText, multipleFacesMessageText, photosensitivityWarningBodyText, photosensitivityWarningHeadingText, photosensitivityWarningInfoText, photosensitivityWarningLabelText, photosensitivyWarningBodyText, photosensitivyWarningHeadingText, photosensitivyWarningInfoText, photosensitivyWarningLabelText, portraitMessageText, retryCameraPermissionsText, recordingIndicatorText, serverHeaderText, serverMessageText, startScreenBeginCheckText, timeoutHeaderText, timeoutMessageText, tooFarCaptionText, tooFarAltText, tryAgainText, waitingCameraPermissionText, } = displayText;
3127
3176
  const hintDisplayText = {
3128
3177
  hintMoveFaceFrontOfCameraText,
3129
3178
  hintTooManyFacesText,
@@ -3133,11 +3182,15 @@ function getDisplayText(overrideDisplayText) {
3133
3182
  hintTooFarText,
3134
3183
  hintConnectingText,
3135
3184
  hintVerifyingText,
3185
+ hintCheckCompleteText,
3136
3186
  hintIlluminationTooBrightText,
3137
3187
  hintIlluminationTooDarkText,
3138
3188
  hintIlluminationNormalText,
3139
3189
  hintHoldFaceForFreshnessText,
3140
3190
  hintCenterFaceText,
3191
+ hintCenterFaceInstructionText,
3192
+ hintFaceOffCenterText,
3193
+ hintMatchIndicatorText,
3141
3194
  };
3142
3195
  const cameraDisplayText = {
3143
3196
  cameraMinSpecificationsHeadingText,
@@ -3168,6 +3221,7 @@ function getDisplayText(overrideDisplayText) {
3168
3221
  recordingIndicatorText,
3169
3222
  };
3170
3223
  const errorDisplayText = {
3224
+ errorLabelText,
3171
3225
  timeoutHeaderText,
3172
3226
  timeoutMessageText,
3173
3227
  faceDistanceHeaderText,
package/dist/styles.css CHANGED
@@ -3911,6 +3911,12 @@ html[dir=rtl] .amplify-field-group__inner-start {
3911
3911
  right: var(--amplify-space-medium);
3912
3912
  }
3913
3913
 
3914
+ .liveness-detector .amplify-button--primary:focus {
3915
+ box-shadow: unset;
3916
+ outline: var(--amplify-components-button-focus-color) solid 2px;
3917
+ outline-offset: 2px;
3918
+ }
3919
+
3914
3920
  .amplify-liveness-cancel-button {
3915
3921
  background-color: #fff;
3916
3922
  color: hsl(190, 95%, 30%);
@@ -3945,6 +3951,7 @@ html[dir=rtl] .amplify-field-group__inner-start {
3945
3951
  left: 0;
3946
3952
  height: 100%;
3947
3953
  width: 100%;
3954
+ z-index: 2;
3948
3955
  }
3949
3956
 
3950
3957
  .amplify-liveness-video {
@@ -4014,7 +4021,7 @@ html[dir=rtl] .amplify-field-group__inner-start {
4014
4021
  }
4015
4022
 
4016
4023
  .amplify-liveness-instruction-overlay {
4017
- z-index: 1;
4024
+ z-index: 2;
4018
4025
  }
4019
4026
 
4020
4027
  .amplify-liveness-countdown-container {
@@ -4248,6 +4255,10 @@ html[dir=rtl] .amplify-field-group__inner-start {
4248
4255
  font-weight: var(--amplify-font-weights-bold);
4249
4256
  }
4250
4257
 
4258
+ .amplify-liveness-hint--mobile {
4259
+ margin-top: var(--amplify-space-xxxl);
4260
+ }
4261
+
4251
4262
  .amplify-liveness-hint__text {
4252
4263
  align-items: center;
4253
4264
  gap: var(--amplify-space-xs);
@@ -4325,6 +4336,7 @@ html[dir=rtl] .amplify-field-group__inner-start {
4325
4336
  flex-direction: column;
4326
4337
  align-items: center;
4327
4338
  justify-content: center;
4339
+ text-align: center;
4328
4340
  height: 480px;
4329
4341
  }
4330
4342
 
@@ -5327,6 +5339,9 @@ html[dir=rtl] .amplify-field-group__inner-start {
5327
5339
  var(--amplify-components-table-header-border-width)
5328
5340
  var(--amplify-components-table-header-border-width);
5329
5341
  }
5342
+ .amplify-table--striped .amplify-table__row:not(.amplify-table__head *):nth-child(odd) {
5343
+ background-color: var(--amplify-components-table-row-striped-background-color);
5344
+ }
5330
5345
  .amplify-table__caption {
5331
5346
  caption-side: var(--amplify-components-table-caption-caption-side);
5332
5347
  color: var(--amplify-components-table-caption-color);
@@ -5385,9 +5400,6 @@ html[dir=rtl] .amplify-field-group__inner-start {
5385
5400
  .amplify-table__td:last-child {
5386
5401
  border-right-width: var(--amplify-components-table-data-border-width);
5387
5402
  }
5388
- .amplify-table[data-variation=striped] .amplify-table__row:not(.amplify-table__head *):nth-child(odd) {
5389
- background-color: var(--amplify-components-table-row-striped-background-color);
5390
- }
5391
5403
  .amplify-table[data-highlightonhover=true] .amplify-table__row:not(.amplify-table__head *):hover {
5392
5404
  background-color: var(--amplify-components-table-row-hover-background-color);
5393
5405
  }
@@ -1,3 +1,4 @@
1
+ import { DisplayTextTemplate } from '@aws-amplify/ui';
1
2
  export type HintDisplayText = {
2
3
  hintMoveFaceFrontOfCameraText?: string;
3
4
  hintTooManyFacesText?: string;
@@ -7,11 +8,15 @@ export type HintDisplayText = {
7
8
  hintTooFarText?: string;
8
9
  hintConnectingText?: string;
9
10
  hintVerifyingText?: string;
11
+ hintCheckCompleteText?: string;
10
12
  hintIlluminationTooBrightText?: string;
11
13
  hintIlluminationTooDarkText?: string;
12
14
  hintIlluminationNormalText?: string;
13
15
  hintHoldFaceForFreshnessText?: string;
14
16
  hintCenterFaceText?: string;
17
+ hintCenterFaceInstructionText?: string;
18
+ hintFaceOffCenterText?: string;
19
+ hintMatchIndicatorText?: string;
15
20
  };
16
21
  export type CameraDisplayText = {
17
22
  cameraMinSpecificationsHeadingText?: string;
@@ -29,19 +34,32 @@ export type InstructionDisplayText = {
29
34
  photosensitivityWarningHeadingText?: string;
30
35
  photosensitivityWarningInfoText?: string;
31
36
  photosensitivityWarningLabelText?: string;
37
+ startScreenBeginCheckText?: string;
38
+ tooFarCaptionText?: string;
39
+ tooFarAltText?: string;
40
+ /**
41
+ * @deprecated `photosensitivyWarningBodyText` has been replaced with `photosensitivityWarningBodyText` amd will be removed in a future major version of `@aws-amplify/ui-react-liveness`
42
+ */
32
43
  photosensitivyWarningBodyText?: string;
44
+ /**
45
+ * @deprecated `photosensitivyWarningHeadingText` has been replaced with `photosensitivityWarningHeadingText` amd will be removed in a future major version of `@aws-amplify/ui-react-liveness`
46
+ */
33
47
  photosensitivyWarningHeadingText?: string;
48
+ /**
49
+ * @deprecated `photosensitivyWarningInfoText` has been replaced with `photosensitivityWarningInfoText` amd will be removed in a future major version of `@aws-amplify/ui-react-liveness`
50
+ */
34
51
  photosensitivyWarningInfoText?: string;
52
+ /**
53
+ * @deprecated `photosensitivyWarningLabelText` has been replaced with `photosensitivityWarningLabelText` amd will be removed in a future major version of `@aws-amplify/ui-react-liveness`
54
+ */
35
55
  photosensitivyWarningLabelText?: string;
36
- startScreenBeginCheckText?: string;
37
- tooFarCaptionText?: string;
38
- tooFarAltText?: string;
39
56
  };
40
57
  export type StreamDisplayText = {
41
58
  recordingIndicatorText?: string;
42
59
  cancelLivenessCheckText?: string;
43
60
  };
44
61
  export declare const defaultErrorDisplayText: {
62
+ errorLabelText: string;
45
63
  timeoutHeaderText: string;
46
64
  timeoutMessageText: string;
47
65
  faceDistanceHeaderText: string;
@@ -57,8 +75,6 @@ export declare const defaultErrorDisplayText: {
57
75
  portraitMessageText: string;
58
76
  tryAgainText: string;
59
77
  };
60
- export type ErrorDisplayTextFoo = typeof defaultErrorDisplayText;
61
- export type ErrorDisplayText = Partial<ErrorDisplayTextFoo>;
78
+ export type ErrorDisplayText = Partial<typeof defaultErrorDisplayText>;
62
79
  export declare const defaultLivenessDisplayText: Required<LivenessDisplayText>;
63
- export interface LivenessDisplayText extends HintDisplayText, CameraDisplayText, InstructionDisplayText, ErrorDisplayText, StreamDisplayText {
64
- }
80
+ export type LivenessDisplayText = DisplayTextTemplate<HintDisplayText & CameraDisplayText & InstructionDisplayText & ErrorDisplayText & StreamDisplayText>;
@@ -101,7 +101,8 @@ export declare enum FaceMatchState {
101
101
  TOO_CLOSE = "TOO CLOSE",
102
102
  CANT_IDENTIFY = "CANNOT IDENTIFY",
103
103
  FACE_IDENTIFIED = "ONE FACE IDENTIFIED",
104
- TOO_MANY = "TOO MANY FACES"
104
+ TOO_MANY = "TOO MANY FACES",
105
+ OFF_CENTER = "OFF CENTER"
105
106
  }
106
107
  export interface LivenessError {
107
108
  state: ErrorState;
@@ -47,6 +47,9 @@ export declare function drawLivenessOvalInCanvas({ canvas, oval, scaleFactor, vi
47
47
  videoEl: HTMLVideoElement;
48
48
  isStartScreen?: boolean;
49
49
  }): void;
50
+ export declare function clearOvalCanvas({ canvas, }: {
51
+ canvas: HTMLCanvasElement;
52
+ }): void;
50
53
  interface FaceMatchStateInLivenessOval {
51
54
  faceMatchState: FaceMatchState;
52
55
  faceMatchPercentage: number;
@@ -12,6 +12,7 @@ export interface FaceLivenessErrorModalProps {
12
12
  export declare const renderErrorModal: ({ errorState, overrideErrorDisplayText, }: {
13
13
  errorState: ErrorState;
14
14
  overrideErrorDisplayText?: Partial<{
15
+ errorLabelText: string;
15
16
  timeoutHeaderText: string;
16
17
  timeoutMessageText: string;
17
18
  faceDistanceHeaderText: string;
@@ -1,7 +1,6 @@
1
1
  import * as React from 'react';
2
2
  interface ToastWithLoaderProps {
3
3
  displayText: string;
4
- labelText?: string;
5
4
  }
6
5
  export declare const ToastWithLoader: React.FC<ToastWithLoaderProps>;
7
6
  export {};
@@ -1 +1 @@
1
- export declare const VERSION = "3.0.8";
1
+ export declare const VERSION = "3.0.9";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws-amplify/ui-react-liveness",
3
- "version": "3.0.8",
3
+ "version": "3.0.9",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/esm/index.mjs",
6
6
  "exports": {
@@ -47,8 +47,8 @@
47
47
  "react-dom": "^16.14.0 || ^17.0 || ^18.0"
48
48
  },
49
49
  "dependencies": {
50
- "@aws-amplify/ui": "6.0.6",
51
- "@aws-amplify/ui-react": "6.1.0",
50
+ "@aws-amplify/ui": "6.0.7",
51
+ "@aws-amplify/ui-react": "6.1.1",
52
52
  "@aws-sdk/client-rekognitionstreaming": "3.398.0",
53
53
  "@aws-sdk/util-format-url": "^3.410.0",
54
54
  "@smithy/eventstream-serde-browser": "^2.0.4",
@@ -80,7 +80,7 @@
80
80
  "name": "FaceLivenessDetector",
81
81
  "path": "dist/esm/index.mjs",
82
82
  "import": "{ FaceLivenessDetector }",
83
- "limit": "274 kB"
83
+ "limit": "275 kB"
84
84
  }
85
85
  ]
86
86
  }