diff --git a/.changeset/brown-moles-grow.md b/.changeset/brown-moles-grow.md new file mode 100644 index 00000000000..daae4236386 --- /dev/null +++ b/.changeset/brown-moles-grow.md @@ -0,0 +1,7 @@ +--- +"@aws-amplify/ui": minor +"@aws-amplify/ui-react": minor +"@aws-amplify/ui-react-liveness": major +--- + +feat: Add ui-react-liveness and FaceLivenessDetector component diff --git a/.github/changeset-presets/bump-react.md b/.github/changeset-presets/bump-react.md index cb55fd41b80..f6fbb5539ae 100644 --- a/.github/changeset-presets/bump-react.md +++ b/.github/changeset-presets/bump-react.md @@ -3,6 +3,7 @@ '@aws-amplify/ui-react': minor '@aws-amplify/ui-react-core': patch '@aws-amplify/ui-react-native': major +'@aws-amplify/ui-react-liveness': minor --- -Version bump for ui, ui-react, ui-react-native and ui-react-core packages +Version bump for ui, ui-react, ui-react-native, ui-react-liveness and ui-react-core packages diff --git a/.github/changeset-presets/bump-versions.md b/.github/changeset-presets/bump-versions.md index 0fdd15263a4..403329d617d 100644 --- a/.github/changeset-presets/bump-versions.md +++ b/.github/changeset-presets/bump-versions.md @@ -5,6 +5,7 @@ '@aws-amplify/ui-vue': patch '@aws-amplify/ui-react-core': patch '@aws-amplify/ui-react-native': patch +'@aws-amplify/ui-react-liveness': patch --- Version bump for all public packages diff --git a/.github/workflow-samples/tagged-release.yml b/.github/workflow-samples/tagged-release.yml index 7b42e2ed4f5..d695b8fa887 100644 --- a/.github/workflow-samples/tagged-release.yml +++ b/.github/workflow-samples/tagged-release.yml @@ -43,6 +43,7 @@ jobs: GEO_E2E_ROLE_ARN: ${{ secrets.GEO_E2E_ROLE_ARN }} STORAGE_E2E_ROLE_ARN: ${{ secrets.STORAGE_E2E_ROLE_ARN }} IN_APP_MESSAGING_E2E_ROLE_ARN: ${{ secrets.IN_APP_MESSAGING_E2E_ROLE_ARN }} + LIVENESS_E2E_ROLE_ARN: ${{ secrets.LIVENESS_E2E_ROLE_ARN }} DOMAIN: ${{ secrets.DOMAIN }} PHONE_NUMBER: ${{ secrets.PHONE_NUMBER }} USERNAME: ${{ secrets.USERNAME }} diff --git a/.github/workflows/publish-next.yml b/.github/workflows/publish-next.yml index 30ff4de756a..51782f0f99d 100644 --- a/.github/workflows/publish-next.yml +++ b/.github/workflows/publish-next.yml @@ -42,6 +42,7 @@ jobs: DATASTORE_E2E_ROLE_ARN: ${{ secrets.DATASTORE_E2E_ROLE_ARN }} GEO_E2E_ROLE_ARN: ${{ secrets.GEO_E2E_ROLE_ARN }} STORAGE_E2E_ROLE_ARN: ${{ secrets.STORAGE_E2E_ROLE_ARN }} + LIVENESS_E2E_ROLE_ARN: ${{ secrets.LIVENESS_E2E_ROLE_ARN }} IN_APP_MESSAGING_E2E_ROLE_ARN: ${{ secrets.IN_APP_MESSAGING_E2E_ROLE_ARN }} DOMAIN: ${{ secrets.DOMAIN }} PHONE_NUMBER: ${{ secrets.PHONE_NUMBER }} diff --git a/.github/workflows/reusable-e2e.yml b/.github/workflows/reusable-e2e.yml index 0667aa1fcf5..61b8816fb6c 100644 --- a/.github/workflows/reusable-e2e.yml +++ b/.github/workflows/reusable-e2e.yml @@ -24,6 +24,8 @@ on: required: true IN_APP_MESSAGING_E2E_ROLE_ARN: required: true + LIVENESS_E2E_ROLE_ARN: + required: true DOMAIN: required: true PHONE_NUMBER: @@ -154,6 +156,10 @@ jobs: if: ${{ matrix.package == 'react' }} run: yarn react-storage build + - name: Build react-liveness package + if: ${{ matrix.package == 'react' }} + run: yarn react-liveness build + - name: Add Amplify CLI run: yarn global add @aws-amplify/cli @@ -228,6 +234,22 @@ jobs: - name: Pull down Datastore AWS environments run: yarn environments datastore pull + - name: Configure liveness credentials + uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef # v2.0.0 https://github.com/aws-actions/configure-aws-credentials/commit/e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef + with: + aws-region: us-west-2 + role-to-assume: ${{ secrets.LIVENESS_E2E_ROLE_ARN }} + + - name: Create temp AWS profile + run: | + aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID && \ + aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY && \ + aws configure set aws_session_token $AWS_SESSION_TOKEN && \ + aws configure set default.region $AWS_REGION + + - name: Pull down Liveness AWS environments + run: yarn environments liveness pull + - name: Configure in-app-messaging credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -255,6 +277,9 @@ jobs: - name: Start ${{ matrix.example }} example run: yarn workspace ${{ matrix.example }}-example start & npx wait-on -c waitOnConfig.json -t 20000 http-get://localhost:3000/ui/components/authenticator/sign-in-with-username + env: + # Setting this value temporarily since the beta liveness sample app hits the gamma endpoint + NEXT_PUBLIC_STREAMING_API_URL: wss://streaming-rekognition-gamma.us-east-1.amazonaws.com - name: Run E2E tests against ${{ matrix.example }} example id: e2e @@ -348,6 +373,9 @@ jobs: - name: Build react-storage package run: yarn react-storage build + - name: Build react-liveness package + run: yarn react-liveness build + - name: Build docs package run: yarn docs build env: @@ -371,6 +399,8 @@ jobs: VALID_PASSWORD: ${{ secrets.VALID_PASSWORD }} - name: Run Docs link checker + # TODO: remove continue-on-error after all the links are available + continue-on-error: true run: yarn docs test:links - name: Upload failure screenshots and errors diff --git a/.github/workflows/reusable-unit.yml b/.github/workflows/reusable-unit.yml index 198059faded..e55efce354f 100644 --- a/.github/workflows/reusable-unit.yml +++ b/.github/workflows/reusable-unit.yml @@ -28,6 +28,7 @@ jobs: - react-native - react-core - react-storage + - react-liveness steps: - name: Checkout Amplify UI @@ -76,11 +77,11 @@ jobs: run: yarn ui build - name: Build react-core package - if: ${{ matrix.package == 'react' || matrix.package == 'react-native' || matrix.package == 'react-storage' }} + if: ${{ matrix.package == 'react' || matrix.package == 'react-native' || matrix.package == 'react-storage' || matrix.package == 'react-liveness' }} run: yarn react-core build - name: Build react package - if: ${{ matrix.package == 'react-storage' && matrix.package != 'react'}} + if: ${{ matrix.package == 'react-storage' || matrix.package == 'react-liveness' && matrix.package != 'react'}} run: yarn react build - name: Build ${{ matrix.package }} package diff --git a/.github/workflows/test-internal-prs.yml b/.github/workflows/test-internal-prs.yml index e0d9c05e196..a32ec9c4181 100644 --- a/.github/workflows/test-internal-prs.yml +++ b/.github/workflows/test-internal-prs.yml @@ -111,7 +111,7 @@ jobs: repository: ${{ github.repository }} e2e: - uses: aws-amplify/amplify-ui/.github/workflows/reusable-e2e.yml@main + uses: aws-amplify/amplify-ui/.github/workflows/reusable-e2e.yml@liveness-main needs: unit with: commit: ${{ github.event.pull_request.head.sha }} @@ -123,6 +123,7 @@ jobs: DATASTORE_E2E_ROLE_ARN: ${{ secrets.DATASTORE_E2E_ROLE_ARN }} GEO_E2E_ROLE_ARN: ${{ secrets.GEO_E2E_ROLE_ARN }} STORAGE_E2E_ROLE_ARN: ${{ secrets.STORAGE_E2E_ROLE_ARN }} + LIVENESS_E2E_ROLE_ARN: ${{ secrets.LIVENESS_E2E_ROLE_ARN }} IN_APP_MESSAGING_E2E_ROLE_ARN: ${{ secrets.IN_APP_MESSAGING_E2E_ROLE_ARN }} DOMAIN: ${{ secrets.DOMAIN }} PHONE_NUMBER: ${{ secrets.PHONE_NUMBER }} diff --git a/canary/e2e/cypress/integration/common/liveness.ts b/canary/e2e/cypress/integration/common/liveness.ts new file mode 100644 index 00000000000..f89daf3d6b3 --- /dev/null +++ b/canary/e2e/cypress/integration/common/liveness.ts @@ -0,0 +1,14 @@ +/// +/// +/// + +import { When } from 'cypress-cucumber-preprocessor/steps'; + +When( + 'I request {string} and get {string}', + (url: string, statusCode: string) => { + cy.request({ url, followRedirect: true }).then(({ status }) => { + expect(status).to.be.equal(+statusCode); + }); + } +); diff --git a/canary/e2e/features/liveness/face-detect.feature b/canary/e2e/features/liveness/face-detect.feature new file mode 100644 index 00000000000..3e71cf30baa --- /dev/null +++ b/canary/e2e/features/liveness/face-detect.feature @@ -0,0 +1,11 @@ +Feature: Liveness Start Screen + + Test Liveness CDN + + Background: + Given I'm running the example "/" + + @react @skip + Scenario: Blazeface CDN is up + Then I request "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm" and get "200" + And I request "https://tfhub.dev/tensorflow/tfjs-model/blazeface/1/default/1/model.json?tfjs-format=file" and get "200" diff --git a/docs/public/android-preview.png b/docs/public/android-preview.png new file mode 100644 index 00000000000..a0c26d8ec30 Binary files /dev/null and b/docs/public/android-preview.png differ diff --git a/docs/public/swift-preview.png b/docs/public/swift-preview.png new file mode 100644 index 00000000000..a0c26d8ec30 Binary files /dev/null and b/docs/public/swift-preview.png differ diff --git a/docs/src/components/InstallScripts.tsx b/docs/src/components/InstallScripts.tsx index f882368ef4d..f4e1fd42a71 100644 --- a/docs/src/components/InstallScripts.tsx +++ b/docs/src/components/InstallScripts.tsx @@ -10,11 +10,13 @@ interface TerminalCommandProps { packageManager?: PackageManager; command?: string; variant?: 'default' | 'hero'; + component?: string; } const frameworkInstallScript = ( framework: Framework, - packageManager: PackageManager + packageManager: PackageManager, + component?: string ) => { const isReactNative = framework === 'react-native'; @@ -26,18 +28,21 @@ const frameworkInstallScript = ( isReactNative ? ` ${REACT_NATIVE_DEPENDENCIES}` : '' }`; - return `${packageManagerPrefix} @aws-amplify/ui-${framework} aws-amplify${extraDependencies}`; + const componentSubpackage = component ? `-${component}` : null; + + return `${packageManagerPrefix} @aws-amplify/ui-${framework}${componentSubpackage} aws-amplify${extraDependencies}`; }; export const TerminalCommand = ({ framework, + component, packageManager, command, variant = 'default', }: TerminalCommandProps) => { const terminalCommand = command ? command - : frameworkInstallScript(framework, packageManager); + : frameworkInstallScript(framework, packageManager, component); return (
@@ -54,9 +59,13 @@ export const TerminalCommand = ({ interface InstallScriptsProps { framework?: Framework; + component?: string; } -export const InstallScripts = ({ framework }: InstallScriptsProps) => { +export const InstallScripts = ({ + framework, + component, +}: InstallScriptsProps) => { const { query: { platform = 'react' }, } = useRouter(); @@ -69,10 +78,18 @@ export const InstallScripts = ({ framework }: InstallScriptsProps) => { return ( - + - + ); diff --git a/docs/src/components/Layout/Sidebar.tsx b/docs/src/components/Layout/Sidebar.tsx index b9530eec885..1b45637df45 100644 --- a/docs/src/components/Layout/Sidebar.tsx +++ b/docs/src/components/Layout/Sidebar.tsx @@ -140,21 +140,30 @@ const SecondaryNav = (props) => { const isFlutter = platform === 'flutter'; const isReactNative = platform === 'react-native'; + const isAndroid = platform === 'android'; + const isSwift = platform === 'swift'; + + const hideGettingStarted = isAndroid || isSwift; + const hideTheming = isAndroid || isSwift; + const hideGuidesExpander = isFlutter || isReactNative || isAndroid || isSwift; return ( - - } - value="getting-started" - > - {gettingStarted.map(({ label, ...rest }) => ( - - {label} - - ))} - + {/* Android and Swift don't have getting started at this time */} + {hideGettingStarted ? null : ( + + } + value="getting-started" + > + {gettingStarted.map(({ label, ...rest }) => ( + + {label} + + ))} + + )} {platform === 'react' ? ( } @@ -177,26 +186,29 @@ const SecondaryNav = (props) => { } value="connected-components" > - {connectedComponents.map(({ label, ...rest }) => ( - + {connectedComponents.map(({ label, href, ...rest }) => ( + {label} ))} - } - value="theming" - > - {theming.map(({ label, ...rest }) => ( - - {label} - - ))} - + {/* Android and Swift don't have theming at this time */} + {hideTheming ? null : ( + } + value="theming" + > + {theming.map(({ label, ...rest }) => ( + + {label} + + ))} + + )} - {/* Flutter and React Native don't have guides at this time */} - {isFlutter || isReactNative ? null : ( + {/* Flutter, React Native, Android, and Swift don't have guides at this time */} + {hideGuidesExpander ? null : ( } value="guides" diff --git a/docs/src/components/Layout/index.tsx b/docs/src/components/Layout/index.tsx index 34961c12fb2..9729ff8c1dc 100644 --- a/docs/src/components/Layout/index.tsx +++ b/docs/src/components/Layout/index.tsx @@ -1,10 +1,16 @@ import * as React from 'react'; import debounce from 'lodash/debounce'; +import { useRouter } from 'next/router'; import { Heading, Link, Text, View, useTheme } from '@aws-amplify/ui-react'; import { TableOfContents } from '../TableOfContents'; import { Footer } from './Footer'; -import { GITHUB_REPO, GITHUB_REPO_FILE } from '@/data/links'; +import { + GITHUB_REPO, + ANDROID_GITHUB_REPO, + SWIFT_GITHUB_REPO, + GITHUB_REPO_FILE, +} from '@/data/links'; import { DesignTokenIcon, ReactIcon, @@ -72,6 +78,11 @@ export default function Page({ return () => window.removeEventListener('load', scrollToHash); }, []); + const { + query: { platform: framework = 'react' }, + } = useRouter(); + const githubRepo = getGitHubRepo(framework as string); + return ( <>
@@ -122,7 +133,7 @@ export default function Page({ ) : null} ); } + +function getGitHubRepo(framework = 'react') { + if (framework === 'android') { + return ANDROID_GITHUB_REPO; + } else if (framework === 'swift') { + return SWIFT_GITHUB_REPO; + } + return GITHUB_REPO; +} diff --git a/docs/src/components/Logo.tsx b/docs/src/components/Logo.tsx index 8d6ffddc5a0..c4d7fdbd119 100644 --- a/docs/src/components/Logo.tsx +++ b/docs/src/components/Logo.tsx @@ -81,6 +81,38 @@ export const FlutterLogo = (props) => ( ); +export const AndroidLogo = (props) => ( + + + + + +); + +export const SwiftLogo = (props) => ( + + + + + + +); + export const FrameworkLogo = ({ framework, ...rest }) => { switch (framework) { case 'react': @@ -91,6 +123,10 @@ export const FrameworkLogo = ({ framework, ...rest }) => { return ; case 'flutter': return ; + case 'android': + return ; + case 'swift': + return ; default: return ; } diff --git a/docs/src/components/home/sections/AmplifySection.tsx b/docs/src/components/home/sections/AmplifySection.tsx index fe50acc5311..239a9b4ef71 100644 --- a/docs/src/components/home/sections/AmplifySection.tsx +++ b/docs/src/components/home/sections/AmplifySection.tsx @@ -61,7 +61,11 @@ export const AmplifySection = ({ platform }) => {
Learn more about Amplify Libraries diff --git a/docs/src/components/home/sections/HeroSection.tsx b/docs/src/components/home/sections/HeroSection.tsx index b71b8a8efeb..ff27efa7257 100644 --- a/docs/src/components/home/sections/HeroSection.tsx +++ b/docs/src/components/home/sections/HeroSection.tsx @@ -102,43 +102,37 @@ export const HeroSection = () => { ) : null} - {platform === 'flutter' ? ( - - ) : ( - + {platform === 'swift' ? null : ( + )} - - - {platform === 'react' && showEditor ? ( + {platform === 'swift' || platform === 'android' ? null : ( + - ) : null} - + {platform === 'react' && showEditor ? ( + + ) : null} + + )} diff --git a/docs/src/data/frameworks.ts b/docs/src/data/frameworks.ts index d2cee648e2e..96c4f2ea73b 100644 --- a/docs/src/data/frameworks.ts +++ b/docs/src/data/frameworks.ts @@ -1,24 +1,30 @@ export type Framework = + | 'android' | 'angular' | 'flutter' | 'react' | 'react-native' + | 'swift' | 'vue'; export type Frameworks = Framework[]; export const FRAMEWORKS: Frameworks = [ + 'android', 'angular', 'flutter', 'react', 'react-native', + 'swift', 'vue', ]; export const FRAMEWORK_DISPLAY_NAMES: Record = { + android: 'Android', angular: 'Angular', flutter: 'Flutter', react: 'React', 'react-native': 'React Native', + swift: 'Swift', vue: 'Vue', }; @@ -27,9 +33,10 @@ export const REACT_NATIVE_DEPENDENCIES = 'react-native-safe-area-context amazon-cognito-identity-js @react-native-community/netinfo @react-native-async-storage/async-storage react-native-get-random-values react-native-url-polyfill'; export const FRAMEWORK_INSTALL_SCRIPTS = { - react: '@aws-amplify/ui-react aws-amplify', - vue: '@aws-amplify/ui-vue aws-amplify', - angular: '@aws-amplify/ui-angular aws-amplify', - flutter: 'amplify_authenticator', - 'react-native': `@aws-amplify/ui-react-native aws-amplify ${REACT_NATIVE_DEPENDENCIES}`, + react: 'npm i @aws-amplify/ui-react aws-amplify', + vue: 'npm i @aws-amplify/ui-vue aws-amplify', + angular: 'npm i @aws-amplify/ui-angular aws-amplify', + flutter: 'flutter pub add amplify_authenticator', + android: "implementation 'com.amplifyframework.ui:liveness:1.0.0'", + 'react-native': `npm i @aws-amplify/ui-react-native aws-amplify ${REACT_NATIVE_DEPENDENCIES}`, }; diff --git a/docs/src/data/links.tsx b/docs/src/data/links.tsx index e8abc6c53d8..7a61107b5c0 100644 --- a/docs/src/data/links.tsx +++ b/docs/src/data/links.tsx @@ -145,6 +145,18 @@ export const connectedComponents: ComponentNavItem[] = [ platforms: ['react'], tertiary: true, }, + { + href: '/connected-components/liveness', + label: 'Liveness', + body: 'Amplify UI provides a UI component for detecting whether the person in front of the camera is live.', + platforms: ['react', 'android', 'swift'], + }, + { + href: '/connected-components/liveness/customization', + label: 'Customization', + platforms: ['react', 'android', 'swift'], + tertiary: true, + }, { href: '/connected-components/geo', label: 'Geo', @@ -607,4 +619,6 @@ export const ANDROID_REFERENCE = export const JS_REFERENCE = 'https://aws-amplify.github.io/amplify-js/api/'; export const GITHUB_REPO = `${GITHUB}/amplify-ui/`; +export const ANDROID_GITHUB_REPO = `${GITHUB}/amplify-ui-android/`; +export const SWIFT_GITHUB_REPO = `${GITHUB}/amplify-ui-swift/`; export const GITHUB_REPO_FILE = `${GITHUB_REPO}/blob/main/`; diff --git a/docs/src/data/test/tsconfig.json b/docs/src/data/test/tsconfig.json new file mode 100644 index 00000000000..3b3f8cedad9 --- /dev/null +++ b/docs/src/data/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "isolatedModules": false + } +} diff --git a/docs/src/pages/[platform]/connected-components/authenticator/configuration/index.page.mdx b/docs/src/pages/[platform]/connected-components/authenticator/configuration/index.page.mdx index 0e9ca14d9bc..5d3cc68d8ce 100644 --- a/docs/src/pages/[platform]/connected-components/authenticator/configuration/index.page.mdx +++ b/docs/src/pages/[platform]/connected-components/authenticator/configuration/index.page.mdx @@ -2,7 +2,7 @@ title: Configuration metaTitle: Configuration description: How to setup and configure your Authenticator component. -supportedFrameworks: all +supportedFrameworks: angular|flutter|react|react-native|vue --- import { Fragment } from '@/components/Fragment'; diff --git a/docs/src/pages/[platform]/connected-components/authenticator/customization/index.page.mdx b/docs/src/pages/[platform]/connected-components/authenticator/customization/index.page.mdx index 8eba7af7d4f..d7b0c05c686 100644 --- a/docs/src/pages/[platform]/connected-components/authenticator/customization/index.page.mdx +++ b/docs/src/pages/[platform]/connected-components/authenticator/customization/index.page.mdx @@ -2,7 +2,7 @@ title: Customization metaTitle: Customization description: Override and customize your Authenticator. -supportedFrameworks: all +supportedFrameworks: angular|flutter|react|react-native|vue --- import { Fragment } from '@/components/Fragment'; diff --git a/docs/src/pages/[platform]/connected-components/authenticator/index.page.mdx b/docs/src/pages/[platform]/connected-components/authenticator/index.page.mdx index f35ed1ba32e..0f2a227102a 100644 --- a/docs/src/pages/[platform]/connected-components/authenticator/index.page.mdx +++ b/docs/src/pages/[platform]/connected-components/authenticator/index.page.mdx @@ -1,7 +1,7 @@ --- title: Authenticator description: Authenticator component adds complete authentication flows to your application with minimal boilerplate. -supportedFrameworks: all +supportedFrameworks: angular|flutter|react|react-native|vue --- import { Fragment } from '@/components/Fragment'; diff --git a/docs/src/pages/[platform]/connected-components/liveness/QuickStartReact.tsx b/docs/src/pages/[platform]/connected-components/liveness/QuickStartReact.tsx new file mode 100644 index 00000000000..e29ae578e1e --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/QuickStartReact.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness'; +import { Loader, ThemeProvider } from '@aws-amplify/ui-react'; + +export function LivenessQuickStartReact() { + const [loading, setLoading] = React.useState(true); + const [createLivenessApiData, setCreateLivenessApiData] = React.useState<{ + sessionId: string; + } | null>(null); + + React.useEffect(() => { + const fetchCreateLiveness = async () => { + /* + * This should be replaced with a real call to your own backend API + */ + await new Promise((r) => setTimeout(r, 2000)); + const mockResponse = { sessionId: 'mockSessionId' }; + const data = mockResponse; + + setCreateLivenessApiData(data); + setLoading(false); + }; + + fetchCreateLiveness(); + }, []); + + const handleAnalysisComplete = async () => { + /* + * This should be replaced with your own backend API + */ + const response = await fetch( + `/api/get?sessionId=${createLivenessApiData.sessionId}` + ); + const data = await response.json(); + + /* + * Note: The isLive flag is not returned from the GetFaceLivenessSession API + * This should be returned from your backend based on the score that you + * get in response. Based on the return value of your API you can determine what to render next. + * Any next steps from an authorization perspective should happen in your backend and you should not rely + * on this value for any auth related decisions. + */ + if (data.isLive) { + console.log('User is live'); + } else { + console.log('User is not live'); + } + }; + + return ( + + {loading ? ( + + ) : ( + + )} + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/liveness/amplify-cli.mdx b/docs/src/pages/[platform]/connected-components/liveness/amplify-cli.mdx new file mode 100644 index 00000000000..1eeac08c6ae --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/amplify-cli.mdx @@ -0,0 +1,21 @@ +import { Tabs, TabItem } from '@aws-amplify/ui-react'; +import { TerminalCommand } from '@/components/InstallScripts'; + +The FaceLivenessDetector works seamlessly with the [Amplify CLI](https://docs.amplify.aws/cli/start/install/) +to **automatically** integrate calls to the Liveness service with your authenticated users. + +First, update `@aws-amplify/cli` with [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) +if you're using a version before `6.4.0`: + + + + + + + + + + + + + diff --git a/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.android.mdx b/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.android.mdx new file mode 100644 index 00000000000..874e19c2543 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.android.mdx @@ -0,0 +1,28 @@ +## Auth + +By default, the FaceLivenessDetector uses Amplify Auth to authorize users to perform the liveness check. You can use your own credentials provider to retrieve credentials from [Amazon Cognito](https://aws.amazon.com/cognito/): + +```kotlin +class MyCredentialsProvider : AWSCredentialsProvider { + override fun fetchAWSCredentials( + onSuccess: Consumer, + onError: Consumer + ) { + // Fetch the credentials + } +} +``` + +```kotlin +FaceLivenessDetector( + sessionId = , + region = , + credentialsProvider = MyCredentialsProvider(), + onComplete = { + Log.i("MyApp", "Liveness flow is complete") + }, + onError = { error -> + Log.e("MyApp", "Error during Liveness flow", error) + } +) +``` diff --git a/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.react.mdx b/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.react.mdx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.swift.mdx b/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.swift.mdx new file mode 100644 index 00000000000..9d8d7af522e --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/credentials-provider.swift.mdx @@ -0,0 +1,41 @@ +## Auth + +By default, the FaceLivenessDetectorView uses Amplify Auth to authorize users to perform the liveness check. You can use your own credentials provider to retrieve credentials from [Amazon Cognito](https://aws.amazon.com/cognito/): + +```swift +import Amplify + +struct MyCredentialsProvider: AWSCredentialsProvider { + func fetchAWSCredentials() async throws -> AWSCredentials { + // Fetch the credentials + } +} +``` + +```swift +import SwiftUI +import FaceLiveness + +struct MyView: View { + @State private var isPresentingLiveness = true + + var body: some View { + FaceLivenessDetectorView( + sessionID: , + region: , + credentialsProvider: MyCredentialsProvider(), + isPresented: $isPresentingLiveness, + onCompletion: { result in + switch result { + case .success: + // ... + case .failure(let error): + // ... + default: + // ... + } + } + ) + } +} +``` diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationComponents.tsx b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationComponents.tsx new file mode 100644 index 00000000000..b23dc985dec --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationComponents.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness'; +import { View, Heading, Alert, Card, Text } from '@aws-amplify/ui-react'; + +export function CustomizationComponents() { + return ( + {}} + 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 ( + + This check displays colored lights. Use caution if you are + photosensitive. + + ); + }, + Instructions: (): JSX.Element => { + return ( + + Instructions to follow to use liveness face detector +
    +
  1. + Make sure your face is not covered with sunglasses or a mask. +
  2. +
  3. + Move to a well-lit place that is not dark or in direct + sunlight. +
  4. +
  5. + Fill onscreen oval with your face and hold for colored lights. +
  6. +
+
+ ); + }, + ErrorView: ({ children }) => { + return ( + + My Custom Error View + {children} + + ); + }, + }} + /> + ); +} diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationTable.tsx b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationTable.tsx new file mode 100644 index 00000000000..b4e682ea85e --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationTable.tsx @@ -0,0 +1,78 @@ +import { + Link, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from '@aws-amplify/ui-react'; +export function CustomizationTable() { + return ( + + + + Component + Customizable? + + + + + Text/Language + + Link + + + + Theme (colors, margin sizes, text sizes, etc) + + Link + + + + Get Ready Screen Header + + Link + + + + Get Ready Screen Photosensitivity Warning + + Link + + + + Get Ready Screen Instruction List + + Link + + + + Recording Icon + No + + + Countdown Timer + No + + + Face Match Timeout + No + + + Oval Placement/Size + No + + + + Colors or length of colors in sequence of colored lights + + No + + + Cancel Button + No + + +
+ ); +} diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationTheme.tsx b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationTheme.tsx new file mode 100644 index 00000000000..17462ef4d1f --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/CustomizationTheme.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness'; +import { Theme, ThemeProvider, useTheme } from '@aws-amplify/ui-react'; + +export function CustomizationTheme() { + const { tokens } = useTheme(); + const theme: Theme = { + name: 'Liveness Example Theme', + tokens: { + colors: { + background: { + primary: { + value: tokens.colors.neutral['90'].value, + }, + secondary: { + value: tokens.colors.neutral['100'].value, + }, + }, + font: { + primary: { + value: tokens.colors.white.value, + }, + }, + brand: { + primary: { + '10': tokens.colors.teal['100'], + '80': tokens.colors.teal['40'], + '90': tokens.colors.teal['20'], + '100': tokens.colors.teal['10'], + }, + }, + }, + }, + }; + + 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 new file mode 100644 index 00000000000..c4476b6e53a --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/Customizationi18n.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness'; +import { ToggleButtonGroup, ToggleButton } from '@aws-amplify/ui-react'; + +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', + }, +}; + +export function Customizationi18n() { + const [language, setLanguage] = React.useState('en'); + return ( + <> + setLanguage(value)} + > + En + Es + + {}} + displayText={dictionary[language]} + /> + + ); +} 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 new file mode 100644 index 00000000000..a3579db2d7d --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.components.react.mdx @@ -0,0 +1,20 @@ +import { Example } from '@/components/Example'; +import { CustomizationComponents } from './CustomizationComponents'; + +## Components + +Liveness component allows overriding some UI components using the `components` prop. + +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 + +```tsx file=./CustomizationComponents.tsx +``` + + + + diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.android.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.android.mdx new file mode 100644 index 00000000000..595fa08a4dc --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.android.mdx @@ -0,0 +1 @@ +The text in the FaceLivenessDetector component is defined as string resources in the component's [strings.xml file](https://github.com/aws-amplify/amplify-ui-android/blob/main/liveness/src/main/res/values/strings.xml). These values can be translated/overwritten by following the instructions [here](https://developer.android.com/guide/topics/resources/localization#creating-alternatives). diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.react.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.react.mdx new file mode 100644 index 00000000000..d4c1147097e --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.react.mdx @@ -0,0 +1,11 @@ +import { Example } from '@/components/Example'; +import { Customizationi18n } from './Customizationi18n'; + +The text in the FaceLivenessDetector component is defined as string resources in the component's [displayText.ts file](https://github.com/aws-amplify/amplify-ui/blob/main/packages/react-liveness/src/components/FaceLivenessDetector/displayText.ts). These values can be translated/overwritten by following the example below: + +```tsx file=./Customizationi18n.tsx +``` + + + + diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.swift.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.swift.mdx new file mode 100644 index 00000000000..35526cdfa80 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.i18n.swift.mdx @@ -0,0 +1 @@ +The text in the FaceLivenessDetectorView component is defined as string resources in the component's [Localizable.strings file](https://github.com/aws-amplify/amplify-ui-swift-liveness/blob/main/Sources/FaceLiveness/Resources/Base.lproj/Localizable.strings). These values can be translated/overwritten by following the instructions [here](https://developer.apple.com/documentation/xcode/localization). diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.android.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.android.mdx new file mode 100644 index 00000000000..fb93e6a8f6b --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.android.mdx @@ -0,0 +1,15 @@ +The FaceLivenessDetector contains an initial (start) view with instructions and information about the liveness check for the end user. By default, the start view is shown before the liveness check. The start view can be disabled, allowing you to provide your own start view before the FaceLivenessDetector component is displayed in the app: + +```kotlin +FaceLivenessDetector( + sessionId = , + region = , + disableStartView = true, + onComplete = { + Log.i("MyApp", "Liveness flow is complete") + }, + onError = { error -> + Log.e("MyApp", "Error during Liveness flow", error) + } +) +``` diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.react.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.react.mdx new file mode 100644 index 00000000000..d609382cef5 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.react.mdx @@ -0,0 +1,10 @@ +The FaceLivenessDetector contains an initial (start) view with instructions and information about the liveness check for the end user. By default, the start view is shown before the liveness check. The start view can be disabled, allowing you to provide your own start view before the FaceLivenessDetector component is displayed in the app: + +```jsx + +``` diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.swift.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.swift.mdx new file mode 100644 index 00000000000..94ffa169fdc --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.start-view.swift.mdx @@ -0,0 +1,20 @@ +The FaceLivenessDetectorView contains an initial (start) view with instructions and information about the liveness check for the end user. By default, the start view is shown before the liveness check. The start view can be disabled, allowing you to provide your own start view before the FaceLivenessDetectorView component is displayed in the app: + +```swift +FaceLivenessDetectorView( + sessionID: , + region: , + disableStartView: true, + isPresented: $isPresentingLiveness, + onCompletion: { result in + switch result { + case .success: + // ... + case .failure(let error): + // ... + default: + // ... + } + } +) +``` diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.theming.android.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.theming.android.mdx new file mode 100644 index 00000000000..d76cbf8e9b1 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.theming.android.mdx @@ -0,0 +1,151 @@ +import { TokenItem, TokenPath, TokenMeta } from '@/components/Theming/TokenList'; +import { ColorBlock } from '@/components/Theming/TokenBlocks'; + +## Theming + +The FaceLivenessDetector component supports [Material3 theming](https://developer.android.com/jetpack/compose/designsystems/material3#material-theming), allowing you to theme the FaceLivenessDetector to match the look and feel of your app. To theme the FaceLivenessDetector component according to your app's theme, wrap the FaceLivenessDetector in your app's theme: + +```kotlin +@Composable +fun MyTheme( + content: @Composable () -> Unit +) { + MaterialTheme( + // Override colorScheme with custom colors + colorScheme = LivenessColorScheme.default(), + // Override shapes with custom shapes + shapes = MaterialTheme.shapes, + // Override typography with custom typography + typography = MaterialTheme.typography, + content = content + ) +} + +MyTheme { + FaceLivenessDetector(...) +} +``` + +Amplify UI Liveness provides a Material3 color scheme for light mode and dark mode. + +**Light mode:** + +
    + + + + {"Color(0xFF047D95)"} + + + + + {"Color.White"} + + + + + {"Color.White"} + + + + + {"Color(0xFF0D1926)"} + + + + + {"Color.White"} + + + + + {"Color(0xFF0D1926)"} + + + + + {"Color(0xFF950404)"} + + + + + {"Color.White"} + + + + + {"Color(0xFFF5D9BC)"} + + + + + {"Color(0xFF663300)"} + +
+ +**Dark mode:** + +
    + + + + {"Color(0xFF7DD6E8)"} + + + + + {"Color(0xFF0D1926)"} + + + + + {"Color(0xFF0D1926)"} + + + + + {"Color.White"} + + + + + {"Color(0xFF0D1926)"} + + + + + {"Color.White"} + + + + + {"Color(0xFFEF8F8F)"} + + + + + {"Color(0xFF0D1926)"} + + + + + {"Color(0xFF663300)"} + + + + + {"Color(0xFFEFBF8F)"} + +
+ +To theme the FaceLivenessDetector component using the Amplify UI Liveness color scheme, wrap the FaceLivenessDetector in a MaterialTheme and pass `LivenessColorScheme` for the theme's color scheme: + +```kotlin +MaterialTheme( + colorScheme = LivenessColorScheme.default() +) { + FaceLivenessDetector(...) +} +``` + +If no theme is provided, the FaceLivenessDetector component uses the default MaterialTheme values. diff --git a/docs/src/pages/[platform]/connected-components/liveness/customization/customization.theming.react.mdx b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.theming.react.mdx new file mode 100644 index 00000000000..536a61b9435 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/liveness/customization/customization.theming.react.mdx @@ -0,0 +1,36 @@ +import { Example } from '@/components/Example'; +import { CustomizationTheme } from './CustomizationTheme'; + +## Theming + +### CSS styles + +You can customize the FaceLivenessDetector's default style by using [CSS variables](../../theming/css-variables). + +The example below uses a `