diff --git a/.changeset/pretty-tables-live.md b/.changeset/pretty-tables-live.md
new file mode 100644
index 00000000000..46e7435ac53
--- /dev/null
+++ b/.changeset/pretty-tables-live.md
@@ -0,0 +1,9 @@
+---
+"@aws-amplify/ui-react-liveness": major
+---
+
+breaking(liveness): update liveness UX
+
+Replace start screen with instructions with a new hair check start screen that lets end users interface with the camera. Also added camera selection and upgraded the blazeface model.
+
+Updated `disableInstructionScreen` to `disableStartScreen`
diff --git a/docs/src/data/ignoredLinks.ts b/docs/src/data/ignoredLinks.ts
index 6017fd32660..090baddd7a5 100644
--- a/docs/src/data/ignoredLinks.ts
+++ b/docs/src/data/ignoredLinks.ts
@@ -40,4 +40,6 @@ export const IGNORED_LINKS = [
'https://pub.dev/documentation/amplify_authenticator/latest/amplify_authenticator/AuthenticatorState-class.html',
'https://pub.dev/documentation/amplify_authenticator/latest/amplify_authenticator/amplify_authenticator-library.html',
'https://docs.flutter.dev/ui/accessibility-and-localization/internationalization',
+ 'https://cdn.liveness.rekognition.amazonaws.com/face-detection/tensorflow/tfjs-backend-wasm/4.11.0/',
+ 'https://tfhub.dev/mediapipe/tfjs-model/face_detection/short/1',
];
diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationComponents.tsx b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationComponents.tsx
index a9da38a2fac..37adfa0463b 100644
--- a/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationComponents.tsx
+++ b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationComponents.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness';
-import { View, Heading, Alert, Card, Text } from '@aws-amplify/ui-react';
+import { View, Heading, Alert } from '@aws-amplify/ui-react';
export function CustomizationComponents() {
return (
@@ -9,17 +9,6 @@ export function CustomizationComponents() {
region={'us-east-1'}
onAnalysisComplete={async () => {}}
components={{
- Header: () => {
- return (
-
- Face Liveness check
-
- You will go through a face verification process to prove that
- you are a real person.
-
-
- );
- },
PhotosensitiveWarning: (): JSX.Element => {
return (
);
},
- Instructions: (): JSX.Element => {
- return (
-
- Instructions to follow to use Face Liveness detector
-
-
- Make sure your face is not covered with sunglasses or a mask.
-
-
- Move to a well-lit place that is not dark or in direct
- sunlight.
-
-
- Fill onscreen oval with your face and hold for colored lights.
-
-
-
- );
- },
ErrorView: ({ children }) => {
return (
diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/Customizationi18n.tsx b/docs/src/pages/[platform]/connected-components/liveness/customization/Customizationi18n.tsx
index c4476b6e53a..ed504fccd14 100644
--- a/docs/src/pages/[platform]/connected-components/liveness/customization/Customizationi18n.tsx
+++ b/docs/src/pages/[platform]/connected-components/liveness/customization/Customizationi18n.tsx
@@ -6,23 +6,13 @@ const dictionary = {
// use default strings for english
en: null,
es: {
- instructionsHeaderHeadingText: 'Verificación de vida',
- instructionsHeaderBodyText:
- 'Pasará por un proceso de verificación facial para demostrar que es una persona real.',
- instructionListStepOneText:
- 'Cuando aparezca un óvalo, rellena el óvalo con tu cara en 7 segundos.',
- instructionListStepTwoText: 'Maximiza el brillo de tu pantalla.',
- instructionListStepThreeText:
- 'Asegúrese de que su cara no esté cubierta con gafas de sol o una máscara.',
- instructionListStepFourText:
- 'Muévase a un lugar bien iluminado que no esté expuesto a la luz solar directa.',
photosensitivyWarningHeadingText: 'Advertencia de fotosensibilidad',
photosensitivyWarningBodyText:
'Esta verificación muestra luces de colores. Tenga cuidado si es fotosensible.',
- instructionListHeadingText:
- 'Siga las instrucciones para completar la verificación:',
goodFitCaptionText: 'Buen ajuste',
tooFarCaptionText: 'Demasiado lejos',
+ hintCenterFaceText: 'Centra tu cara',
+ startScreenBeginCheckText: 'Comenzar a verificar',
},
};
diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.components.react.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.components.react.mdx
index ad9cc09bd73..4521373c9dd 100644
--- a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.components.react.mdx
+++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.components.react.mdx
@@ -7,10 +7,10 @@ FaceLivenessDetector allows overriding some UI components using the `components`
The following code snippet demonstrates how to pass in custom HTML rendering functions:
-- Custom Header
- Custom Photo Sensitivity Warning
-- Custom Instruction List
- Custom Error View
+- Custom Cancel Button
+- Custom Recording Icon
```tsx file=./CustomizationComponents.tsx
```
diff --git a/docs/src/pages/[platform]/connected-components/liveness/full-api.react.mdx b/docs/src/pages/[platform]/connected-components/liveness/full-api.react.mdx
index 5f2fdeb8c5d..ce5383e1f79 100644
--- a/docs/src/pages/[platform]/connected-components/liveness/full-api.react.mdx
+++ b/docs/src/pages/[platform]/connected-components/liveness/full-api.react.mdx
@@ -62,13 +62,13 @@ Below is the full list of props that can be used with the `FaceLivenessDetectorC
binaryPath?
{/* WARNING: Ensure that this URL matches the value in the liveness component */}
- Overrides the WASM binary path, the default is https://cdn.liveness.rekognition.amazonaws.com/face-detection/tensorflow/tfjs-backend-wasm/3.11.0/. When overriding this path ensure that the wasm version matches the version of [@tensorflow/tfjs-backend-wasm](https://www.npmjs.com/package/@tensorflow/tfjs-backend-wasm) installed by npm.
+ Overrides the WASM binary path, the default is https://cdn.liveness.rekognition.amazonaws.com/face-detection/tensorflow/tfjs-backend-wasm/4.11.0/. When overriding this path ensure that the wasm version matches the version of [@tensorflow/tfjs-backend-wasm](https://www.npmjs.com/package/@tensorflow/tfjs-backend-wasm) installed by npm.faceModelUrl?
{/* WARNING: Ensure that this URL matches the value in the liveness component */}
- Overrides the Blazeface model and weights bin CDN URL. Default value is https://cdn.liveness.rekognition.amazonaws.com/face-detection/tensorflow-models/blazeface/0.0.7/model/model.json
+ Overrides the Blazeface model and weights bin CDN URL. Default value is https://cdn.liveness.rekognition.amazonaws.com/face-detection/tensorflow-models/blazeface/1.0.2/model/model.json
diff --git a/docs/src/pages/[platform]/connected-components/liveness/react-props.ts b/docs/src/pages/[platform]/connected-components/liveness/react-props.ts
index 33b014fe32d..c594f90ac72 100644
--- a/docs/src/pages/[platform]/connected-components/liveness/react-props.ts
+++ b/docs/src/pages/[platform]/connected-components/liveness/react-props.ts
@@ -27,9 +27,9 @@ export const FACE_LIVENESS_DETECTOR_PROPS = [
type: `(error: LivenessError) => void`,
},
{
- name: `disableInstructionScreen?`,
+ name: `disableStartScreen?`,
description:
- 'Optional parameter for the disabling the Start/Get Ready Screen, default: false.',
+ 'Optional parameter for the disabling the start screen, default: false.',
type: `boolean`,
},
{
@@ -50,23 +50,12 @@ export const FACE_LIVENESS_DETECTOR_PROPS = [
];
export const FACE_LIVENESS_DETECTOR_COMPONENTS = [
- {
- name: `Header?`,
- description: 'Overrides the rendered component in the header section.',
- type: `React.ComponentType`,
- },
{
name: `PhotosensitiveWarning?`,
description:
'Overrides the rendered component for the photosensitivity warning.',
type: `React.ComponentType`,
},
- {
- name: `Instructions?`,
- description:
- 'Overrides the rendered component for the instruction section.',
- type: `React.ComponentType`,
- },
{
name: `ErrorView?`,
description: 'Overrides the rendered component for error view.',
diff --git a/docs/src/pages/[platform]/getting-started/migration/migration.react.mdx b/docs/src/pages/[platform]/getting-started/migration/migration.react.mdx
index 1e50f407ba9..2a48a2b68ae 100644
--- a/docs/src/pages/[platform]/getting-started/migration/migration.react.mdx
+++ b/docs/src/pages/[platform]/getting-started/migration/migration.react.mdx
@@ -681,6 +681,55 @@ The latest version of the `Authenticator` has a completely different set of CSS
Previous versions of `Authenticator` exposed a `onAuthUIStateChange` handler to detect Auth state changes. For similar functionality see [useAuthenticator](/react/connected-components/authenticator/advanced#access-auth-state).
## `@aws-amplify/ui-react-liveness`
+### Migrate from 2.x to 3.x
+#### Installation
+
+Install the 3.x version of the `@aws-amplify/ui-react-liveness` library.
+
+
+
+ npm
+ yarn
+
+
+
+
+
+
+
+
+
+#### Update and Usage
+
+Optionally update your App with the new prop usage:
+
+**App.js**
+
+```diff
+ const App = () => (
+ return (
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ );
+);
+
+```
+
+#### CDN CSP Policy
+
+The 3.x version of the `FaceLivenessDetector` has been updated to use the latest version of TensorFlow and Blazeface, thus the default CDN paths have changed. If your application has an existing CSP policy, ensure your policy allows https://cdn.liveness.rekognition.amazonaws.com. If you are using a custom CDN make sure to update your CDN versions to match [@tensorflow/tfjs-backend-wasm](https://www.npmjs.com/package/@tensorflow/tfjs-backend-wasm/v/4.11.0) and [@tensorflow-models/face-detection](https://www.npmjs.com/package/@tensorflow-models/face-detection/v/1.0.2).Please look over the [Liveness Config](../connected-components/liveness/customization#facelivenessdetectorconfig) section for more information.
+
### Migrate from 1.x to 2.x
#### Installation
diff --git a/examples/next/pages/ui/components/liveness/components/LivenessDefault.tsx b/examples/next/pages/ui/components/liveness/components/LivenessDefault.tsx
index d861a28be92..0efd8d0657f 100644
--- a/examples/next/pages/ui/components/liveness/components/LivenessDefault.tsx
+++ b/examples/next/pages/ui/components/liveness/components/LivenessDefault.tsx
@@ -1,14 +1,14 @@
import { View, Flex, Loader, Text } from '@aws-amplify/ui-react';
import {
- FaceLivenessDetector,
FaceLivenessDetectorCore,
+ FaceLivenessDetector,
} from '@aws-amplify/ui-react-liveness';
import { useLiveness } from './useLiveness';
import { SessionIdAlert } from './SessionIdAlert';
import LivenessInlineResults from './LivenessInlineResults';
export default function LivenessDefault({
- disableInstructionScreen = false,
+ disableStartScreen = false,
components = undefined,
credentialProvider = undefined,
}) {
@@ -36,12 +36,7 @@ export default function LivenessDefault({
Loading...
) : (
-
+
@@ -55,40 +50,24 @@ export default function LivenessDefault({
{!getLivenessResponse ? (
- credentialProvider ? (
- {
- await handleGetLivenessDetection(
- createLivenessSessionApiData['sessionId']
- );
- }}
- onError={(error) => {
- console.error(error);
- }}
- disableInstructionScreen={disableInstructionScreen}
- components={components}
- config={{ credentialProvider }}
- />
- ) : (
- {
- await handleGetLivenessDetection(
- createLivenessSessionApiData['sessionId']
- );
- }}
- onError={(error) => {
- console.error(error);
- }}
- disableInstructionScreen={disableInstructionScreen}
- components={components}
- />
- )
+ {
+ await handleGetLivenessDetection(
+ createLivenessSessionApiData['sessionId']
+ );
+ }}
+ onError={(error) => {
+ console.error(error);
+ }}
+ disableStartScreen={disableStartScreen}
+ components={components}
+ {...(credentialProvider
+ ? { config: { credentialProvider } }
+ : {})}
+ />
) : null}
diff --git a/examples/next/pages/ui/components/liveness/components/LivenessInlineResults.tsx b/examples/next/pages/ui/components/liveness/components/LivenessInlineResults.tsx
index ed39dced0cd..4dea98ed0d9 100644
--- a/examples/next/pages/ui/components/liveness/components/LivenessInlineResults.tsx
+++ b/examples/next/pages/ui/components/liveness/components/LivenessInlineResults.tsx
@@ -19,7 +19,7 @@ export default function LivenessInlineResults({
).toString('base64');
const displayScore = truncateNumber(confidenceScore, 4);
return (
- <>
+ Liveness result:
@@ -28,12 +28,27 @@ export default function LivenessInlineResults({
- Confidence score:
+ Liveness confidence score:
{displayScore}
+ {!isLive && (
+
+ Tips to pass the video check:
+
+
Maximize you screen's brightness
+
+ Avoid very bright lighting conditions, such as direct sunlight.
+
+
+ Remove sunglasses, mask, hat, or anything blocking your face.
+
+
+
+ )}
+
);
}
diff --git a/examples/next/pages/ui/components/liveness/disable-start-screen/index.page.tsx b/examples/next/pages/ui/components/liveness/disable-start-screen/index.page.tsx
index 1c6ab54d297..3b3ddd32d88 100644
--- a/examples/next/pages/ui/components/liveness/disable-start-screen/index.page.tsx
+++ b/examples/next/pages/ui/components/liveness/disable-start-screen/index.page.tsx
@@ -15,7 +15,7 @@ Amplify.configure({
const App = () => {
return (
-
+
);
};
diff --git a/examples/next/pages/ui/components/liveness/with-custom-components/index.page.tsx b/examples/next/pages/ui/components/liveness/with-custom-components/index.page.tsx
index fbfcca8c01b..a594120fe15 100644
--- a/examples/next/pages/ui/components/liveness/with-custom-components/index.page.tsx
+++ b/examples/next/pages/ui/components/liveness/with-custom-components/index.page.tsx
@@ -17,7 +17,7 @@ const App = () => {
return (
{
return (
diff --git a/packages/e2e/features/ui/components/liveness/liveness-detector.feature b/packages/e2e/features/ui/components/liveness/liveness-detector.feature
index 1939c9a2c2c..524bc14ccae 100644
--- a/packages/e2e/features/ui/components/liveness/liveness-detector.feature
+++ b/packages/e2e/features/ui/components/liveness/liveness-detector.feature
@@ -7,23 +7,23 @@ Feature: Liveness Detector
@react
Scenario: Navigate with keyboard only
- Then I hit the "enter" key on "Begin check" button
+ Then I hit the "enter" key on "Start video check" button
Then I click the "close-icon"
- # TODO: Change this to use keyboard navigation, at this time it doesnt work the same way begin check does
- Then I see the "Begin check" button
+ # TODO: Change this to use keyboard navigation, at this time it doesnt work the same way Start video check does
+ Then I see the "Start video check" button
@react
Scenario: See camera module and close with the close icon
- Then I click the "Begin check" button
+ Then I click the "Start video check" button
Then I click the "close-icon"
- Then I see the "Begin check" button
+ Then I see the "Start video check" button
@react
Scenario: See camera module and instructions
- Then I click the "Begin check" button
+ Then I click the "Start video check" button
Then I see "liveness-detector" element
Then I see "connecting"
Then I see "Move closer"
Then I see "Face didn't fit inside oval in time limit."
Then I click the "Try again" button
- Then I see the "Begin check" button
+ Then I see the "Start video check" button
diff --git a/packages/e2e/features/ui/components/liveness/start-screen.feature b/packages/e2e/features/ui/components/liveness/start-screen.feature
index c3b0719d3bb..35b732ad012 100644
--- a/packages/e2e/features/ui/components/liveness/start-screen.feature
+++ b/packages/e2e/features/ui/components/liveness/start-screen.feature
@@ -9,20 +9,10 @@ Feature: Liveness Start Screen
Scenario: The Start Screen has all the elements
Then I see "SessionId:"
Then I see "Photosensitivity warning"
- Then I see "Liveness check"
- Then I see "You will go through a face verification process to prove that you are a real person."
- Then I see "This check displays colored lights. Use caution if you are photosensitive."
- Then I see the "Begin check" button
+ Then I see "Center your face"
+ Then I see the "Start video check" button
@react
Scenario: Click the pop-over icon and see the notes
Then I click the "popover-icon"
- Then I see "A small percentage of individuals may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition."
-
- @react
- Scenario: The start Screen has the instructions
- Then I see "Follow the instructions to complete the check:"
- Then I see "Make sure your face is not covered with sunglasses or a mask."
- Then I see "Move to a well-lit place that is not in direct sunlight."
- Then I see "Maximize your screen's brightness."
- Then I see "When an oval appears, follow the instructions to fit your face in it."
+ Then I see "Some people may experience may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition."
diff --git a/packages/e2e/features/ui/components/liveness/with-credential-provider.feature b/packages/e2e/features/ui/components/liveness/with-credential-provider.feature
index 6793f854712..72af5dbfa72 100644
--- a/packages/e2e/features/ui/components/liveness/with-credential-provider.feature
+++ b/packages/e2e/features/ui/components/liveness/with-credential-provider.feature
@@ -7,16 +7,16 @@ Liveness component supports using a custom credential provider.
@react
Scenario: See camera module and close with the close icon
- Then I click the "Begin check" button
+ Then I click the "Start video check" button
Then I click the "close-icon"
- Then I see the "Begin check" button
+ Then I see the "Start video check" button
@react
Scenario: See camera module and instructions
- Then I click the "Begin check" button
+ Then I click the "Start video check" button
Then I see "liveness-detector" element
Then I see "connecting"
Then I see "Move closer"
Then I see "Face didn't fit inside oval in time limit."
Then I click the "Try again" button
- Then I see the "Begin check" button
+ Then I see the "Start video check" button
diff --git a/packages/e2e/features/ui/components/liveness/with-custom-components.feature b/packages/e2e/features/ui/components/liveness/with-custom-components.feature
index c09b42f48e6..b475f15824b 100644
--- a/packages/e2e/features/ui/components/liveness/with-custom-components.feature
+++ b/packages/e2e/features/ui/components/liveness/with-custom-components.feature
@@ -8,14 +8,6 @@ Feature: Liveness with Custom Components
@react
Scenario: The Start Screen has all the elements
Then I see "SessionId:"
- Then I see "Face liveness check"
- Then I see "You will go through a face verification process to prove that you are a real person."
- Then I see "Caution"
- Then I see "This check displays colored lights. Use caution if you are photosensitive."
- Then I see the "Begin check" button
-
- @react
- Scenario: The start Screen has the instructions
- Then I see "Instructions to follow to use liveness face detector"
- Then I see "Make sure your face is not covered with sunglasses or a mask."
- Then I see "Move to a well-lit place that is not dark or in direct sunlight."
+ Then I see "Photosensitivity warning"
+ Then I see "Center your face"
+ Then I see the "Start video check" button
diff --git a/packages/react-liveness/jest.config.ts b/packages/react-liveness/jest.config.ts
index eee77d1cf73..231e2b34c1a 100644
--- a/packages/react-liveness/jest.config.ts
+++ b/packages/react-liveness/jest.config.ts
@@ -9,29 +9,15 @@ const config: Config = {
// do not collect from top level version and styles files
'!/src/(styles|version).(ts|tsx)',
],
- // coverageThreshold: {
- // global: {
- // branches: 80,
- // functions: 82,
- // lines: 89,
- // statements: 89,
- // },
- // },
coverageThreshold: {
global: {
- branches: 77.0,
- functions: 57.74,
- lines: 72.0,
- statements: 73.0,
+ branches: 80,
+ functions: 82,
+ lines: 89,
+ statements: 89,
},
},
- // @todo-migration update test API usage and remove temp thresholds
- testPathIgnorePatterns: [
- 'src/components/FaceLivenessDetector/service/utils/__tests__/streamProvider.test.ts',
- 'src/components/FaceLivenessDetector/StartLiveness/__tests__/StartLiveness.test.tsx',
- 'src/components/FaceLivenessDetector/shared/__tests__/CancelButton.test.tsx',
- 'src/components/FaceLivenessDetector/shared/__tests__/LivenessIconWithPopover.test.tsx',
- ],
+ testPathIgnorePatterns: [],
moduleNameMapper: {
'^nanoid$': '/../../node_modules/nanoid',
'^uuid$': '/../../node_modules/uuid',
diff --git a/packages/react-liveness/package.json b/packages/react-liveness/package.json
index 7a76403ec28..0c1cfa00858 100644
--- a/packages/react-liveness/package.json
+++ b/packages/react-liveness/package.json
@@ -1,5 +1,4 @@
{
- "private": true,
"name": "@aws-amplify/ui-react-liveness",
"version": "2.0.12",
"main": "dist/index.js",
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetector.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetector.tsx
index 53f4a4fb5ba..397747fe2b9 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetector.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetector.tsx
@@ -1,10 +1,9 @@
import * as React from 'react';
import { fetchAuthSession } from 'aws-amplify/auth';
import { FaceLivenessDetectorProps as FaceLivenessDetectorPropsFromUi } from './service';
-import FaceLivenessDetectorCore, {
- FaceLivenessDetectorComponents,
-} from './FaceLivenessDetectorCore';
+import FaceLivenessDetectorCore from './FaceLivenessDetectorCore';
import { LivenessDisplayText } from './displayText';
+import { FaceLivenessDetectorComponents } from './shared/DefaultStartScreenComponents';
export interface FaceLivenessDetectorProps
extends FaceLivenessDetectorPropsFromUi {
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetectorCore.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetectorCore.tsx
index 87d5c78f786..200dbf7ec20 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetectorCore.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/FaceLivenessDetectorCore.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { useActor, useInterpret } from '@xstate/react';
+import { useInterpret } from '@xstate/react';
import {
livenessMachine,
FaceLivenessDetectorCoreProps as FaceLivenessDetectorPropsFromUi,
@@ -7,18 +7,13 @@ import {
import { View, Flex } from '@aws-amplify/ui-react';
import { FaceLivenessDetectorProvider } from './providers';
-import { StartLiveness } from './StartLiveness';
import { LivenessCheck } from './LivenessCheck';
-import { StartScreenComponents } from './shared/DefaultStartScreenComponents';
+import { FaceLivenessDetectorComponents } from './shared/DefaultStartScreenComponents';
import { LivenessDisplayText } from './displayText';
import { getDisplayText } from './utils/getDisplayText';
-import { CheckScreenComponents } from './shared/FaceLivenessErrorModal';
const DETECTOR_CLASS_NAME = 'liveness-detector';
-export type FaceLivenessDetectorComponents = StartScreenComponents &
- CheckScreenComponents;
-
export interface FaceLivenessDetectorCoreProps
extends FaceLivenessDetectorPropsFromUi {
components?: FaceLivenessDetectorComponents;
@@ -28,12 +23,7 @@ export interface FaceLivenessDetectorCoreProps
export default function FaceLivenessDetectorCore(
props: FaceLivenessDetectorCoreProps
): JSX.Element {
- const {
- disableInstructionScreen = false,
- components,
- config,
- displayText,
- } = props;
+ const { components, config, displayText } = props;
const currElementRef = React.useRef(null);
const {
hintDisplayText,
@@ -53,40 +43,18 @@ export default function FaceLivenessDetectorCore(
},
});
- const [state, send] = useActor(service);
- const isStartView = state.matches('start') || state.matches('userCancel');
-
- const beginLivenessCheck = React.useCallback(() => {
- send({
- type: 'BEGIN',
- });
- }, [send]);
-
- React.useLayoutEffect(() => {
- if (disableInstructionScreen && isStartView) {
- beginLivenessCheck();
- }
- }, [beginLivenessCheck, disableInstructionScreen, isStartView]);
-
return (
- {isStartView ? (
-
- ) : (
-
- )}
+
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.tsx
index 25f3c52fff7..9213eeeed87 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.tsx
@@ -1,8 +1,17 @@
import React, { useState, useRef } from 'react';
import { classNames } from '@aws-amplify/ui';
-import { Flex, Loader, View } from '@aws-amplify/ui-react';
-import { FaceMatchState } from '../service';
+import {
+ Button,
+ Flex,
+ Label,
+ Loader,
+ SelectField,
+ Text,
+ View,
+} from '@aws-amplify/ui-react';
+import { useColorMode } from '@aws-amplify/ui-react/internal';
+import { FaceMatchState, drawStaticOval } from '../service';
import {
useLivenessActor,
useLivenessSelector,
@@ -11,25 +20,25 @@ import {
UseMediaStreamInVideo,
} from '../hooks';
import {
+ InstructionDisplayText,
ErrorDisplayText,
HintDisplayText,
StreamDisplayText,
+ CameraDisplayText,
} from '../displayText';
-import {
- CancelButton,
- Hint,
- RecordingIcon,
- Overlay,
- selectErrorState,
- MatchIndicator,
-} from '../shared';
+import { Hint, Overlay, selectErrorState, MatchIndicator } from '../shared';
import { LivenessClassNames } from '../types/classNames';
import {
- CheckScreenComponents,
FaceLivenessErrorModal,
renderErrorModal,
} from '../shared/FaceLivenessErrorModal';
+import {
+ DefaultPhotosensitiveWarning,
+ FaceLivenessDetectorComponents,
+ DefaultCancelButton,
+ DefaultRecordingIcon,
+} from '../shared/DefaultStartScreenComponents';
export const selectVideoConstraints = createLivenessSelector(
(state) => state.context.videoAssociatedParams?.videoConstraints
@@ -38,19 +47,27 @@ export const selectVideoStream = createLivenessSelector(
(state) => state.context.videoAssociatedParams?.videoMediaStream
);
export const selectFaceMatchPercentage = createLivenessSelector(
- (state) => state.context.faceMatchAssociatedParams!.faceMatchPercentage
+ (state) => state.context.faceMatchAssociatedParams?.faceMatchPercentage
);
export const selectFaceMatchState = createLivenessSelector(
- (state) => state.context.faceMatchAssociatedParams!.faceMatchState
+ (state) => state.context.faceMatchAssociatedParams?.faceMatchState
+);
+export const selectSelectedDeviceId = createLivenessSelector(
+ (state) => state.context.videoAssociatedParams?.selectedDeviceId
+);
+export const selectSelectableDevices = createLivenessSelector(
+ (state) => state.context.videoAssociatedParams?.selectableDevices
);
export interface LivenessCameraModuleProps {
isMobileScreen: boolean;
isRecordingStopped: boolean;
+ instructionDisplayText: Required;
streamDisplayText: Required;
hintDisplayText: Required;
errorDisplayText: Required;
- components?: CheckScreenComponents;
+ cameraDisplayText: Required;
+ components?: FaceLivenessDetectorComponents;
testId?: string;
}
@@ -62,6 +79,13 @@ const centeredLoader = (
/>
);
+const showMatchIndicatorStates = [
+ FaceMatchState.TOO_FAR,
+ FaceMatchState.CANT_IDENTIFY,
+ FaceMatchState.FACE_IDENTIFIED,
+ FaceMatchState.MATCHED,
+];
+
/**
* For now we want to memoize the HOC for MatchIndicator because to optimize renders
* The LivenessCameraModule still needs to be optimized for re-renders and at that time
@@ -75,9 +99,11 @@ export const LivenessCameraModule = (
const {
isMobileScreen,
isRecordingStopped,
+ instructionDisplayText,
streamDisplayText,
hintDisplayText,
errorDisplayText,
+ cameraDisplayText,
components: customComponents,
testId,
} = props;
@@ -89,15 +115,15 @@ export const LivenessCameraModule = (
const [state, send] = useLivenessActor();
const videoStream = useLivenessSelector(selectVideoStream);
+ const videoConstraints = useLivenessSelector(selectVideoConstraints);
+ const selectedDeviceId = useLivenessSelector(selectSelectedDeviceId);
+ const selectableDevices = useLivenessSelector(selectSelectableDevices);
+
const faceMatchPercentage = useLivenessSelector(selectFaceMatchPercentage);
const faceMatchState = useLivenessSelector(selectFaceMatchState);
const errorState = useLivenessSelector(selectErrorState);
- const showMatchIndicatorStates = [
- FaceMatchState.TOO_FAR,
- FaceMatchState.CANT_IDENTIFY,
- FaceMatchState.FACE_IDENTIFIED,
- FaceMatchState.MATCHED,
- ];
+
+ const colorMode = useColorMode();
const { videoRef, videoWidth, videoHeight } = useMediaStreamInVideo(
videoStream!
@@ -108,6 +134,8 @@ export const LivenessCameraModule = (
const [isCameraReady, setIsCameraReady] = useState(false);
const isCheckingCamera = state.matches('cameraCheck');
+ const isWaitingForCamera = state.matches('waitForDOMAndCameraDetails');
+ const isStartView = state.matches('start') || state.matches('userCancel');
const isRecording = state.matches('recording');
const isCheckSucceeded = state.matches('checkSucceeded');
const isFlashingFreshness = state.matches({
@@ -125,6 +153,50 @@ export const LivenessCameraModule = (
videoWidth && videoHeight ? videoWidth / videoHeight : 0
);
+ React.useEffect(() => {
+ if (
+ canvasRef &&
+ videoRef &&
+ canvasRef.current &&
+ videoRef.current &&
+ videoStream &&
+ isStartView
+ ) {
+ drawStaticOval(canvasRef.current, videoRef.current, videoStream);
+ }
+ }, [canvasRef, videoRef, videoStream, colorMode, isStartView]);
+
+ React.useEffect(() => {
+ const updateColorModeHandler = (e: MediaQueryListEvent) => {
+ if (
+ e.matches &&
+ canvasRef &&
+ videoRef &&
+ canvasRef.current &&
+ videoRef.current &&
+ videoStream &&
+ isStartView
+ ) {
+ drawStaticOval(canvasRef.current, videoRef.current, videoStream);
+ }
+ };
+
+ const darkModePreference = window.matchMedia(
+ '(prefers-color-scheme: dark)'
+ );
+ const lightModePreference = window.matchMedia(
+ '(prefers-color-scheme: light)'
+ );
+
+ darkModePreference.addEventListener('change', updateColorModeHandler);
+ lightModePreference.addEventListener('change', updateColorModeHandler);
+
+ return () => {
+ darkModePreference.removeEventListener('change', updateColorModeHandler);
+ lightModePreference.addEventListener('change', updateColorModeHandler);
+ };
+ }, [canvasRef, videoRef, videoStream, isStartView]);
+
React.useLayoutEffect(() => {
if (isCameraReady) {
send({
@@ -147,113 +219,219 @@ export const LivenessCameraModule = (
}
}, [send, videoRef, isCameraReady, isMobileScreen]);
+ const photoSensitivtyWarning = React.useMemo(() => {
+ return (
+
+
+
+ );
+ }, [instructionDisplayText, isStartView]);
+
const handleMediaPlay = () => {
setIsCameraReady(true);
};
+ const beginLivenessCheck = React.useCallback(() => {
+ send({
+ type: 'BEGIN',
+ });
+ }, [send]);
+
+ const onCameraChange = React.useCallback(
+ (e: React.ChangeEvent) => {
+ const newDeviceId = e.target.value;
+ const changeCamera = async () => {
+ const newStream = await navigator.mediaDevices.getUserMedia({
+ video: {
+ ...videoConstraints,
+ deviceId: { exact: newDeviceId },
+ },
+ audio: false,
+ });
+ send({
+ type: 'UPDATE_DEVICE_AND_STREAM',
+ data: { newDeviceId, newStream },
+ });
+ };
+ changeCamera();
+ },
+ [videoConstraints, send]
+ );
+
if (isCheckingCamera) {
return (
-
- {centeredLoader}
+
+
+
+ {cameraDisplayText.waitingCameraPermissionText}
+
);
}
- return (
-
- {!isCameraReady && centeredLoader}
-
-
-
-
-
-
-
+ const isRecordingOnMobile =
+ isMobileScreen && !isStartView && !isWaitingForCamera && isRecording;
- {isRecording && (
-
- {recordingIndicatorText}
-
- )}
+ return (
+ <>
+ {photoSensitivtyWarning}
- {!isCheckSucceeded && (
-
-
-
+
+ {!isCameraReady && centeredLoader}
-
+
-
-
- {errorState && (
- {
- send({ type: 'CANCEL' });
- }}
- >
- {renderErrorModal({
- errorState,
- overrideErrorDisplayText: errorDisplayText,
- })}
-
+
+
+
+
+
+ {isRecording && (
+
+ )}
+
+ {!isStartView && !isWaitingForCamera && !isCheckSucceeded && (
+
)}
- {/*
+
+
+
+ {errorState && (
+ {
+ send({ type: 'CANCEL' });
+ }}
+ >
+ {renderErrorModal({
+ errorState,
+ overrideErrorDisplayText: errorDisplayText,
+ })}
+
+ )}
+
+ {/*
We only want to show the MatchIndicator when we're recording
and when the face is in either the too far state, or the
initial face identified state. Using the a memoized MatchIndicator here
so that even when this component re-renders the indicator is only
re-rendered if the percentage prop changes.
*/}
- {isRecording &&
- !isFlashingFreshness &&
- showMatchIndicatorStates.includes(faceMatchState!) ? (
-
- ) : null}
-
-
-
+ {isRecording &&
+ !isFlashingFreshness &&
+ showMatchIndicatorStates.includes(faceMatchState!) ? (
+
+ ) : null}
+
+
+ {isStartView &&
+ !isMobileScreen &&
+ selectableDevices &&
+ selectableDevices.length > 1 && (
+
+
+
+
+ {selectableDevices?.map((device) => (
+
+ ))}
+
+
+
+ )}
+
+
+
+ {isStartView && (
+
+
+
+ )}
+ >
);
};
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCheck.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCheck.tsx
index 9315131f189..d7203ce4f42 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCheck.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/LivenessCheck.tsx
@@ -12,6 +12,7 @@ import {
import { isMobileScreen, getLandscapeMediaQuery } from '../utils/device';
import { CancelButton } from '../shared/CancelButton';
import {
+ InstructionDisplayText,
HintDisplayText,
CameraDisplayText,
StreamDisplayText,
@@ -19,24 +20,28 @@ import {
defaultErrorDisplayText,
} from '../displayText';
import { LandscapeErrorModal } from '../shared/LandscapeErrorModal';
-import { CheckScreenComponents } from '../shared/FaceLivenessErrorModal';
import { selectErrorState } from '../shared';
+import { FaceLivenessDetectorComponents } from '../shared/DefaultStartScreenComponents';
const CHECK_CLASS_NAME = 'liveness-detector-check';
-const selectIsRecordingStopped = createLivenessSelector(
+const CAMERA_ERROR_TEXT_WIDTH = 420;
+
+export const selectIsRecordingStopped = createLivenessSelector(
(state) => state.context.isRecordingStopped
);
interface LivenessCheckProps {
+ instructionDisplayText: Required;
hintDisplayText: Required;
cameraDisplayText: Required;
streamDisplayText: Required;
errorDisplayText: Required;
- components?: CheckScreenComponents;
+ components?: FaceLivenessDetectorComponents;
}
export const LivenessCheck: React.FC = ({
+ instructionDisplayText,
hintDisplayText,
cameraDisplayText,
streamDisplayText,
@@ -143,7 +148,7 @@ export const LivenessCheck: React.FC = ({
? cameraMinSpecificationsHeadingText
: cameraNotFoundHeadingText}
-
+
{errorState === LivenessErrorState.CAMERA_FRAMERATE_ERROR
? cameraMinSpecificationsMessageText
: cameraNotFoundMessageText}
@@ -165,9 +170,11 @@ export const LivenessCheck: React.FC = ({
);
@@ -180,6 +187,7 @@ export const LivenessCheck: React.FC = ({
position="relative"
testId={CHECK_CLASS_NAME}
className={CHECK_CLASS_NAME}
+ gap="xl"
>
{renderCheck()}
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCameraModule.test.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCameraModule.test.tsx
index 03671b8938d..260920fe0e7 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCameraModule.test.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCameraModule.test.tsx
@@ -15,16 +15,22 @@ import {
} from '../../hooks';
import {
LivenessCameraModule,
+ selectFaceMatchPercentage,
+ selectFaceMatchState,
+ selectSelectableDevices,
+ selectSelectedDeviceId,
selectVideoConstraints,
selectVideoStream,
} from '../LivenessCameraModule';
import { FaceMatchState } from '../../service';
import { getDisplayText } from '../../utils/getDisplayText';
+import { selectIsRecordingStopped } from '../LivenessCheck';
jest.mock('../../hooks');
jest.mock('../../hooks/useLivenessSelector');
jest.mock('../../shared/CancelButton');
jest.mock('../../shared/Hint');
+jest.mock('../../service');
const mockUseLivenessActor = getMockedFunction(useLivenessActor);
const mockUseLivenessSelector = getMockedFunction(useLivenessSelector);
@@ -39,9 +45,15 @@ describe('LivenessCameraModule', () => {
let isCheckingCamera = false;
let isNotRecording = false;
let isRecording = false;
-
- const { hintDisplayText, streamDisplayText, errorDisplayText } =
- getDisplayText(undefined);
+ let isStart = false;
+
+ const {
+ hintDisplayText,
+ streamDisplayText,
+ errorDisplayText,
+ cameraDisplayText,
+ instructionDisplayText,
+ } = getDisplayText(undefined);
const { cancelLivenessCheckText, recordingIndicatorText } = streamDisplayText;
function mockStateMatchesAndSelectors() {
@@ -50,6 +62,8 @@ describe('LivenessCameraModule', () => {
.mockReturnValue(isCheckingCamera)
.calledWith('notRecording')
.mockReturnValue(isNotRecording)
+ .calledWith('start')
+ .mockReturnValue(isStart)
.calledWith('recording')
.mockReturnValue(isRecording);
}
@@ -69,6 +83,7 @@ describe('LivenessCameraModule', () => {
isCheckingCamera = false;
isNotRecording = false;
isRecording = false;
+ isStart = false;
jest.clearAllMocks();
jest.clearAllTimers();
@@ -86,6 +101,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
/>
);
@@ -103,6 +120,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
/>
);
@@ -145,6 +164,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
/>
);
const videoEl = screen.getByTestId('video');
@@ -170,6 +191,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
testId={testId}
/>
);
@@ -199,6 +222,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
testId={testId}
/>
);
@@ -228,6 +253,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
testId={testId}
/>
);
@@ -257,6 +284,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
testId={testId}
/>
);
@@ -286,6 +315,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
testId={testId}
/>
);
@@ -315,6 +346,8 @@ describe('LivenessCameraModule', () => {
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ cameraDisplayText={cameraDisplayText}
+ instructionDisplayText={instructionDisplayText}
testId={testId}
/>
);
@@ -337,14 +370,104 @@ describe('LivenessCameraModule', () => {
videoAssociatedParams: {
videoConstraints: expectedConstraints,
videoMediaStream: expectedStream,
+ selectedDeviceId: 'foobar',
+ selectableDevices: ['foobar'],
+ },
+ faceMatchAssociatedParams: {
+ faceMatchPercentage: 100,
+ faceMatchState: FaceMatchState.MATCHED,
},
},
};
const actualConstraints = selectVideoConstraints(state);
const actualStream = selectVideoStream(state);
+ const actualPercentage = selectFaceMatchPercentage(state);
+ const actualDeviceId = selectSelectedDeviceId(state);
+ const actualSelectableDevices = selectSelectableDevices(state);
+ const actualFaceMatchState = selectFaceMatchState(state);
expect(actualConstraints).toEqual(expectedConstraints);
expect(actualStream).toEqual(expectedStream);
+ expect(actualPercentage).toEqual(100);
+ expect(actualDeviceId).toEqual('foobar');
+ expect(actualSelectableDevices).toEqual(['foobar']);
+ expect(actualFaceMatchState).toEqual(FaceMatchState.MATCHED);
+ });
+
+ it('selectors should work with undefined values', () => {
+ const state: any = {
+ context: {},
+ };
+
+ const actualConstraints = selectVideoConstraints(state);
+ const actualStream = selectVideoStream(state);
+ const actualPercentage = selectFaceMatchPercentage(state);
+ const actualDeviceId = selectSelectedDeviceId(state);
+ const actualSelectableDevices = selectSelectableDevices(state);
+ const actualFaceMatchState = selectFaceMatchState(state);
+
+ expect(actualConstraints).toEqual(undefined);
+ expect(actualStream).toEqual(undefined);
+ expect(actualPercentage).toEqual(undefined);
+ expect(actualDeviceId).toEqual(undefined);
+ expect(actualSelectableDevices).toEqual(undefined);
+ expect(actualFaceMatchState).toEqual(undefined);
+ });
+
+ it('should render with custom components', () => {
+ isCheckingCamera = true;
+ mockStateMatchesAndSelectors();
+
+ renderWithLivenessProvider(
+
+ );
+
+ expect(screen.getByTestId('centered-loader')).toBeInTheDocument();
+ });
+
+ it('should render hair check screen when isStart = true', () => {
+ isStart = true;
+ mockStateMatchesAndSelectors();
+ mockUseLivenessSelector.mockReturnValue(25).mockReturnValue(['device-id']);
+
+ renderWithLivenessProvider(
+
+ );
+ const videoEl = screen.getByTestId('video');
+ videoEl.dispatchEvent(new Event('canplay'));
+
+ expect(screen.getByTestId('popover-icon')).toBeInTheDocument();
+ });
+
+ it('selectors should work', () => {
+ mockUseLivenessSelector.mockReturnValueOnce({}).mockReturnValueOnce({});
+ const state: any = {
+ context: {
+ isRecordingStopped: true,
+ },
+ };
+
+ console.log(selectIsRecordingStopped);
+ const isRecordingStopped = selectIsRecordingStopped(state);
+
+ expect(isRecordingStopped).toEqual(true);
});
});
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCheck.test.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCheck.test.tsx
index f201d78657c..a1a5e98871c 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCheck.test.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/LivenessCheck/__tests__/LivenessCheck.test.tsx
@@ -29,6 +29,7 @@ const {
cameraDisplayText,
streamDisplayText,
errorDisplayText,
+ instructionDisplayText,
} = getDisplayText(undefined);
const {
@@ -89,6 +90,7 @@ describe('LivenessCheck', () => {
cameraDisplayText={cameraDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ instructionDisplayText={instructionDisplayText}
/>
);
@@ -115,6 +117,7 @@ describe('LivenessCheck', () => {
cameraDisplayText={cameraDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ instructionDisplayText={instructionDisplayText}
/>
);
@@ -142,6 +145,7 @@ describe('LivenessCheck', () => {
cameraDisplayText={cameraDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ instructionDisplayText={instructionDisplayText}
/>
);
@@ -167,6 +171,7 @@ describe('LivenessCheck', () => {
cameraDisplayText={cameraDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
+ instructionDisplayText={instructionDisplayText}
/>
);
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/StartLiveness.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/StartLiveness.tsx
deleted file mode 100644
index 039f2a203ac..00000000000
--- a/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/StartLiveness.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import * as React from 'react';
-import { Flex, Button, Card } from '@aws-amplify/ui-react';
-
-import { InstructionDisplayText } from '../displayText';
-import {
- DefaultHeader,
- DefaultPhotosensitiveWarning,
- DefaultInstructions,
- StartScreenComponents,
-} from '../shared/DefaultStartScreenComponents';
-
-const START_CLASS_NAME = 'liveness-detector-start';
-
-export interface StartLivenessProps {
- beginLivenessCheck: () => void;
- components?: StartScreenComponents;
- instructionDisplayText: Required;
-}
-
-export function StartLiveness(props: StartLivenessProps): JSX.Element {
- const {
- beginLivenessCheck,
- components: customComponents,
- instructionDisplayText,
- } = props;
-
- return (
-
-
- {customComponents?.Header ? (
-
- ) : (
-
- )}
-
- {customComponents?.PhotosensitiveWarning ? (
-
- ) : (
-
- )}
-
- {customComponents?.Instructions ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- );
-}
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/__tests__/StartLiveness.test.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/__tests__/StartLiveness.test.tsx
deleted file mode 100644
index ad3ec56d484..00000000000
--- a/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/__tests__/StartLiveness.test.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import * as React from 'react';
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-import {
- renderWithLivenessProvider,
- getMockedFunction,
-} from '../../__mocks__/utils';
-import { useLivenessActor } from '../../hooks/useLivenessActor';
-import { StartLiveness } from '../StartLiveness';
-import { getDisplayText } from '../../utils/getDisplayText';
-
-jest.mock('../../hooks/useLivenessActor');
-jest.mock('../../shared/CancelButton');
-jest.mock('../helpers');
-
-const mockUseLivenessActor = getMockedFunction(useLivenessActor);
-
-describe('StartLiveness', () => {
- const mockActorState: any = {};
- const mockActorSend = jest.fn();
- const mockBeginCheck = () => {
- mockActorSend({
- type: 'BEGIN',
- });
- };
-
- const { instructionDisplayText } = getDisplayText(undefined);
-
- const { instructionsBeginCheckText } = instructionDisplayText;
-
- beforeEach(() => {
- mockUseLivenessActor.mockReturnValue([mockActorState, mockActorSend]);
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it('should render the StartLiveness component and content appropriately', () => {
- renderWithLivenessProvider(
-
- );
- expect(
- screen.getByText(instructionDisplayText.photosensitivyWarningHeadingText)
- ).toBeInTheDocument();
- expect(
- screen.getByText(instructionDisplayText.photosensitivyWarningBodyText)
- ).toBeInTheDocument();
- expect(
- screen.getByText(instructionDisplayText.instructionListHeadingText)
- ).toBeInTheDocument();
- expect(
- screen.getByText(instructionDisplayText.instructionListStepOneText)
- ).toBeInTheDocument();
- expect(
- screen.getByText(instructionDisplayText.instructionListStepTwoText)
- ).toBeInTheDocument();
- expect(
- screen.getByText(instructionDisplayText.instructionListStepThreeText)
- ).toBeInTheDocument();
- expect(
- screen.getByText(instructionDisplayText.instructionListStepFourText)
- ).toBeInTheDocument();
-
- expect(
- screen.getByRole('button', {
- name: instructionsBeginCheckText,
- })
- ).toBeInTheDocument();
- });
-
- it('should render the StartLiveness component with custom component override', () => {
- const photosensitiveWarning = 'Some warning related to photosensitivity';
- const livenessInstructions =
- 'Some instructions to follow to use liveness face detector';
- renderWithLivenessProvider(
- {
- return {photosensitiveWarning};
- },
- Instructions: (): JSX.Element => {
- return {livenessInstructions};
- },
- }}
- />
- );
-
- expect(screen.getByText(photosensitiveWarning)).toBeInTheDocument();
- expect(screen.getByText(livenessInstructions)).toBeInTheDocument();
- // check for default liveness header component if no custom override
- expect(
- screen.getByText(instructionDisplayText.instructionsHeaderHeadingText)
- ).toBeInTheDocument();
-
- // check that the default components are not present when custom overrides are provided
- expect(
- screen.queryByText(
- instructionDisplayText.photosensitivyWarningHeadingText
- )
- ).not.toBeInTheDocument();
- expect(
- screen.queryByText(instructionDisplayText.instructionListHeadingText)
- ).not.toBeInTheDocument();
-
- expect(
- screen.getByRole('button', { name: instructionsBeginCheckText })
- ).toBeInTheDocument();
- });
-
- it('should call the begin handler on begin check', () => {
- renderWithLivenessProvider(
-
- );
-
- userEvent.click(
- screen.getByRole('button', { name: instructionsBeginCheckText })
- );
-
- expect(mockActorSend).toHaveBeenCalledWith({
- type: 'BEGIN',
- });
- });
-});
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/index.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/index.tsx
deleted file mode 100644
index 8c0f022919b..00000000000
--- a/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './StartLiveness';
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/__tests__/FaceLivenessDetector.test.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/__tests__/FaceLivenessDetector.test.tsx
index ca7d4309924..79c8a84d921 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/__tests__/FaceLivenessDetector.test.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/__tests__/FaceLivenessDetector.test.tsx
@@ -8,7 +8,7 @@ import { useMediaStreamInVideo, useLivenessActor } from '../hooks';
jest.mock('../../../styles.css', () => ({}));
jest.mock('@xstate/react');
-jest.mock('../StartLiveness/helpers');
+jest.mock('../utils/helpers');
jest.mock('../hooks');
const mockUseActor = getMockedFunction(useActor);
@@ -94,19 +94,8 @@ describe('FaceLivenessDetector', () => {
it('should show the check screen if disableInstructionScreen is true', () => {
render(
-
+
);
expect(screen.queryByTestId(livenessCheckTestId)).toBeInTheDocument();
});
-
- it('should not show the instruction if disableInstructionScreen is true and xstate is at the start', async () => {
- mockMatches.mockReturnValueOnce(true);
- render(
-
- );
-
- expect(mockActorSend).toHaveBeenCalledWith({
- type: 'BEGIN',
- });
- });
});
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/displayText.ts b/packages/react-liveness/src/components/FaceLivenessDetector/displayText.ts
index 5c831325772..22c3219762d 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/displayText.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/displayText.ts
@@ -11,6 +11,7 @@ export type HintDisplayText = {
hintIlluminationTooDarkText?: string;
hintIlluminationNormalText?: string;
hintHoldFaceForFreshnessText?: string;
+ hintCenterFaceText?: string;
};
export type CameraDisplayText = {
@@ -19,24 +20,18 @@ export type CameraDisplayText = {
cameraNotFoundHeadingText?: string;
cameraNotFoundMessageText?: string;
retryCameraPermissionsText?: string;
+ waitingCameraPermissionText?: string;
};
export type InstructionDisplayText = {
- instructionsHeaderHeadingText?: string;
- instructionsHeaderBodyText?: string;
- instructionsBeginCheckText?: string;
photosensitivyWarningHeadingText?: string;
photosensitivyWarningBodyText?: string;
photosensitivyWarningInfoText?: string;
- instructionListHeadingText?: string;
goodFitCaptionText?: string;
goodFitAltText?: string;
tooFarCaptionText?: string;
tooFarAltText?: string;
- instructionListStepOneText?: string;
- instructionListStepTwoText?: string;
- instructionListStepThreeText?: string;
- instructionListStepFourText?: string;
+ startScreenBeginCheckText?: string;
};
export type StreamDisplayText = {
@@ -69,37 +64,28 @@ export type ErrorDisplayTextFoo = typeof defaultErrorDisplayText;
export type ErrorDisplayText = Partial;
export const defaultLivenessDisplayText: Required = {
- instructionsHeaderHeadingText: 'Liveness check',
- instructionsHeaderBodyText:
- 'You will go through a face verification process to prove that you are a real person.',
- instructionsBeginCheckText: 'Begin check',
+ hintCenterFaceText: 'Center your face',
+ startScreenBeginCheckText: 'Start video check',
photosensitivyWarningHeadingText: 'Photosensitivity warning',
photosensitivyWarningBodyText:
- 'This check displays colored lights. Use caution if you are photosensitive.',
+ 'This check flashes different colors. Use caution if you are photosensitive.',
photosensitivyWarningInfoText:
- 'A small percentage of individuals may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition.',
- instructionListHeadingText: 'Follow the instructions to complete the check:',
+ 'Some people may experience may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition.',
goodFitCaptionText: 'Good fit',
goodFitAltText:
"Ilustration of a person's face, perfectly fitting inside of an oval.",
tooFarCaptionText: 'Too far',
tooFarAltText:
"Illustration of a person's face inside of an oval; there is a gap between the perimeter of the face and the boundaries of the oval.",
- instructionListStepOneText:
- 'When an oval appears, follow the instructions to fit your face in it.',
- instructionListStepTwoText: "Maximize your screen's brightness.",
- instructionListStepThreeText:
- 'Make sure your face is not covered with sunglasses or a mask.',
- instructionListStepFourText:
- 'Move to a well-lit place that is not in direct sunlight.',
cameraMinSpecificationsHeadingText:
'Camera does not meet minimum specifications',
cameraMinSpecificationsMessageText:
'Camera must support at least 320*240 resolution and 15 frames per second.',
- cameraNotFoundHeadingText: 'Camera not accessible.',
+ cameraNotFoundHeadingText: 'Camera is not accessible.',
cameraNotFoundMessageText:
- 'Check that camera is connected and camera permissions are enabled in settings before retrying.',
+ 'Check that a camera is connected and there is not another application using the camera. You may have to go into settings to grant camera permissions and close out all instances of your browser and retry.',
retryCameraPermissionsText: 'Retry',
+ waitingCameraPermissionText: 'Waiting for you to allow camera permission.',
cancelLivenessCheckText: 'Cancel Liveness check',
recordingIndicatorText: 'Rec',
hintMoveFaceFrontOfCameraText: 'Move face in front of camera',
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/hooks/useMediaStreamInVideo.ts b/packages/react-liveness/src/components/FaceLivenessDetector/hooks/useMediaStreamInVideo.ts
index bd30aa3b8c5..a6c77d591e2 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/hooks/useMediaStreamInVideo.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/hooks/useMediaStreamInVideo.ts
@@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from 'react';
import { isObject } from '@aws-amplify/ui';
-import { STATIC_VIDEO_CONSTRAINTS } from '../StartLiveness/helpers';
+import { STATIC_VIDEO_CONSTRAINTS } from '../utils/helpers';
export interface UseMediaStreamInVideo {
videoRef: React.MutableRefObject;
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/__tests__/index.test.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/__tests__/index.test.ts
index ea1b0230ca1..0e07148e508 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/__tests__/index.test.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/__tests__/index.test.ts
@@ -18,7 +18,7 @@ import {
mockSessionInformation,
mockVideoRecorder,
} from '../../utils/__mocks__/testUtils';
-import { STATIC_VIDEO_CONSTRAINTS } from '../../../StartLiveness/helpers';
+import { STATIC_VIDEO_CONSTRAINTS } from '../../../utils/helpers';
jest.useFakeTimers();
jest.mock('../../utils');
@@ -51,7 +51,10 @@ describe('Liveness Machine', () => {
config: {},
};
- const mockVideoConstaints: MediaTrackConstraints = STATIC_VIDEO_CONSTRAINTS;
+ const mockVideoConstaints: MediaTrackConstraints = {
+ deviceId: 'some-device-id',
+ ...STATIC_VIDEO_CONSTRAINTS,
+ };
const mockCameraDevice: MediaDeviceInfo = {
deviceId: 'some-device-id',
groupId: 'some-group-id',
@@ -75,7 +78,6 @@ describe('Liveness Machine', () => {
} as any as MediaStreamTrack;
const mockVideoMediaStream = {
- getVideoTracks: () => [mockVideoTrack],
getTracks: () => [mockVideoTrack],
} as MediaStream;
@@ -111,7 +113,7 @@ describe('Liveness Machine', () => {
currentDetectedFace: mockFace,
startFace: mockFace,
endFace: mockFace,
- initialFaceMatchTime: Date.now() - 500,
+ initialFaceMatchTime: Date.now() - 1000,
},
freshnessColorAssociatedParams: {
freshnessColorEl: document.createElement('canvas'),
@@ -144,13 +146,19 @@ describe('Liveness Machine', () => {
freshnessColorEl: mockFreshnessColorEl,
},
});
- jest.advanceTimersToNextTimer(); // detectFaceBeforeStart
- await flushPromises();
- jest.advanceTimersToNextTimer(); // checkFaceDetectedBeforeStart
+ jest.advanceTimersToNextTimer(); // start
+
+ service.send({
+ type: 'BEGIN',
+ });
+ await flushPromises(); // checkFaceDetectedBeforeStart
+ jest.advanceTimersToNextTimer(); // detectFaceDistanceBeforeRecording
+ await flushPromises(); // checkFaceDistanceBeforeRecording
+ jest.advanceTimersToNextTimer(); // initializeLivenessStream
}
async function transitionToRecording(service: LivenessInterpreter) {
await transitionToInitializeLivenessStream(service);
- await flushPromises(); // notRecording
+ await flushPromises(); // notRecording: 'waitForSessionInfo'
service.send({
type: 'SET_SESSION_INFO',
@@ -158,9 +166,7 @@ describe('Liveness Machine', () => {
sessionInfo: mockSessionInformation,
},
});
- jest.advanceTimersToNextTimer(); // waitForSessionInformation
- await flushPromises(); // detectFaceDistanceBeforeRecording
- jest.advanceTimersToNextTimer(); // checkFaceDistanceBeforeRecording
+ jest.advanceTimersToNextTimer(); // "recording": "ovalDrawing",
}
async function advanceMinFaceMatches() {
@@ -225,9 +231,9 @@ describe('Liveness Machine', () => {
service.stop();
});
- it('should be in the idle state', () => {
+ it('should be in the cameraCheck state', () => {
service.start();
- expect(service.state.value).toBe('start');
+ expect(service.state.value).toBe('cameraCheck');
});
it('should reach start state on CANCEL', async () => {
@@ -235,22 +241,10 @@ describe('Liveness Machine', () => {
service.send('CANCEL');
await flushPromises();
- expect(service.state.value).toBe('start');
+ expect(service.state.value).toBe('waitForDOMAndCameraDetails');
expect(mockcomponentProps.onUserCancel).toHaveBeenCalledTimes(1);
});
- it('should reach cameraCheck state on BEGIN from start', () => {
- transitionToCameraCheck(service);
-
- expect(service.state.value).toBe('cameraCheck');
- expect(
- service.state.context.videoAssociatedParams!.videoConstraints
- ).toEqual(mockVideoConstaints);
- expect(
- service.state.context.ovalAssociatedParams!.faceDetector
- ).toBeDefined();
- });
-
describe('cameraCheck', () => {
it('should reach waitForDOMAndCameraDetails state on checkVirtualCameraAndGetStream success', async () => {
transitionToCameraCheck(service);
@@ -277,7 +271,7 @@ describe('Liveness Machine', () => {
getSettings: () => ({
width: 640,
height: 480,
- deviceId: 'virtual-device-id',
+ deviceId: 'some-device-id',
frameRate: 30,
}),
},
@@ -292,8 +286,8 @@ describe('Liveness Machine', () => {
await flushPromises();
expect(service.state.value).toBe('waitForDOMAndCameraDetails');
expect(
- service.state.context.videoAssociatedParams!.videoMediaStream
- ).toEqual(mockVideoMediaStream);
+ service.state.context.videoAssociatedParams!.videoMediaStream?.getTracks
+ ).toBeDefined();
expect(mockNavigatorMediaDevices.getUserMedia).toHaveBeenNthCalledWith(
1,
{
@@ -301,16 +295,6 @@ describe('Liveness Machine', () => {
audio: false,
}
);
- expect(mockNavigatorMediaDevices.getUserMedia).toHaveBeenNthCalledWith(
- 2,
- {
- video: {
- ...mockVideoConstaints,
- deviceId: { exact: mockCameraDevice.deviceId },
- },
- audio: false,
- }
- );
});
it('should reach permissionDenied state on checkVirtualCameraAndGetStream failure due to no real device', async () => {
@@ -390,12 +374,12 @@ describe('Liveness Machine', () => {
});
jest.advanceTimersToNextTimer();
- expect(service.state.value).toEqual('detectFaceBeforeStart');
+ expect(service.state.value).toEqual('start');
});
});
describe('detectFaceBeforeStart', () => {
- it('should reach detectFaceDistanceBeforeRecording state on face detected', async () => {
+ it('should reach detectFaceBeforeStart on begin button press', async () => {
transitionToCameraCheck(service);
await flushPromises(); // waitForDOMAndCameraDetails
@@ -407,36 +391,48 @@ describe('Liveness Machine', () => {
freshnessColorEl: mockFreshnessColorEl,
},
});
- jest.advanceTimersToNextTimer(); // detectFaceBeforeStart
- await flushPromises();
- jest.advanceTimersToNextTimer(); // checkFaceDetectedBeforeStart
+ jest.advanceTimersToNextTimer(); // start
- expect(service.state.value).toEqual('detectFaceDistanceBeforeRecording');
+ service.send({
+ type: 'BEGIN',
+ });
+
+ expect(service.state.value).toEqual('detectFaceBeforeStart');
});
- });
- describe('notRecording', () => {
- it('should reach waitForSessionInfo', async () => {
- await transitionToInitializeLivenessStream(service);
- await flushPromises(); // checkFaceDistanceBeforeRecording
+ it('should reach detectFaceBeforeStart on begin button press', async () => {
+ transitionToCameraCheck(service);
+ await flushPromises(); // waitForDOMAndCameraDetails
service.send({
- type: 'SET_SESSION_INFO',
+ type: 'SET_DOM_AND_CAMERA_DETAILS',
data: {
- sessionInfo: mockSessionInformation,
+ videoEl: mockVideoEl,
+ canvasEl: mockCanvasEl,
+ freshnessColorEl: mockFreshnessColorEl,
},
});
+ jest.advanceTimersToNextTimer(); // start
+
+ service.send({
+ type: 'BEGIN',
+ });
+ await flushPromises(); // checkFaceDetectedBeforeStart
+ jest.advanceTimersToNextTimer(); // detectFaceDistanceBeforeRecording
+ await flushPromises(); // checkFaceDistanceBeforeRecording
jest.advanceTimersToNextTimer(); // initializeLivenessStream
- await flushPromises(); // { notRecording: 'waitForSessionInfo' }
+ await flushPromises(); // notRecording
expect(service.state.value).toEqual({
notRecording: 'waitForSessionInfo',
});
});
+ });
+ describe('notRecording', () => {
it('should reach recording state on START_RECORDING', async () => {
await transitionToInitializeLivenessStream(service);
- await flushPromises(); // checkFaceDistanceBeforeRecording
+ await flushPromises(); // notRecording: 'waitForSessionInfo'
service.send({
type: 'SET_SESSION_INFO',
@@ -444,9 +440,7 @@ describe('Liveness Machine', () => {
sessionInfo: mockSessionInformation,
},
});
- jest.advanceTimersToNextTimer(); // initializeLivenessStream
- await flushPromises(); // { notRecording: 'waitForSessionInfo' }
- jest.advanceTimersToNextTimer(); // { recording: 'ovalDrawing' }
+ jest.advanceTimersToNextTimer(); // "recording": "ovalDrawing",
expect(service.state.value).toEqual({ recording: 'ovalDrawing' });
});
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/index.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/index.ts
index 140ff3e1de4..416c3f7262d 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/index.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/machine/index.ts
@@ -30,6 +30,7 @@ import {
estimateIllumination,
isCameraDeviceVirtual,
FreshnessColorDisplay,
+ drawStaticOval,
} from '../utils';
import { nanoid } from 'nanoid';
import { getStaticLivenessOvalDetails } from '../utils/liveness';
@@ -46,10 +47,10 @@ import {
ClientSessionInformationEvent,
LivenessResponseStream,
} from '@aws-sdk/client-rekognitionstreaming';
-import { STATIC_VIDEO_CONSTRAINTS } from '../../StartLiveness/helpers';
+import { STATIC_VIDEO_CONSTRAINTS } from '../../utils/helpers';
import { WS_CLOSURE_CODE } from '../utils/constants';
-export const MIN_FACE_MATCH_TIME = 500;
+export const MIN_FACE_MATCH_TIME = 1000;
const DEFAULT_FACE_FIT_TIMEOUT = 7000;
// timer metrics variables
@@ -59,10 +60,20 @@ let streamConnectionOpenTimestamp: number;
let responseStream: Promise>;
+const CAMERA_ID_KEY = 'AmplifyLivenessCameraId';
+
+function getLastSelectedCameraId(): string | null {
+ return localStorage.getItem(CAMERA_ID_KEY);
+}
+
+function setLastSelectedCameraId(deviceId: string) {
+ localStorage.setItem(CAMERA_ID_KEY, deviceId);
+}
+
export const livenessMachine = createMachine(
{
id: 'livenessMachine',
- initial: 'start',
+ initial: 'cameraCheck',
predictableActionArguments: true,
context: {
challengeId: nanoid(),
@@ -72,6 +83,7 @@ export const livenessMachine = createMachine(
serverSessionInformation: undefined,
videoAssociatedParams: {
videoConstraints: STATIC_VIDEO_CONSTRAINTS,
+ selectableDevices: [],
},
ovalAssociatedParams: undefined,
faceMatchAssociatedParams: {
@@ -120,6 +132,9 @@ export const livenessMachine = createMachine(
SET_DOM_AND_CAMERA_DETAILS: {
actions: 'setDOMAndCameraDetails',
},
+ UPDATE_DEVICE_AND_STREAM: {
+ actions: 'updateDeviceAndStream',
+ },
SERVER_ERROR: {
target: 'error',
actions: 'updateErrorStateForServer',
@@ -133,13 +148,8 @@ export const livenessMachine = createMachine(
},
},
states: {
- start: {
- on: {
- BEGIN: 'cameraCheck',
- },
- },
cameraCheck: {
- entry: ['resetErrorState', 'initializeFaceDetector'],
+ entry: ['resetErrorState'],
invoke: {
src: 'checkVirtualCameraAndGetStream',
onDone: {
@@ -154,11 +164,22 @@ export const livenessMachine = createMachine(
waitForDOMAndCameraDetails: {
after: {
0: {
- target: 'detectFaceBeforeStart',
+ target: 'start',
cond: 'hasDOMAndCameraDetails',
},
- // setting this to check every 500 ms sometimes caused detectFaceBeforeStart to be called twice
- 500: { target: 'waitForDOMAndCameraDetails' },
+ 10: { target: 'waitForDOMAndCameraDetails' },
+ },
+ },
+ start: {
+ entry: ['drawStaticOval', 'initializeFaceDetector'],
+ always: [
+ {
+ target: 'detectFaceBeforeStart',
+ cond: 'shouldSkipStartScreen',
+ },
+ ],
+ on: {
+ BEGIN: 'detectFaceBeforeStart',
},
},
detectFaceBeforeStart: {
@@ -210,9 +231,6 @@ export const livenessMachine = createMachine(
},
},
notRecording: {
- on: {
- START_RECORDING: 'recording', // if countdown completes while face is far enough, start recording
- },
initial: 'waitForSessionInfo',
states: {
waitForSessionInfo: {
@@ -391,7 +409,7 @@ export const livenessMachine = createMachine(
},
userCancel: {
entry: ['cleanUpResources', 'callUserCancelCallback', 'resetContext'],
- always: [{ target: 'start' }],
+ always: [{ target: 'cameraCheck' }],
},
},
},
@@ -409,6 +427,8 @@ export const livenessMachine = createMachine(
videoAssociatedParams: (context, event) => ({
...context.videoAssociatedParams,
videoMediaStream: event.data?.stream,
+ selectedDeviceId: event.data?.selectedDeviceId,
+ selectableDevices: event.data?.selectableDevices,
}),
}),
initializeFaceDetector: assign({
@@ -447,6 +467,22 @@ export const livenessMachine = createMachine(
freshnessColorEl: event.data?.freshnessColorEl,
}),
}),
+ updateDeviceAndStream: assign({
+ videoAssociatedParams: (context, event) => {
+ setLastSelectedCameraId(event.data?.newDeviceId);
+ return {
+ ...context.videoAssociatedParams,
+ selectedDeviceId: event.data?.newDeviceId,
+ videoMediaStream: event.data?.newStream,
+ };
+ },
+ }),
+ drawStaticOval: (context) => {
+ const { canvasEl, videoEl, videoMediaStream } =
+ context.videoAssociatedParams!;
+
+ drawStaticOval(canvasEl!, videoEl!, videoMediaStream!);
+ },
updateRecordingStartTimestampMs: assign({
videoAssociatedParams: (context) => {
const {
@@ -858,14 +894,21 @@ export const livenessMachine = createMachine(
undefined
);
},
+ shouldSkipStartScreen: (context) => {
+ return !!context.componentProps?.disableStartScreen;
+ },
},
services: {
async checkVirtualCameraAndGetStream(context) {
const { videoConstraints } = context.videoAssociatedParams!;
// Get initial stream to enumerate devices with non-empty labels
+ const existingDeviceId = getLastSelectedCameraId();
const initialStream = await navigator.mediaDevices.getUserMedia({
- video: videoConstraints,
+ video: {
+ ...videoConstraints,
+ ...(existingDeviceId ? { deviceId: existingDeviceId } : {}),
+ },
audio: false,
});
const devices = await navigator.mediaDevices.enumerateDevices();
@@ -896,8 +939,10 @@ export const livenessMachine = createMachine(
(device) => device.deviceId === initialStreamDeviceId
);
+ let deviceId = initialStreamDeviceId;
let realVideoDeviceStream = initialStream;
if (!isInitialStreamFromRealDevice) {
+ deviceId = realVideoDevices[0].deviceId;
realVideoDeviceStream = await navigator.mediaDevices.getUserMedia({
video: {
...videoConstraints,
@@ -906,8 +951,13 @@ export const livenessMachine = createMachine(
audio: false,
});
}
+ setLastSelectedCameraId(deviceId!);
- return { stream: realVideoDeviceStream };
+ return {
+ stream: realVideoDeviceStream,
+ selectedDeviceId: initialStreamDeviceId,
+ selectableDevices: realVideoDevices,
+ };
},
async openLivenessStreamConnection(context) {
const { config } = context.componentProps!;
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/types/liveness.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/types/liveness.ts
index be103758e4e..a7c91b8b933 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/service/types/liveness.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/types/liveness.ts
@@ -34,7 +34,7 @@ export interface FaceLivenessDetectorCoreProps {
/**
* Optional parameter for the disabling the Start/Get Ready Screen, default: false
*/
- disableInstructionScreen?: boolean;
+ disableStartScreen?: boolean;
/**
* Optional parameter for advanced options for the component
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/types/machine.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/types/machine.ts
index 924e17e71bc..04bd489e518 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/service/types/machine.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/types/machine.ts
@@ -53,6 +53,8 @@ export interface VideoAssociatedParams {
videoRecorder?: VideoRecorder;
recordingStartTimestampMs?: number;
isMobile?: boolean;
+ selectedDeviceId?: string;
+ selectableDevices?: MediaDeviceInfo[];
}
export type LivenessContext = Partial;
@@ -85,6 +87,7 @@ export type LivenessEventTypes =
| 'SET_SESSION_INFO'
| 'DISCONNECT_EVENT'
| 'SET_DOM_AND_CAMERA_DETAILS'
+ | 'UPDATE_DEVICE_AND_STREAM'
| 'SERVER_ERROR'
| 'RUNTIME_ERROR'
| 'RETRY_CAMERA_CHECK'
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__mocks__/testUtils.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__mocks__/testUtils.ts
index f99813352ec..bc0c19e14c0 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__mocks__/testUtils.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__mocks__/testUtils.ts
@@ -220,7 +220,7 @@ export const mockSessionInformation: SessionInformation = {
FaceIouWidthThreshold: 0.15000000596046448,
OvalHeightWidthRatio: 1.6180000305175781,
OvalIouHeightThreshold: 0.25,
- OvalIouThreshold: 0.699999988079071,
+ OvalIouThreshold: 0.6,
OvalIouWidthThreshold: 0.25,
},
OvalParameters: {
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/streamProvider.test.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/streamProvider.test.ts
index 4ba2fa3d627..aa0f8062343 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/streamProvider.test.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/streamProvider.test.ts
@@ -4,7 +4,6 @@ import 'blob-polyfill';
import { TextDecoder } from 'util';
import { RekognitionStreamingClient } from '@aws-sdk/client-rekognitionstreaming';
-import { Amplify } from 'aws-amplify';
import { LivenessStreamProvider } from '../streamProvider';
import { VideoRecorder } from '../videoRecorder';
@@ -20,20 +19,25 @@ Object.defineProperty(window, 'TextDecoder', {
value: TextDecoder,
});
-const mockGet = jest.fn().mockImplementation(() => {
+jest.mock('aws-amplify/auth', () => {
+ const originalModule = jest.requireActual('aws-amplify/auth');
return {
- accessKeyId: 'accessKeyId',
- sessionToken: 'sessionTokenId',
- secretAccessKey: 'secretAccessKey',
- identityId: 'identityId',
- authenticated: true,
- expiration: new Date(),
+ ...originalModule,
+ fetchAuthSession: jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ credentials: {
+ accessKeyId: 'accessKeyId',
+ sessionToken: 'sessionTokenId',
+ secretAccessKey: 'secretAccessKey',
+ identityId: 'identityId',
+ authenticated: true,
+ expiration: new Date(),
+ },
+ });
+ }),
};
});
-// @todo-migration remove cast and fix mock if needed
-(Amplify as any).Credentials.get = mockGet;
-
let SWITCH = false;
describe('LivenessStreamProvider', () => {
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/liveness.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/liveness.ts
index f477a0346eb..aaff3c3908d 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/liveness.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/liveness.ts
@@ -182,6 +182,42 @@ export function getStaticLivenessOvalDetails({
};
}
+export function drawStaticOval(
+ canvasEl: HTMLCanvasElement,
+ videoEl: HTMLVideoElement,
+ videoMediaStream: MediaStream
+) {
+ const { width, height } = videoMediaStream!.getTracks()[0].getSettings();
+
+ // Get width/height of video element so we can compute scaleFactor
+ // and set canvas width/height.
+ const { width: videoScaledWidth, height: videoScaledHeight } =
+ videoEl!.getBoundingClientRect();
+
+ canvasEl!.width = Math.ceil(videoScaledWidth);
+ canvasEl!.height = Math.ceil(videoScaledHeight);
+
+ const ovalDetails = getStaticLivenessOvalDetails({
+ width: width!,
+ height: height!,
+ ratioMultiplier: 0.5,
+ });
+ ovalDetails.flippedCenterX = width! - ovalDetails.centerX;
+
+ // Compute scaleFactor which is how much our video element is scaled
+ // vs the intrinsic video resolution
+ const scaleFactor = videoScaledWidth / videoEl!.videoWidth;
+
+ // Draw oval in canvas using ovalDetails and scaleFactor
+ drawLivenessOvalInCanvas({
+ canvas: canvasEl!,
+ oval: ovalDetails,
+ scaleFactor,
+ videoEl: videoEl!,
+ isStartScreen: true,
+ });
+}
+
/**
* Draws the provided liveness oval on the canvas.
*/
@@ -190,11 +226,13 @@ export function drawLivenessOvalInCanvas({
oval,
scaleFactor,
videoEl,
+ isStartScreen,
}: {
canvas: HTMLCanvasElement;
oval: LivenessOvalDetails;
scaleFactor: number;
videoEl: HTMLVideoElement;
+ isStartScreen?: boolean;
}): void {
const { flippedCenterX, centerY, width, height } = oval;
@@ -203,10 +241,15 @@ export function drawLivenessOvalInCanvas({
const ctx = canvas.getContext('2d');
if (ctx) {
+ ctx.restore();
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// fill the canvas with a transparent rectangle
- ctx.fillStyle = 'rgba(255, 255, 255, 1.0)';
+ ctx.fillStyle = isStartScreen
+ ? getComputedStyle(canvas).getPropertyValue(
+ '--amplify-colors-background-primary'
+ )
+ : '#fff';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// On mobile our canvas is the width/height of the full screen.
@@ -235,9 +278,12 @@ export function drawLivenessOvalInCanvas({
);
// add stroke to the oval path
- ctx.strokeStyle = '#AEB3B7';
+ ctx.strokeStyle = getComputedStyle(canvas).getPropertyValue(
+ '--amplify-colors-border-secondary'
+ );
ctx.lineWidth = 3;
ctx.stroke();
+ ctx.save();
ctx.clip();
// Restore default canvas transform matrix
@@ -405,7 +451,7 @@ export function generateBboxFromLandmarks(
const faceBottom = faceTop + faceHeight;
const top = faceBottom - oh;
const left = Math.min(cx - ow / 2, rightEar[0]);
- const right = Math.min(cx + ow / 2, leftEar[0]);
+ const right = Math.max(cx + ow / 2, leftEar[0]);
return {
left: left,
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.tsx
index 330563526b4..4076ec8243b 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.tsx
@@ -1,37 +1,19 @@
import React from 'react';
import { ComponentClassName } from '@aws-amplify/ui';
-import { Flex, View, Text } from '@aws-amplify/ui-react';
-import { GoodFitIllustration, TooFarIllustration, StartScreenFigure } from './';
+import { Flex, View } from '@aws-amplify/ui-react';
+import { RecordingIcon } from './';
import { LivenessIconWithPopover } from './LivenessIconWithPopover';
+import { CancelButton as CancelButtonComponent } from './CancelButton';
import { LivenessClassNames } from '../types/classNames';
+import { CheckScreenComponents } from './FaceLivenessErrorModal';
+
+export type FaceLivenessDetectorComponents = StartScreenComponents &
+ CheckScreenComponents;
export interface StartScreenComponents {
- Header?: React.ComponentType;
PhotosensitiveWarning?: React.ComponentType;
- Instructions?: React.ComponentType;
}
-interface DefaultHeaderProps {
- headingText: string;
- bodyText: string;
-}
-
-export const DefaultHeader = ({
- headingText,
- bodyText,
-}: DefaultHeaderProps): JSX.Element => {
- return (
-
-
- {headingText}
-
-
- {bodyText}
-
-
- );
-};
-
interface DefaultPhotosensitiveWarningProps {
headingText: string;
bodyText: string;
@@ -46,6 +28,7 @@ export const DefaultPhotosensitiveWarning = ({
return (
{headingText}
@@ -56,48 +39,32 @@ export const DefaultPhotosensitiveWarning = ({
);
};
-interface DefaultInstructionsProps {
- headingText: string;
- goodFitCaptionText: string;
- goodFitAltText: string;
- tooFarCaptionText: string;
- tooFarAltText: string;
- steps: string[];
+interface DefaultRecordingIconProps {
+ recordingIndicatorText: string;
}
-export const DefaultInstructions = ({
- headingText,
- goodFitCaptionText,
- goodFitAltText,
- tooFarCaptionText,
- tooFarAltText,
- steps,
-}: DefaultInstructionsProps): JSX.Element => {
+export const DefaultRecordingIcon = ({
+ recordingIndicatorText,
+}: DefaultRecordingIconProps): JSX.Element => {
return (
-
-
- {headingText}
-
-
-
-
-
-
-
-
-
-
- {steps.map((item, index) => {
- return (
-
-
- {index + 1}.
-
- {item}
-
- );
- })}
-
-
+
+ {recordingIndicatorText}
+
+ );
+};
+
+interface CancelButtonProps {
+ cancelLivenessCheckText: string;
+}
+
+export const DefaultCancelButton = ({
+ cancelLivenessCheckText,
+}: CancelButtonProps): JSX.Element => {
+ return (
+
+
+
);
};
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/shared/Hint.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/shared/Hint.tsx
index 3533b84f091..b158366c223 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/shared/Hint.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/shared/Hint.tsx
@@ -10,7 +10,6 @@ import {
createLivenessSelector,
} from '../hooks';
import { Toast } from './Toast';
-import { Overlay } from './Overlay';
import { HintDisplayText } from '../displayText';
import { LivenessClassNames } from '../types/classNames';
@@ -54,6 +53,7 @@ export const Hint: React.FC = ({ hintDisplayText }) => {
const isCheckFaceDistanceBeforeRecording = state.matches(
'checkFaceDistanceBeforeRecording'
);
+ const isStartView = state.matches('start') || state.matches('userCancel');
const isRecording = state.matches('recording');
const isNotRecording = state.matches('notRecording');
const isUploading = state.matches('uploading');
@@ -79,6 +79,14 @@ export const Hint: React.FC = ({ hintDisplayText }) => {
};
const getInstructionContent = () => {
+ if (isStartView) {
+ return (
+
+ {hintDisplayText.hintCenterFaceText}
+
+ );
+ }
+
if (errorState ?? (isCheckFailed || isCheckSuccessful)) {
return;
}
@@ -87,10 +95,16 @@ export const Hint: React.FC = ({ hintDisplayText }) => {
if (isCheckFaceDetectedBeforeStart) {
if (faceMatchStateBeforeStart === FaceMatchState.TOO_MANY) {
return (
- {FaceMatchStateStringMap[faceMatchStateBeforeStart]}
+
+ {FaceMatchStateStringMap[faceMatchStateBeforeStart]}
+
);
}
- return {hintDisplayText.hintMoveFaceFrontOfCameraText};
+ return (
+
+ {hintDisplayText.hintMoveFaceFrontOfCameraText}
+
+ );
}
// Specifically checking for false here because initially the value is undefined and we do not want to show the instruction
@@ -98,7 +112,11 @@ export const Hint: React.FC = ({ hintDisplayText }) => {
isCheckFaceDistanceBeforeRecording &&
isFaceFarEnoughBeforeRecordingState === false
) {
- return {hintDisplayText.hintTooCloseText};
+ return (
+
+ {hintDisplayText.hintTooCloseText}
+
+ );
}
if (isNotRecording) {
@@ -114,22 +132,21 @@ export const Hint: React.FC = ({ hintDisplayText }) => {
if (isUploading) {
return (
-
-
-
-
- {hintDisplayText.hintVerifyingText}
-
-
-
+
+
+
+ {hintDisplayText.hintVerifyingText}
+
+
);
}
if (illuminationState && illuminationState !== IlluminationState.NORMAL) {
- return {IlluminationStateStringMap[illuminationState]};
+ return (
+
+ {IlluminationStateStringMap[illuminationState]}
+
+ );
}
}
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/shared/Overlay.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/shared/Overlay.tsx
index 8d1b202b99c..bc385b10b97 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/shared/Overlay.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/shared/Overlay.tsx
@@ -2,26 +2,23 @@ import * as React from 'react';
import { Flex, FlexProps } from '@aws-amplify/ui-react';
import { LivenessClassNames } from '../types/classNames';
-interface AnchorOrigin {
- horizontal: 'start' | 'center' | 'end';
- vertical: 'start' | 'center' | 'end' | 'space-between';
-}
-
interface OverlayProps extends FlexProps {
- anchorOrigin?: AnchorOrigin;
+ horizontal?: 'start' | 'center' | 'end';
+ vertical?: 'start' | 'center' | 'end' | 'space-between';
}
export const Overlay: React.FC = ({
children,
- anchorOrigin = { horizontal: 'center', vertical: 'center' },
+ horizontal = 'center',
+ vertical = 'center',
className,
...rest
}) => {
return (
{children}
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/shared/Toast.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/shared/Toast.tsx
index 93d06c8b828..bd8d88c9116 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/shared/Toast.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/shared/Toast.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Flex, View, ViewProps } from '@aws-amplify/ui-react';
+import { Flex, View, ViewProps, useTheme } from '@aws-amplify/ui-react';
import { LivenessClassNames } from '../types/classNames';
@@ -7,22 +7,30 @@ interface ToastProps extends ViewProps {
variation?: 'default' | 'primary' | 'error';
size?: 'medium' | 'large';
heading?: string;
+ isInitial?: boolean;
}
export const Toast: React.FC = ({
variation = 'default',
size = 'medium',
children,
+ isInitial = false,
...rest
}) => {
+ const { tokens } = useTheme();
return (
- {children}
+
+ {children}
+
);
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/CancelButton.test.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/CancelButton.test.tsx
index 0da7ceab1f3..38459e7d3cb 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/CancelButton.test.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/CancelButton.test.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
import {
renderWithLivenessProvider,
@@ -44,7 +43,10 @@ describe('CancelButton', () => {
it('should call the send method on cancel', () => {
renderWithLivenessProvider();
- userEvent.click(screen.getByRole('button', { name: buttonAriaLabel }));
+ expect(
+ screen.getByRole('button', { name: buttonAriaLabel })
+ ).toBeInTheDocument();
+ screen.getByRole('button', { name: buttonAriaLabel }).click();
expect(mockActorSend).toHaveBeenCalledWith({
type: 'CANCEL',
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/LivenessIconWithPopover.test.tsx b/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/LivenessIconWithPopover.test.tsx
index 964b6927bf3..4d54a81cfc8 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/LivenessIconWithPopover.test.tsx
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/shared/__tests__/LivenessIconWithPopover.test.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { LivenessIconWithPopover } from '../LivenessIconWithPopover';
+import { act } from 'react-test-renderer';
describe('LivenessIconWithPopover', () => {
it('should render the component content appropriately', () => {
@@ -11,7 +12,9 @@ describe('LivenessIconWithPopover', () => {
const popover = screen.queryByTestId('popover-icon');
expect(screen.queryByTestId('popover-icon')).toBeInTheDocument();
- popover?.click();
+ act(() => {
+ popover?.click();
+ });
expect(screen.queryByTestId('popover-text')).toBeInTheDocument();
expect(screen.getByText(infoText)).toBeInTheDocument();
});
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/types/classNames.ts b/packages/react-liveness/src/components/FaceLivenessDetector/types/classNames.ts
index 300dceb990d..9a6aefe2a50 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/types/classNames.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/types/classNames.ts
@@ -34,6 +34,9 @@ export enum LivenessClassNames {
PopoverAnchorSecondary = 'amplify-liveness-popover__anchor-secondary',
RecordingIconContainer = 'amplify-liveness-recording-icon-container',
RecordingIcon = 'amplify-liveness-recording-icon',
+ StartScreenCameraSelect = 'amplify-liveness-start-screen-camera-select',
+ StartScreenCameraSelectContainer = 'amplify-liveness-start-screen-camera-select__container',
+ StartScreenCameraWaiting = 'amplify-liveness-start-screen-camera-waiting',
StartScreenHeader = 'amplify-liveness-start-screen-header',
StartScreenHeaderBody = 'amplify-liveness-start-screen-header__body',
StartScreenHeaderHeading = 'amplify-liveness-start-screen-header__heading',
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/utils/__tests__/getDisplayText.test.ts b/packages/react-liveness/src/components/FaceLivenessDetector/utils/__tests__/getDisplayText.test.ts
index a922427d500..26634937507 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/utils/__tests__/getDisplayText.test.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/utils/__tests__/getDisplayText.test.ts
@@ -5,7 +5,7 @@ describe('getDisplayText', () => {
const customDisplayText = {
hintTooCloseText: 'Way too close!',
cancelLivenessCheckText: 'Cancel verification process',
- instructionsHeaderHeadingText: 'Verification process',
+ startScreenBeginCheckText: 'Verification process',
cameraNotFoundHeadingText: 'Camera was not found',
};
@@ -22,8 +22,8 @@ describe('getDisplayText', () => {
expect(streamDisplayText.cancelLivenessCheckText).toBe(
customDisplayText.cancelLivenessCheckText
);
- expect(instructionDisplayText.instructionsHeaderHeadingText).toBe(
- customDisplayText.instructionsHeaderHeadingText
+ expect(instructionDisplayText.startScreenBeginCheckText).toBe(
+ customDisplayText.startScreenBeginCheckText
);
expect(cameraDisplayText.cameraNotFoundHeadingText).toBe(
customDisplayText.cameraNotFoundHeadingText
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/utils/getDisplayText.ts b/packages/react-liveness/src/components/FaceLivenessDetector/utils/getDisplayText.ts
index facc4df481f..24f4c8eb153 100644
--- a/packages/react-liveness/src/components/FaceLivenessDetector/utils/getDisplayText.ts
+++ b/packages/react-liveness/src/components/FaceLivenessDetector/utils/getDisplayText.ts
@@ -32,26 +32,19 @@ export function getDisplayText(
};
const {
- instructionsHeaderHeadingText,
- instructionsHeaderBodyText,
- instructionsBeginCheckText,
photosensitivyWarningHeadingText,
photosensitivyWarningBodyText,
photosensitivyWarningInfoText,
- instructionListHeadingText,
goodFitCaptionText,
goodFitAltText,
tooFarCaptionText,
tooFarAltText,
- instructionListStepOneText,
- instructionListStepTwoText,
- instructionListStepThreeText,
- instructionListStepFourText,
cameraMinSpecificationsHeadingText,
cameraMinSpecificationsMessageText,
cameraNotFoundHeadingText,
cameraNotFoundMessageText,
retryCameraPermissionsText,
+ waitingCameraPermissionText,
cancelLivenessCheckText,
recordingIndicatorText,
hintMoveFaceFrontOfCameraText,
@@ -80,6 +73,8 @@ export function getDisplayText(
landscapeMessageText,
portraitMessageText,
tryAgainText,
+ startScreenBeginCheckText,
+ hintCenterFaceText,
} = displayText;
const hintDisplayText: Required = {
@@ -95,6 +90,7 @@ export function getDisplayText(
hintIlluminationTooDarkText,
hintIlluminationNormalText,
hintHoldFaceForFreshnessText,
+ hintCenterFaceText,
};
const cameraDisplayText: Required = {
@@ -103,24 +99,18 @@ export function getDisplayText(
cameraNotFoundHeadingText,
cameraNotFoundMessageText,
retryCameraPermissionsText,
+ waitingCameraPermissionText,
};
const instructionDisplayText: Required = {
- instructionsHeaderHeadingText,
- instructionsHeaderBodyText,
- instructionsBeginCheckText,
photosensitivyWarningHeadingText,
photosensitivyWarningBodyText,
photosensitivyWarningInfoText,
- instructionListHeadingText,
goodFitCaptionText,
goodFitAltText,
tooFarCaptionText,
tooFarAltText,
- instructionListStepOneText,
- instructionListStepTwoText,
- instructionListStepThreeText,
- instructionListStepFourText,
+ startScreenBeginCheckText,
};
const streamDisplayText: Required = {
diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/helpers.ts b/packages/react-liveness/src/components/FaceLivenessDetector/utils/helpers.ts
similarity index 100%
rename from packages/react-liveness/src/components/FaceLivenessDetector/StartLiveness/helpers.ts
rename to packages/react-liveness/src/components/FaceLivenessDetector/utils/helpers.ts
diff --git a/packages/react/__tests__/__snapshots__/exports.ts.snap b/packages/react/__tests__/__snapshots__/exports.ts.snap
index dc54a9b9e03..8e3e95981d5 100644
--- a/packages/react/__tests__/__snapshots__/exports.ts.snap
+++ b/packages/react/__tests__/__snapshots__/exports.ts.snap
@@ -105,6 +105,7 @@ exports[`@aws-amplify/ui-react/internal exports should match snapshot 1`] = `
"IconWarning",
"PrimitiveCatalog",
"useAuth",
+ "useColorMode",
"useDropZone",
"useIcons",
"useStorageURL",
diff --git a/packages/react/src/components/ThemeProvider/ThemeContext.tsx b/packages/react/src/components/ThemeProvider/ThemeContext.tsx
index 3b4bb122851..6d866c8792b 100644
--- a/packages/react/src/components/ThemeProvider/ThemeContext.tsx
+++ b/packages/react/src/components/ThemeProvider/ThemeContext.tsx
@@ -1,11 +1,14 @@
import * as React from 'react';
import { createTheme, WebTheme } from '@aws-amplify/ui';
+import { ColorMode } from './ThemeProvider';
export interface ThemeContextType {
theme: WebTheme;
+ colorMode?: ColorMode;
}
export const ThemeContext = React.createContext({
theme: createTheme(),
+ colorMode: undefined,
});
diff --git a/packages/react/src/components/ThemeProvider/ThemeProvider.tsx b/packages/react/src/components/ThemeProvider/ThemeProvider.tsx
index 074904962b1..a15dfdde409 100644
--- a/packages/react/src/components/ThemeProvider/ThemeProvider.tsx
+++ b/packages/react/src/components/ThemeProvider/ThemeProvider.tsx
@@ -50,7 +50,10 @@ export function ThemeProvider({
nonce,
theme,
}: ThemeProviderProps): JSX.Element {
- const value = React.useMemo(() => ({ theme: createTheme(theme) }), [theme]);
+ const value = React.useMemo(
+ () => ({ theme: createTheme(theme), colorMode }),
+ [theme, colorMode]
+ );
const {
theme: { name, cssText },
} = value;
diff --git a/packages/react/src/hooks/index.tsx b/packages/react/src/hooks/index.tsx
index 5575a5fa6cf..5826fea6581 100644
--- a/packages/react/src/hooks/index.tsx
+++ b/packages/react/src/hooks/index.tsx
@@ -1,3 +1,3 @@
-export { useTheme } from './useTheme';
+export { useTheme, useColorMode } from './useTheme';
export { useBreakpointValue } from './useBreakpointValue';
diff --git a/packages/react/src/hooks/useTheme.ts b/packages/react/src/hooks/useTheme.ts
index 6a22e6e3fe8..069d524457e 100644
--- a/packages/react/src/hooks/useTheme.ts
+++ b/packages/react/src/hooks/useTheme.ts
@@ -1,6 +1,7 @@
import * as React from 'react';
import { createTheme, WebTheme } from '@aws-amplify/ui';
+import { ColorMode } from '../components/ThemeProvider/ThemeProvider';
import {
ThemeContext,
ThemeContextType,
@@ -27,3 +28,11 @@ export const useTheme = (): WebTheme => {
const context = React.useContext(ThemeContext);
return getThemeFromContext(context);
};
+
+/**
+ * Internal use only
+ */
+export const useColorMode = (): ColorMode | undefined => {
+ const context = React.useContext(ThemeContext);
+ return context.colorMode;
+};
diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts
index ef34d9e458d..b87e318929e 100644
--- a/packages/react/src/internal.ts
+++ b/packages/react/src/internal.ts
@@ -1,6 +1,7 @@
export * from './hooks/useAuth';
export * from './hooks/useStorageURL';
export * from './hooks/useThemeBreakpoint';
+export { useColorMode } from './hooks/useTheme';
export { AlertIcon } from './primitives/Alert/AlertIcon';
export * from './primitives/Icon/internal';
diff --git a/packages/ui/src/theme/css/component/liveness.scss b/packages/ui/src/theme/css/component/liveness.scss
index 580fee282c8..046c066ce94 100644
--- a/packages/ui/src/theme/css/component/liveness.scss
+++ b/packages/ui/src/theme/css/component/liveness.scss
@@ -6,7 +6,8 @@
}
.amplify-liveness-cancel-button {
- background-color: var(--amplify-colors-background-primary);
+ background-color: #fff;
+ color: hsl(190, 95%, 30%);
}
.amplify-liveness-fade-out {
@@ -49,6 +50,7 @@
left: 0;
width: 100%;
height: 100%;
+ transform: scaleX(-1);
}
.amplify-liveness-freshness-canvas {
@@ -97,12 +99,16 @@
.amplify-liveness-recording-icon {
flex-direction: column;
align-items: center;
- background-color: var(--amplify-colors-background-primary);
+ background-color: #fff;
padding: var(--amplify-space-xxs);
gap: var(--amplify-space-xxs);
border-radius: var(--amplify-radii-small);
}
+.amplify-liveness-recording-icon .amplify-text {
+ color: var(--amplify-colors-black);
+}
+
.amplify-liveness-instruction-overlay {
z-index: 1;
}
@@ -123,7 +129,7 @@
.amplify-liveness-toast {
background-color: var(--amplify-colors-background-primary);
padding: var(--amplify-space-small);
- border-radius: var(--amplify-radii-medium);
+ max-width: 100%;
}
.amplify-liveness-toast__message {
color: var(--amplify-colors-font-primary);
@@ -131,8 +137,13 @@
flex-direction: column;
}
+.amplify-liveness-toast--medium {
+ border-radius: var(--amplify-radii-medium);
+}
+
.amplify-liveness-toast--large {
- font-size: var(--amplify-font-sizes-xl);
+ font-size: var(--amplify-font-sizes-xxl);
+ padding: 0 var(--amplify-space-xs);
}
.amplify-liveness-toast--primary {
@@ -289,9 +300,10 @@
}
.amplify-liveness-start-screen-warning {
- color: var(--amplify-colors-orange-80);
- background-color: var(--amplify-colors-orange-20);
+ color: var(--amplify-colors-blue-90);
+ background-color: var(--amplify-colors-blue-20);
align-items: center;
+ z-index: 3;
}
.amplify-liveness-start-screen-instructions__heading {
@@ -310,7 +322,7 @@
top: 0;
width: 100%;
height: 100%;
- padding: var(--amplify-space-xl);
+ padding: var(--amplify-space-large);
}
.amplify-liveness-error-modal {
@@ -367,4 +379,41 @@
width: 240px;
border: 1px solid var(--amplify-colors-border-secondary);
border-radius: 2px;
+ z-index: 4;
+}
+
+.amplify-liveness-start-screen-camera-select {
+ flex-direction: column;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ padding: var(--amplify-space-xl);
+ align-items: center;
+ justify-content: flex-end;
+ display: flex;
+ z-index: 2;
+}
+
+.amplify-liveness-start-screen-camera-select__container {
+ display: flex;
+ justify-content: space-between;
+ align-items: inherit;
+ gap: var(--amplify-space-xs);
+}
+
+.amplify-liveness-start-screen-camera-select__label,
+.amplify-liveness-start-screen-camera-select .amplify-select,
+.amplify-liveness-start-screen-camera-select .amplify-select__wrapper,
+.amplify-liveness-start-screen-camera-select .amplify-select__icon-wrapper {
+ background-color: var(--amplify-colors-background-primary);
+ color: var(--amplify-colors-font-primary);
+}
+
+.amplify-liveness-start-screen-camera-waiting {
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 480px;
}