From 467f11424f8e659d908523717f22ad2ff3ee82e7 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 11 Apr 2025 10:30:29 -0500 Subject: [PATCH] look and feel changes --- .github/CODEOWNERS | 3 + .github/ISSUE_TEMPLATE/bug_report.yaml | 98 + .github/ISSUE_TEMPLATE/feature_request.yaml | 32 + .github/PULL_REQUEST_TEMPLATE.md | 16 + .../run_xcodebuild/action.yml | 44 + .../run_xcodebuild_test/action.yml | 76 + .github/workflows/build_liveness.yml | 44 + .github/workflows/dependency-review.yml | 24 + .github/workflows/deploy_liveness.yml | 76 + .github/workflows/deploy_release.yml | 18 + .github/workflows/deploy_unstable.yml | 17 + .github/workflows/feature_request.yml | 23 + .github/workflows/fortify_scan.yml | 74 + .github/workflows/issue_closed.yml | 34 + .github/workflows/issue_comment.yml | 46 + .github/workflows/issue_opened.yml | 53 + .github/workflows/liveness_unit_tests.yml | 31 + .github/workflows/notify_release.yml | 21 + .github/workflows/release_block_manual_pr.yml | 32 + .github/workflows/release_kickoff.yml | 21 + .gitignore | 39 + .idea/.gitignore | 10 + .idea/amplify-ui-swift-liveness.iml | 9 + .idea/codeStyles/Project.xml | 7 + .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/misc.xml | 5 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .../xcshareddata/IDETemplateMacros.plist | 13 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/AmplifyUILiveness.xcscheme | 92 + .../xcschemes/FaceLiveness.xcscheme | 78 + AmplifyUILiveness.podspec | 12 + CHANGELOG.md | 125 + CODE_OF_CONDUCT.md | 4 + CONTRIBUTING.md | 59 + Gemfile | 8 + HostApp/.gitignore | 21 + HostApp/HostApp.xcodeproj/project.pbxproj | 750 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 68 + .../xcshareddata/xcschemes/HostApp.xcscheme | 105 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + HostApp/HostApp/Assets.xcassets/Contents.json | 6 + HostApp/HostApp/HostApp.entitlements | 10 + HostApp/HostApp/HostAppApp.swift | 72 + HostApp/HostApp/Info.plist | 15 + .../HostApp/Model/CreateSessionResponse.swift | 12 + HostApp/HostApp/Model/LivenessResult.swift | 25 + .../Preview Assets.xcassets/Contents.json | 6 + .../Utilities/Color+DynamicColors.swift | 23 + HostApp/HostApp/Utilities/Color+Hex.swift | 15 + HostApp/HostApp/Utilities/UIColor+Hex.swift | 29 + .../HostApp/Utilities/View+Background.swift | 21 + HostApp/HostApp/Views/CredsProvider.swift | 22 + .../HostApp/Views/ExampleLivenessView.swift | 97 + .../Views/ExampleLivenessViewModel.swift | 42 + .../Views/LivenessCheckErrorContentView.swift | 69 + .../LivenessResultContentView+Result.swift | 80 + .../Views/LivenessResultContentView.swift | 113 + .../HostApp/Views/LivenessResultView.swift | 149 + HostApp/HostApp/Views/RootView.swift | 30 + .../StartSessionView+PresentationState.swift | 55 + HostApp/HostApp/Views/StartSessionView.swift | 96 + .../HostApp/Views/StartSessionViewModel.swift | 103 + HostApp/HostAppUITests/HostAppUITests.swift | 41 + .../HostAppUITestsLaunchTests.swift | 32 + HostApp/README.md | 117 + LICENSE | 175 + NOTICE | 1 + Package.resolved | 68 + Package.swift | 35 + README.md | 26 + .../AV/LivenessAVAssetWriter.swift | 20 + .../AV/LivenessAVAssetWriterInput.swift | 27 + .../AV/LivenessCaptureDevice.swift | 39 + .../AV/LivenessCaptureSession.swift | 140 + .../AV/LivenessCaptureSessionError.swift | 18 + .../AV/OutputSampleBufferCapturer.swift | 32 + .../FaceLiveness/AV/VideoChunkProcessor.swift | 12 + .../AV/VideoChunker+AssetWriterDelegate.swift | 40 + .../FaceLiveness/AV/VideoChunker+State.swift | 17 + Sources/FaceLiveness/AV/VideoChunker.swift | 83 + .../BlazeFace/DetectedFace.swift | 93 + .../FaceDetectorShortRange+Model.swift | 214 + .../face_detection_short_range.swift | 300 + .../FaceDetection/FaceDetector.swift | 23 + .../FaceDetection/FaceStateMatching.swift | 108 + .../FaceDetection/Instructor.swift | 67 + .../Resources/Assets.xcassets/Contents.json | 6 + .../Resources/Base.lproj/Localizable.strings | 51 + .../analytics/coremldata.bin | Bin 0 -> 441 bytes .../coremldata.bin | Bin 0 -> 309 bytes .../metadata.json | 86 + .../model.espresso.net | 4828 +++++++++++++++++ .../model.espresso.shape | 1502 +++++ .../model.espresso.weights | Bin 0 -> 517376 bytes .../model/coremldata.bin | Bin 0 -> 200 bytes .../neural_network_optionals/coremldata.bin | Bin 0 -> 40 bytes .../Utilities/Backports/Background.swift | 21 + .../Utilities/Backports/Overlay.swift | 21 + .../Utilities/BoundingBox+Normalize.swift | 20 + .../Utilities/CGImage+Convert.swift | 26 + .../Utilities/Color+DynamicColors.swift | 24 + .../FaceLiveness/Utilities/Color+Hex.swift | 15 + .../Utilities/Color+Liveness.swift | 55 + .../Date+TimestampMilliseconds.swift | 14 + Sources/FaceLiveness/Utilities/Device.swift | 69 + .../Utilities/FinalClientEvent+Init.swift | 37 + .../Utilities/LivenessLocalizedStrings.swift | 121 + .../Utilities/String+Localizable.swift | 27 + .../FaceLiveness/Utilities/UIColor+Hex.swift | 29 + .../FaceLiveness/Utilities/UserAgent.swift | 90 + .../CameraPermissionView.swift | 79 + Sources/FaceLiveness/Views/CloseButton.swift | 36 + .../Freshness/DisplayColor+UIColor.swift | 21 + .../Views/Freshness/Freshness.swift | 168 + .../Views/Freshness/FreshnessView.swift | 49 + ...eraPreviewOutputSampleBufferDelegate.swift | 28 + .../GetReadyPage/CameraPreviewView.swift | 52 + .../GetReadyPage/CameraPreviewViewModel.swift | 65 + .../Views/GetReadyPage/GetReadyPageView.swift | 79 + .../Views/GetReadyPage/ImageFrameView.swift | 35 + .../InstructionContainerView.swift | 111 + .../Views/Instruction/InstructionView.swift | 24 + .../Views/Liveness/CameraView.swift | 34 + .../Liveness/FaceLivenessDetectionError.swift | 138 + .../Liveness/FaceLivenessDetectionView.swift | 284 + ...ViewModel+FaceDetectionResultHandler.swift | 140 + ...ctionViewModel+VideoSegmentProcessor.swift | 21 + .../FaceLivenessDetectionViewModel.swift | 364 ++ .../FaceLivenessViewControllerPresenter.swift | 15 + .../Views/Liveness/LivenessStateMachine.swift | 177 + .../Liveness/LivenessViewController.swift | 176 + .../Liveness/_FaceLivenessDetectionView.swift | 61 + Sources/FaceLiveness/Views/OvalView.swift | 35 + .../FaceLiveness/Views/ProgressBarView.swift | 39 + .../FaceLiveness/Views/RecordingButton.swift | 34 + .../Views/RoundedCornerShape.swift | 28 + Sources/FaceLiveness/Views/WarningBox.swift | 60 + .../CredentialsProviderTestCase.swift | 115 + .../FaceLivenessTests/DetectedFaceTests.swift | 126 + Tests/FaceLivenessTests/LivenessTests.swift | 177 + .../MockAWSCredentials.swift | 14 + .../MockAWSTemporaryCredentials.swift | 16 + .../MockCredentialsProvider.swift | 17 + .../MockFaceDetectionResultHandler.swift | 18 + .../FaceLivenessTests/MockFaceDetector.swift | 27 + .../MockLivenessService.swift | 70 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../FaceLivenessDetectorView+Mock.swift | 45 + .../MockLivenessCaptureSession.swift | 134 + .../IntegrationTestApp/Info.plist | 15 + .../IntegrationTestApp.entitlements | 12 + .../IntegrationTestApp.swift | 71 + .../IntegrationTestApp/Localizable.xcstrings | 5 + .../Model/CreateSessionResponse.swift | 12 + .../Model/LivenessResult.swift | 25 + .../Preview Assets.xcassets/Contents.json | 6 + .../Utilities/Color+DynamicColors.swift | 23 + .../Utilities/Color+Hex.swift | 15 + .../Utilities/UIColor+Hex.swift | 29 + .../Utilities/View+Background.swift | 21 + .../Views/ExampleLivenessView.swift | 81 + .../Views/ExampleLivenessViewModel.swift | 35 + .../Views/LivenessCheckErrorContentView.swift | 59 + .../LivenessResultContentView+Result.swift | 80 + .../Views/LivenessResultContentView.swift | 121 + .../Views/LivenessResultView.swift | 149 + .../IntegrationTestApp/Views/RootView.swift | 30 + .../StartSessionView+PresentationState.swift | 55 + .../Views/StartSessionView.swift | 72 + .../Views/StartSessionViewModel.swift | 69 + .../LivenessIntegrationUITests.swift | 52 + .../UIConstants.swift | 31 + codecov.yml | 15 + fastlane/.gitignore | 1 + fastlane/Fastfile | 95 + fastlane/Pluginfile | 5 + fastlane/README.md | 40 + 184 files changed, 16480 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/composite_actions/run_xcodebuild/action.yml create mode 100644 .github/composite_actions/run_xcodebuild_test/action.yml create mode 100644 .github/workflows/build_liveness.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/deploy_liveness.yml create mode 100644 .github/workflows/deploy_release.yml create mode 100644 .github/workflows/deploy_unstable.yml create mode 100644 .github/workflows/feature_request.yml create mode 100644 .github/workflows/fortify_scan.yml create mode 100644 .github/workflows/issue_closed.yml create mode 100644 .github/workflows/issue_comment.yml create mode 100644 .github/workflows/issue_opened.yml create mode 100644 .github/workflows/liveness_unit_tests.yml create mode 100644 .github/workflows/notify_release.yml create mode 100644 .github/workflows/release_block_manual_pr.yml create mode 100644 .github/workflows/release_kickoff.yml create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/amplify-ui-swift-liveness.iml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist create mode 100644 .swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/AmplifyUILiveness.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/FaceLiveness.xcscheme create mode 100644 AmplifyUILiveness.podspec create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Gemfile create mode 100644 HostApp/.gitignore create mode 100644 HostApp/HostApp.xcodeproj/project.pbxproj create mode 100644 HostApp/HostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 HostApp/HostApp.xcodeproj/xcshareddata/xcschemes/HostApp.xcscheme create mode 100644 HostApp/HostApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 HostApp/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 HostApp/HostApp/Assets.xcassets/Contents.json create mode 100644 HostApp/HostApp/HostApp.entitlements create mode 100644 HostApp/HostApp/HostAppApp.swift create mode 100644 HostApp/HostApp/Info.plist create mode 100644 HostApp/HostApp/Model/CreateSessionResponse.swift create mode 100644 HostApp/HostApp/Model/LivenessResult.swift create mode 100644 HostApp/HostApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 HostApp/HostApp/Utilities/Color+DynamicColors.swift create mode 100644 HostApp/HostApp/Utilities/Color+Hex.swift create mode 100644 HostApp/HostApp/Utilities/UIColor+Hex.swift create mode 100644 HostApp/HostApp/Utilities/View+Background.swift create mode 100644 HostApp/HostApp/Views/CredsProvider.swift create mode 100644 HostApp/HostApp/Views/ExampleLivenessView.swift create mode 100644 HostApp/HostApp/Views/ExampleLivenessViewModel.swift create mode 100644 HostApp/HostApp/Views/LivenessCheckErrorContentView.swift create mode 100644 HostApp/HostApp/Views/LivenessResultContentView+Result.swift create mode 100644 HostApp/HostApp/Views/LivenessResultContentView.swift create mode 100644 HostApp/HostApp/Views/LivenessResultView.swift create mode 100644 HostApp/HostApp/Views/RootView.swift create mode 100644 HostApp/HostApp/Views/StartSessionView+PresentationState.swift create mode 100644 HostApp/HostApp/Views/StartSessionView.swift create mode 100644 HostApp/HostApp/Views/StartSessionViewModel.swift create mode 100644 HostApp/HostAppUITests/HostAppUITests.swift create mode 100644 HostApp/HostAppUITests/HostAppUITestsLaunchTests.swift create mode 100644 HostApp/README.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/FaceLiveness/AV/LivenessAVAssetWriter.swift create mode 100644 Sources/FaceLiveness/AV/LivenessAVAssetWriterInput.swift create mode 100644 Sources/FaceLiveness/AV/LivenessCaptureDevice.swift create mode 100644 Sources/FaceLiveness/AV/LivenessCaptureSession.swift create mode 100644 Sources/FaceLiveness/AV/LivenessCaptureSessionError.swift create mode 100644 Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift create mode 100644 Sources/FaceLiveness/AV/VideoChunkProcessor.swift create mode 100644 Sources/FaceLiveness/AV/VideoChunker+AssetWriterDelegate.swift create mode 100644 Sources/FaceLiveness/AV/VideoChunker+State.swift create mode 100644 Sources/FaceLiveness/AV/VideoChunker.swift create mode 100644 Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift create mode 100644 Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift create mode 100644 Sources/FaceLiveness/FaceDetection/BlazeFace/face_detection_short_range.swift create mode 100644 Sources/FaceLiveness/FaceDetection/FaceDetector.swift create mode 100644 Sources/FaceLiveness/FaceDetection/FaceStateMatching.swift create mode 100644 Sources/FaceLiveness/FaceDetection/Instructor.swift create mode 100644 Sources/FaceLiveness/Resources/Assets.xcassets/Contents.json create mode 100644 Sources/FaceLiveness/Resources/Base.lproj/Localizable.strings create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/analytics/coremldata.bin create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/coremldata.bin create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/metadata.json create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/model.espresso.net create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/model.espresso.shape create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/model.espresso.weights create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/model/coremldata.bin create mode 100644 Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/neural_network_optionals/coremldata.bin create mode 100644 Sources/FaceLiveness/Utilities/Backports/Background.swift create mode 100644 Sources/FaceLiveness/Utilities/Backports/Overlay.swift create mode 100644 Sources/FaceLiveness/Utilities/BoundingBox+Normalize.swift create mode 100644 Sources/FaceLiveness/Utilities/CGImage+Convert.swift create mode 100644 Sources/FaceLiveness/Utilities/Color+DynamicColors.swift create mode 100644 Sources/FaceLiveness/Utilities/Color+Hex.swift create mode 100644 Sources/FaceLiveness/Utilities/Color+Liveness.swift create mode 100644 Sources/FaceLiveness/Utilities/Date+TimestampMilliseconds.swift create mode 100644 Sources/FaceLiveness/Utilities/Device.swift create mode 100644 Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift create mode 100644 Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift create mode 100644 Sources/FaceLiveness/Utilities/String+Localizable.swift create mode 100644 Sources/FaceLiveness/Utilities/UIColor+Hex.swift create mode 100644 Sources/FaceLiveness/Utilities/UserAgent.swift create mode 100644 Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift create mode 100644 Sources/FaceLiveness/Views/CloseButton.swift create mode 100644 Sources/FaceLiveness/Views/Freshness/DisplayColor+UIColor.swift create mode 100644 Sources/FaceLiveness/Views/Freshness/Freshness.swift create mode 100644 Sources/FaceLiveness/Views/Freshness/FreshnessView.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewOutputSampleBufferDelegate.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift create mode 100644 Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift create mode 100644 Sources/FaceLiveness/Views/Instruction/InstructionView.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/CameraView.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+VideoSegmentProcessor.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift create mode 100644 Sources/FaceLiveness/Views/Liveness/_FaceLivenessDetectionView.swift create mode 100644 Sources/FaceLiveness/Views/OvalView.swift create mode 100644 Sources/FaceLiveness/Views/ProgressBarView.swift create mode 100644 Sources/FaceLiveness/Views/RecordingButton.swift create mode 100644 Sources/FaceLiveness/Views/RoundedCornerShape.swift create mode 100644 Sources/FaceLiveness/Views/WarningBox.swift create mode 100644 Tests/FaceLivenessTests/CredentialsProviderTestCase.swift create mode 100644 Tests/FaceLivenessTests/DetectedFaceTests.swift create mode 100644 Tests/FaceLivenessTests/LivenessTests.swift create mode 100644 Tests/FaceLivenessTests/MockAWSCredentials.swift create mode 100644 Tests/FaceLivenessTests/MockAWSTemporaryCredentials.swift create mode 100644 Tests/FaceLivenessTests/MockCredentialsProvider.swift create mode 100644 Tests/FaceLivenessTests/MockFaceDetectionResultHandler.swift create mode 100644 Tests/FaceLivenessTests/MockFaceDetector.swift create mode 100644 Tests/FaceLivenessTests/MockLivenessService.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/Contents.json create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Extension/FaceLivenessDetectorView+Mock.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Info.plist create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.entitlements create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Localizable.xcstrings create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Model/CreateSessionResponse.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Model/LivenessResult.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+DynamicColors.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+Hex.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Utilities/UIColor+Hex.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Utilities/View+Background.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessView.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessViewModel.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessCheckErrorContentView.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultView.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/RootView.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView+PresentationState.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionViewModel.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestAppUITests/LivenessIntegrationUITests.swift create mode 100644 Tests/IntegrationTestApp/IntegrationTestAppUITests/UIConstants.swift create mode 100644 codecov.yml create mode 100644 fastlane/.gitignore create mode 100644 fastlane/Fastfile create mode 100644 fastlane/Pluginfile create mode 100644 fastlane/README.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..67798e4f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# The amplify-ui team is marked as required reviewers for all pull requests to main. +# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +* @aws-amplify/amplify-ios @aws-amplify/amplify-ui diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..5f7e11db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,98 @@ +name: Bug Report +description: Create a report to help us improve +body: + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps To Reproduce + description: How do you trigger this bug? Please walk us through it step by step. + value: | + Steps to reproduce the behavior: + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + render: swift + validations: + required: true + - type: textarea + id: behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: input + id: liveness-version + attributes: + label: Swift Liveness Version + placeholder: e.g. 1.2.4 + validations: + required: true + - type: input + id: xcode + attributes: + label: Xcode version + placeholder: e.g. 14.3.1 (14E300c) -- run `xcodebuild -version` + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: >- + Include any relevant log + value: | +
+ Log Messages + + ``` + INSERT LOG MESSAGES HERE + ``` +
+ render: shell + - type: dropdown + id: regression + attributes: + label: Is this a regression? + multiple: false + options: + - "Yes" + - "No" + validations: + required: true + - type: textarea + id: regression-info + attributes: + label: Regression additional context + placeholder: If it was a regression provide the versions used before and after the upgrade. + - type: input + id: os-version + attributes: + label: OS Version + placeholder: e.g. iOS 15.3 / macOS 11.0 + validations: + required: true + - type: input + id: device + attributes: + label: Device + placeholder: e.g. iPhone6 + validations: + required: true + - type: input + id: simulators + attributes: + label: Specific to simulators + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000..56299900 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,32 @@ +name: Feature request +description: Suggest an idea for this project +body: + - type: textarea + id: description + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..caeb351d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +*Issue #, if available:* + +*Description of changes:* + +*Check points: (check or cross out if not relevant)* + +- [ ] Added new tests to cover change, if needed +- [ ] Build succeeds with all target using Swift Package Manager +- [ ] All unit tests pass +- [ ] Security oriented best practices and standards are followed (e.g. using input sanitization, principle of least privilege, etc) +- [ ] Documentation update for the change if required +- [ ] PR title conforms to conventional commit style +- [ ] If breaking change, documentation/changelog update with migration instructions + + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/composite_actions/run_xcodebuild/action.yml b/.github/composite_actions/run_xcodebuild/action.yml new file mode 100644 index 00000000..e66f40cb --- /dev/null +++ b/.github/composite_actions/run_xcodebuild/action.yml @@ -0,0 +1,44 @@ +name: 'Run xcodebuild' +description: 'Action runs `xcodebuild build` for the scheme specified' + +inputs: + scheme: + required: true + type: string + project_path: + required: false + type: string + xcode_path: + required: false + type: string + destination: + required: false + type: string + default: 'platform=iOS Simulator,name=iPhone 13,OS=latest' + sdk: + required: false + type: string + default: 'iphonesimulator' + other_flags: + required: false + type: string + default: '' + +runs: + using: "composite" + steps: + - name: Build ${{ inputs.scheme }} + env: + SCHEME: ${{ inputs.scheme }} + PROJECT_PATH: ${{ inputs.project_path }} + XCODE_PATH: ${{ inputs.xcode_path }} + run: | + if [ ! -z "$PROJECT_PATH" ]; then + cd $PROJECT_PATH + fi + if [ ! -z "$XCODE_PATH" ]; then + sudo xcode-select -s $XCODE_PATH + fi + xcodebuild -version + xcodebuild build -scheme $SCHEME -sdk '${{ inputs.sdk }}' -destination '${{ inputs.destination }}' ${{ inputs.other_flags }} | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]} + shell: bash \ No newline at end of file diff --git a/.github/composite_actions/run_xcodebuild_test/action.yml b/.github/composite_actions/run_xcodebuild_test/action.yml new file mode 100644 index 00000000..e83fa68e --- /dev/null +++ b/.github/composite_actions/run_xcodebuild_test/action.yml @@ -0,0 +1,76 @@ +name: 'Run xcodebuild test' +description: 'Action runs the test for the scheme specified' + +inputs: + scheme: + required: true + type: string + token: + required: true + project_path: + required: false + type: string + xcode_path: + required: false + type: string + destination: + required: false + type: string + default: 'platform=iOS Simulator,name=iPhone 13,OS=latest' + sdk: + required: false + type: string + default: 'iphonesimulator' + other_flags: + required: false + type: string + default: '' + generate_coverage: + required: false + type: boolean + default: false + +runs: + using: "composite" + steps: + - name: Test ${{ inputs.scheme }} + env: + SCHEME: ${{ inputs.scheme }} + PROJECT_PATH: ${{ inputs.project_path }} + XCODE_PATH: ${{ inputs.xcode_path }} + run: | + if [ ! -z "$PROJECT_PATH" ]; then + cd $PROJECT_PATH + fi + if [ ! -z "$XCODE_PATH" ]; then + echo "Using Xcode $XCODE_PATH" + sudo xcode-select -s $XCODE_PATH + fi + coverageFlags="" + if [ "${{ inputs.generate_coverage }}" == "true" ]; then + echo "Code Coverage is enabled!" + coverageFlags+="-derivedDataPath Build/ -clonedSourcePackagesDirPath "~/Library/Developer/Xcode/DerivedData/$SCHEME" -enableCodeCoverage YES" + fi + xcode-select -p + xcodebuild -version + xcodebuild test -scheme $SCHEME -sdk '${{ inputs.sdk }}' -destination '${{ inputs.destination }}' ${{ inputs.other_flags }} $coverageFlags | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]} + shell: bash + + - name: Generate Coverage report + if: ${{ inputs.generate_coverage == 'true' }} + env: + SCHEME: ${{ inputs.scheme }} + run: | + echo "Generating Coverage report..." + cd Build/Build/ProfileData + cd $(ls -d */|head -n 1) + pathCoverage=Build/Build/ProfileData/${PWD##*/}/Coverage.profdata + cd ${{ github.workspace }} + xcrun llvm-cov export -format="lcov" -instr-profile $pathCoverage Build/Build/Products/Debug-${{ inputs.sdk }}/${SCHEME}Tests.xctest/${SCHEME}Tests > $SCHEME-Coverage.lcov + shell: bash + + - name: Upload Report + if: ${{ inputs.generate_coverage == 'true' }} + uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4.3.0 + with: + token: ${{ inputs.token }} \ No newline at end of file diff --git a/.github/workflows/build_liveness.yml b/.github/workflows/build_liveness.yml new file mode 100644 index 00000000..06463bdf --- /dev/null +++ b/.github/workflows/build_liveness.yml @@ -0,0 +1,44 @@ +name: Build | Amplify UI Swift Liveness +on: + workflow_call: + inputs: + identifier: + required: true + type: string + workflow_dispatch: + push: + branches-ignore: + - main + - release + +permissions: + contents: read + +concurrency: + group: ${{ inputs.identifier || github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref_name != 'main'}} + +jobs: + build-amplify-ui-swift-liveness: + runs-on: macos-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 #v3.5.3 + with: + persist-credentials: false + - name: Build Amplify Swift Liveness UI + uses: ./.github/composite_actions/run_xcodebuild + with: + scheme: FaceLiveness + destination: 'platform=iOS Simulator,name=iPhone 15,OS=latest' + + confirm-pass: + runs-on: ubuntu-latest + name: Confirm Passing Build Steps + if: ${{ !cancelled() }} + needs: [ build-amplify-ui-swift-liveness ] + env: + EXIT_CODE: ${{ contains(needs.*.result, 'failure') && 1 || 0 }} + steps: + - run: exit $EXIT_CODE + diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..27ebe3b7 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,24 @@ +name: Dependency Review + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + persist-credentials: false + + - name: Dependency Review + uses: actions/dependency-review-action@7d90b4f05fea31dde1c4a1fb3fa787e197ea93ab # v3.0.7 + with: + config-file: aws-amplify/amplify-ci-support/.github/dependency-review-config.yml@main diff --git a/.github/workflows/deploy_liveness.yml b/.github/workflows/deploy_liveness.yml new file mode 100644 index 00000000..3c2a90ce --- /dev/null +++ b/.github/workflows/deploy_liveness.yml @@ -0,0 +1,76 @@ +name: Deploy Liveness +on: + workflow_call: + inputs: + type: + description: 'The type of deployment. Valid values are unstable (default) and release' + default: 'unstable' + required: false + type: string + +permissions: + id-token: write + contents: write + actions: write + +jobs: + build-amplify-ui-swift-liveness: + name: Build Amplify package + uses: ./.github/workflows/build_liveness.yml + with: + identifier: 'workflow-call-build-liveness' + + unit-tests: + name: Run Unit Tests + uses: ./.github/workflows/liveness_unit_tests.yml + with: + identifier: 'workflow-call-unit-test' + + fortify: + name: Run Fortify Scan + uses: ./.github/workflows/fortify_scan.yml + secrets: inherit + with: + identifier: 'workflow-call-fortify' + + release: + environment: Release + name: Release new ${{ inputs.type }} version + needs: [unit-tests, fortify, build-amplify-ui-swift-liveness] + runs-on: macos-latest + env: + GITHUB_EMAIL: aws-amplify-ops@amazon.com + GITHUB_USER: aws-amplify-ops + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@8c3f20df09ac63af7b3ae3d7c91f105f857d8497 #v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + role-session-name: ${{ format('{0}.release', github.run_id) }} + aws-region: ${{ secrets.AWS_REGION }} + mask-aws-account-id: true + + - id: retrieve-token + name: Retrieve Token + env: + DEPLOY_SECRET_ARN: ${{ secrets.DEPLOY_SECRET_ARN }} + run: | + PAT=$(aws secretsmanager get-secret-value \ + --secret-id "$DEPLOY_SECRET_ARN" \ + | jq -r ".SecretString | fromjson | .Credential") + echo "token=$PAT" >> $GITHUB_OUTPUT + + - name: Checkout repo + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + with: + fetch-depth: 10 + token: ${{steps.retrieve-token.outputs.token}} + + - name: Setup Ruby + uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0 + with: + ruby-version: '3.2.1' + bundler-cache: true + + - name: Release Package + run: bundle exec fastlane ${{ inputs.type }} diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml new file mode 100644 index 00000000..d5e8326c --- /dev/null +++ b/.github/workflows/deploy_release.yml @@ -0,0 +1,18 @@ +name: Build, Test and Release | Stable version +on: + push: + branches: + release + +permissions: + id-token: write + contents: write + actions: write + +jobs: + release-stable: + uses: ./.github/workflows/deploy_liveness.yml + with: + type: release + secrets: inherit + diff --git a/.github/workflows/deploy_unstable.yml b/.github/workflows/deploy_unstable.yml new file mode 100644 index 00000000..2c967ad5 --- /dev/null +++ b/.github/workflows/deploy_unstable.yml @@ -0,0 +1,17 @@ +name: Build, Test and Release | Unstable version +on: + push: + branches: + main + +permissions: + id-token: write + contents: write + actions: write + +jobs: + release-unstable: + uses: ./.github/workflows/deploy_liveness.yml + with: + type: unstable + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/feature_request.yml b/.github/workflows/feature_request.yml new file mode 100644 index 00000000..2a0c5bb6 --- /dev/null +++ b/.github/workflows/feature_request.yml @@ -0,0 +1,23 @@ +name: Feature request label added. + +on: + issues: + types: [labeled] + +permissions: + issues: write + +jobs: + project: + name: Feature request message + runs-on: ubuntu-latest + if: ${{ github.event.label.name == 'feature-request' }} + + steps: + - name: add feature request comment + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + gh issue comment $ISSUE_NUMBER --repo aws-amplify/amplify-ui-swift-liveness -b "This has been identified as a feature request. If this feature is important to you, we strongly encourage you to give a 👍 reaction on the request. This helps us prioritize new features most important to you. Thank you!" \ No newline at end of file diff --git a/.github/workflows/fortify_scan.yml b/.github/workflows/fortify_scan.yml new file mode 100644 index 00000000..bccf9687 --- /dev/null +++ b/.github/workflows/fortify_scan.yml @@ -0,0 +1,74 @@ +name: Fortify Scan +on: + workflow_dispatch: + workflow_call: + inputs: + identifier: + required: true + type: string + push: + branches-ignore: + - main + - release + +permissions: + id-token: write + contents: read + +concurrency: + group: ${{ inputs.identifier || github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref_name != 'main'}} + +jobs: + fortify-scan: + runs-on: macos-latest + environment: Fortify + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 #v3.5.3 + with: + persist-credentials: false + + - name: Configure AWS credentials for fetching fortify resources + uses: aws-actions/configure-aws-credentials@8c3f20df09ac63af7b3ae3d7c91f105f857d8497 #v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + role-session-name: GHAFortifySession + role-duration-seconds: 900 + mask-aws-account-id: true + + - name: Download License + run: | + aws s3 cp s3://${{ secrets.AWS_S3_FORTIFY_BUCKET }}${{ vars.LICENSE_PATH }} fortify.license + + - name: Download Installer + run: | + aws s3 cp s3://${{ secrets.AWS_S3_FORTIFY_BUCKET }}${{ vars.INSTALLER_PATH }} Fortify_SCA_and_Apps_22.1.1_Mac.tar.gz + tar -xvf Fortify_SCA_and_Apps_22.1.1_Mac.tar.gz + unzip Fortify_SCA_and_Apps_22.1.1_osx_x64.app.zip + + - name: Download Scripts + run: | + aws s3 cp s3://${{ secrets.AWS_S3_FORTIFY_BUCKET }}${{ vars.SCRIPTS_PATH }} liveness_swift_fortify_scan.sh + + - name: Run Installer + run: | + Fortify_SCA_and_Apps_22.1.1_osx_x64.app/Contents/MacOS/installbuilder.sh --mode unattended --installdir ~/amplify-ui-swift-liveness/Fortify --InstallSamples 0 --fortify_license_path fortify.license --MigrateSCA 0 + export PATH=~/amplify-ui-swift-liveness/Fortify/bin:$PATH + fortifyupdate -acceptKey + sourceanalyzer -version + + - name: Run Scan + run: | + export PATH=~/amplify-ui-swift-liveness/Fortify/bin:$PATH + sh ./liveness_swift_fortify_scan.sh Sources + + confirm-pass: + runs-on: ubuntu-latest + name: Confirm Passing Fortify Scan + if: ${{ !cancelled() }} + needs: [ fortify-scan ] + env: + EXIT_CODE: ${{ contains(needs.*.result, 'failure') && 1 || 0 }} + steps: + - run: exit $EXIT_CODE \ No newline at end of file diff --git a/.github/workflows/issue_closed.yml b/.github/workflows/issue_closed.yml new file mode 100644 index 00000000..d36ceb8d --- /dev/null +++ b/.github/workflows/issue_closed.yml @@ -0,0 +1,34 @@ +name: Issue Closed + +on: + issues: + types: [closed] + +permissions: + issues: write + +jobs: + cleanup-labels: + runs-on: ubuntu-latest + if: ${{ contains(github.event.issue.labels.*.name, 'pending-community-response') || contains(github.event.issue.labels.*.name, 'pending-maintainer-response') || contains(github.event.issue.labels.*.name, 'closing soon') || contains(github.event.issue.labels.*.name, 'pending-release') || contains(github.event.issue.labels.*.name, 'pending-triage') }} + steps: + - name: Remove unnecessary labels after closing + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "closing soon" --remove-label "pending-community-response" --remove-label "pending-maintainer-response" --remove-label "pending-release" --remove-label "pending-triage" + + comment-visibility-warning: + runs-on: ubuntu-latest + steps: + - uses: aws-actions/closed-issue-message@v1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + message: | + ### ⚠️COMMENT VISIBILITY WARNING⚠️ + Comments on closed issues are hard for our team to see. + If you need more assistance, please open a new issue that references this one. + If you wish to keep having a conversation with other community members under this issue feel free to do so. \ No newline at end of file diff --git a/.github/workflows/issue_comment.yml b/.github/workflows/issue_comment.yml new file mode 100644 index 00000000..20a8339a --- /dev/null +++ b/.github/workflows/issue_comment.yml @@ -0,0 +1,46 @@ +name: Issue Comment + +on: + issue_comment: + types: [created] + +jobs: + notify: + runs-on: ubuntu-latest + permissions: {} + if: ${{ !github.event.issue.pull_request && !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} + steps: + - name: Run webhook curl command + env: + WEBHOOK_URL: ${{ secrets.SLACK_COMMENT_WEBHOOK_URL }} + COMMENT: ${{toJson(github.event.comment.body)}} + USER: ${{github.event.comment.user.login}} + COMMENT_URL: ${{github.event.comment.html_url}} + shell: bash + run: echo $COMMENT | sed "s/\\\n/. /g; s/\\\r//g; s/[^a-zA-Z0-9 &().,:]//g" | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"comment":"{}", "commentUrl":"'$COMMENT_URL'", "user":"'$USER'"}' + + adjust-labels: + runs-on: ubuntu-latest + if: ${{ github.event.issue.state == 'open' }} + permissions: + issues: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} + steps: + - name: remove pending-community-response when new comment received + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && !github.event.issue.pull_request }} + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "pending-community-response" + - name: add pending-maintainer-response when new community comment received + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-maintainer-response" + - name: remove pending-maintainer-response when new owner/member comment received + if: ${{ contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "pending-maintainer-response" \ No newline at end of file diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml new file mode 100644 index 00000000..0478b683 --- /dev/null +++ b/.github/workflows/issue_opened.yml @@ -0,0 +1,53 @@ +name: Issue Opened +on: + issues: + types: [opened] + +jobs: + notify: + runs-on: ubuntu-latest + permissions: {} + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.issue.author_association) }} + steps: + - name: Run webhook curl command + env: + WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} + ISSUE: ${{toJson(github.event.issue.title)}} + ISSUE_URL: ${{github.event.issue.html_url}} + USER: ${{github.event.issue.user.login}} + shell: bash + run: echo $ISSUE | sed 's/[^a-zA-Z0-9 &().,:]//g' | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"issue":"{}", "issueUrl":"'$ISSUE_URL'", "user":"'$USER'"}' + + add-issue-opened-labels: + runs-on: ubuntu-latest + permissions: + issues: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} + steps: + - name: Add the pending-triage label + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-triage" + - name: Add the pending-maintainer-response label + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.issue.author_association) }} + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-maintainer-response" + + maintainer-opened: + runs-on: ubuntu-latest + permissions: + issues: write + if: ${{ contains(fromJSON('["MEMBER", "OWNER"]'), github.event.issue.author_association) }} + steps: + - name: Post comment if maintainer opened. + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} + run: | + gh issue comment $ISSUE_NUMBER --repo $REPOSITORY_NAME -b "This issue was opened by a maintainer of this repository; updates will be posted here. If you are also experiencing this issue, please comment here with any relevant information so that we're aware and can prioritize accordingly." \ No newline at end of file diff --git a/.github/workflows/liveness_unit_tests.yml b/.github/workflows/liveness_unit_tests.yml new file mode 100644 index 00000000..b6b62af0 --- /dev/null +++ b/.github/workflows/liveness_unit_tests.yml @@ -0,0 +1,31 @@ +name: Run Unit Tests | Amplify UI Swift Liveness + +on: + workflow_dispatch: + workflow_call: + inputs: + identifier: + required: true + type: string + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-iOS: + name: Liveness iOS Unit Tests + runs-on: macos-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + with: + persist-credentials: false + - name: Test FaceLiveness + continue-on-error: false + uses: ./.github/composite_actions/run_xcodebuild_test + with: + scheme: FaceLiveness + token: ${{ secrets.CODECOV_TOKEN }} + destination: 'platform=iOS Simulator,name=iPhone 15,OS=latest' + generate_coverage: true \ No newline at end of file diff --git a/.github/workflows/notify_release.yml b/.github/workflows/notify_release.yml new file mode 100644 index 00000000..45a95e98 --- /dev/null +++ b/.github/workflows/notify_release.yml @@ -0,0 +1,21 @@ +name: Notify Amplify UI Swift Liveness Release + +on: + release: + types: [released] + +permissions: {} + +jobs: + notify: + runs-on: ubuntu-latest + + steps: + - name: Run webhook curl command + env: + WEBHOOK_URL: ${{ secrets.SLACK_RELEASE_WEBHOOK_URL }} + VERSION: ${{github.event.release.html_url}} + REPO_NAME: ${{github.event.repository.name}} + ACTION_NAME: ${{github.event.action}} + shell: bash + run: echo $VERSION | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"action":"'$ACTION_NAME'", "repo":"'$REPO_NAME'", "version":"{}"}' \ No newline at end of file diff --git a/.github/workflows/release_block_manual_pr.yml b/.github/workflows/release_block_manual_pr.yml new file mode 100644 index 00000000..b9665b14 --- /dev/null +++ b/.github/workflows/release_block_manual_pr.yml @@ -0,0 +1,32 @@ +# Blocks PRs targeting the release branch that are not created by the release GHA workflow. +# +# This works because workflows cannot trigger other workflows unless they call them directly. +# As a result, this workflow will only run with a PR targeting release is manually created. +# +# https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow + +name: Block Manual PR to Release + +on: + pull_request: + branches: + - release + +permissions: + pull-requests: write + +jobs: + check: + name: Block Manual PR to Release + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Close PR + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + gh pr close $PR_NUMBER --comment "Invalid PR! PRs targeting the release branch must be created by the GHA release workflow." diff --git a/.github/workflows/release_kickoff.yml b/.github/workflows/release_kickoff.yml new file mode 100644 index 00000000..bff6b4c1 --- /dev/null +++ b/.github/workflows/release_kickoff.yml @@ -0,0 +1,21 @@ +# Creates a PR to push main to release branch to kick-off the release workflow +name: Release Amplify UI Swift Liveness + +on: + workflow_dispatch: + +permissions: + pull-requests: write + +jobs: + release: + name: Release + runs-on: macos-latest + + steps: + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Create PR to push main to release branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: "gh pr create --title 'chore: kickoff release' --body 'kickoff release' --head main --base release" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f9d33fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +.DS_Store +/.build +/.index-build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +HostApp/amplify/ +HostApp/build/ +HostApp/dist/ +HostApp/node_modules/ +HostApp/generated-src/ +HostApp/aws-exports.js +HostApp/awsconfiguration.json +HostApp/amplifyconfiguration.json +HostApp/amplifyconfiguration.dart +HostApp/amplify-build-config.json +HostApp/amplify-gradle-config.json +HostApp/amplifytools.xcconfig +HostApp/.secret-* +HostApp/**.sample +Tests/IntegrationTestApp/amplify/ +Tests/IntegrationTestApp/build/ +Tests/IntegrationTestApp/dist/ +Tests/IntegrationTestApp/node_modules/ +Tests/IntegrationTestApp/generated-src/ +Tests/IntegrationTestApp/aws-exports.js +Tests/IntegrationTestApp/awsconfiguration.json +Tests/IntegrationTestApp/amplifyconfiguration.json +Tests/IntegrationTestApp/amplifyconfiguration.dart +Tests/IntegrationTestApp/amplify-build-config.json +Tests/IntegrationTestApp/amplify-gradle-config.json +Tests/IntegrationTestApp/amplifytools.xcconfig +Tests/IntegrationTestApp/.secret-* +Tests/IntegrationTestApp/**.sample +Tests/IntegrationTestApp/*.xcodeproj \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..0a8642fa --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Zeppelin ignored files +/ZeppelinRemoteNotebooks/ diff --git a/.idea/amplify-ui-swift-liveness.iml b/.idea/amplify-ui-swift-liveness.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/amplify-ui-swift-liveness.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..919ce1f1 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..1ec64b08 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..b1003417 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 00000000..83c40ebb --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,13 @@ + + + + + FILEHEADER + +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + \ No newline at end of file diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AmplifyUILiveness.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AmplifyUILiveness.xcscheme new file mode 100644 index 00000000..c84dd659 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AmplifyUILiveness.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/FaceLiveness.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/FaceLiveness.xcscheme new file mode 100644 index 00000000..44071885 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/FaceLiveness.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyUILiveness.podspec b/AmplifyUILiveness.podspec new file mode 100644 index 00000000..09e78d6a --- /dev/null +++ b/AmplifyUILiveness.podspec @@ -0,0 +1,12 @@ +Pod::Spec.new do |s| + s.name = 'AmplifyUILiveness' + s.version = '1.0.0' + s.summary = 'AWS Amplify UI Liveness module' + s.homepage = 'https://github.com/aws-amplify/amplify-ui-swift-liveness' + s.license = { :type => 'Apache 2.0', :file => 'LICENSE' } + s.author = { 'AWS Amplify' => 'aws-amplify@amazon.com' } + s.source = { :git => 'https://github.com/aws-amplify/amplify-ui-swift-liveness.git', :branch => 'main' } + s.ios.deployment_target = '13.0' + s.source_files = 'Sources/**/*.{swift,h,m}' + s.swift_version = '5.0' +end \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..82e486c6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,125 @@ +# Changelog + +## 1.3.4 (2025-01-13) + +## 1.3.3 (2024-09-24) + +## 1.3.2 (2024-08-20) + +## 1.3.1 (2024-08-19) + +### Bug Fixes + +- Returning .userCancelled when the app goes to the background during a Liveness check. (#167) + +## 1.3.0 (2024-08-06) + +### Features + +- Adding new error when the camera is not available even though permissions were granted. (#163) + +## 1.2.18 (2024-07-29) + +### Bug Fixes + +- Fixing a crash when attempting to call finishWriting (#161) + +## 1.2.17 (2024-07-11) + +### Bug Fixes + +- Updating the camera frame position when the subviews are laid out (#158) + +## 1.2.16 (2024-07-02) + +### Bug Fixes + +- Fixing video not being mirrored in the 'Get Ready' screen (#153) + +## 1.2.15 (2024-06-26) + +## 1.2.14 (2024-05-15) + +## 1.2.13 (2024-05-06) + +## 1.2.12 (2024-04-25) + +### Bug Fixes + +- remove strong reference retain cycle from camera preview view model (#135) + +## 1.2.11 (2024-04-23) + +## 1.2.10 (2024-04-18) + +## 1.2.9 (2024-04-11) + +### Bug Fixes + +- prevent AVCaptureSession from starting during session configuration (#124) + +## 1.2.8 (2024-04-09) + +### Bug Fixes + +- update preview layer to be optional and check for nil values (#122) + +## 1.2.7 (2024-03-18) + +### Bug Fixes + +- use higher priority task queue for AVCaptureSession (#118) + +## 1.2.6 (2024-03-11) + +## 1.2.5 (2024-02-08) + +### Bug Fixes + +- add additional guard for finishing AVAssetWriter (#108) + +## 1.2.4 (2024-02-05) + +### Bug Fixes + +- ensure video image is mirrored (#105) + +## 1.2.3 (2024-02-05) + +### Bug Fixes + +- cleanup camera session in the preview page (#101) + +## 1.2.2 (2024-01-10) + +### Bug Fixes + +- resolve race condition when starting AVCaptureSession (#93) + +## 1.2.1 (2023-12-05) + +### Bug Fixes + +- send the final video event at a delayed time interval (#87) + +## 1.2.0 (2023-12-04) + +### Features + +- **ux**: update get ready page with new preview screen (#78) + +## 1.1.4 (2023-10-31) + +### Bug Fixes + +- prevent duplicate session timeout error messages (#70) + +## 1.1.3 (2023-10-23) + +### Bug Fixes + +- update session timed out error handling (#65) + +## 1.1.2 (2023-10-05) + + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5b627cfa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c4b6a1c5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,59 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *main* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..00c7ad01 --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +# Gemfile + +source 'https://rubygems.org' + +gem 'xcpretty', '0.3.0' +gem 'fastlane', '2.205.1' +eval_gemfile('fastlane/Pluginfile') + diff --git a/HostApp/.gitignore b/HostApp/.gitignore new file mode 100644 index 00000000..f54d96e8 --- /dev/null +++ b/HostApp/.gitignore @@ -0,0 +1,21 @@ +#amplify-do-not-edit-begin +amplify/\#current-cloud-backend +amplify/.config/local-* +amplify/logs +amplify/mock-data +amplify/mock-api-resources +amplify/backend/amplify-meta.json +amplify/backend/.temp +build/ +dist/ +node_modules/ +aws-exports.js +awsconfiguration.json +amplifyconfiguration.json +amplifyconfiguration.dart +amplify-build-config.json +amplify-gradle-config.json +amplifytools.xcconfig +.secret-* +**.sample +#amplify-do-not-edit-end \ No newline at end of file diff --git a/HostApp/HostApp.xcodeproj/project.pbxproj b/HostApp/HostApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..b39fb77e --- /dev/null +++ b/HostApp/HostApp.xcodeproj/project.pbxproj @@ -0,0 +1,750 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 87BCB9652D67C0A40055559D /* CredsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BCB9642D67C09D0055559D /* CredsProvider.swift */; }; + 90236C77299D6D41009FD1A7 /* HostAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90236C76299D6D40009FD1A7 /* HostAppApp.swift */; }; + 90493F822992D64000CFE674 /* LivenessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90493F812992D64000CFE674 /* LivenessResult.swift */; }; + 904CC73D2996E650002E0753 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 904CC73C2996E650002E0753 /* RootView.swift */; }; + 906AB82229E9F432007FFC81 /* View+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906AB82129E9F432007FFC81 /* View+Background.swift */; }; + 906AB82429E9F48C007FFC81 /* StartSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906AB82329E9F48C007FFC81 /* StartSessionView.swift */; }; + 906AB82629E9F554007FFC81 /* Color+DynamicColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906AB82529E9F554007FFC81 /* Color+DynamicColors.swift */; }; + 906AB82829EA04B5007FFC81 /* ExampleLivenessViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906AB82729EA04B5007FFC81 /* ExampleLivenessViewModel.swift */; }; + 906AB82A29EA0927007FFC81 /* StartSessionView+PresentationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906AB82929EA0927007FFC81 /* StartSessionView+PresentationState.swift */; }; + 906AB82C29EA0BFD007FFC81 /* StartSessionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906AB82B29EA0BFD007FFC81 /* StartSessionViewModel.swift */; }; + 906AB83029EA0DC5007FFC81 /* LivenessResultContentView+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906AB82F29EA0DC5007FFC81 /* LivenessResultContentView+Result.swift */; }; + 9070FFA8285112B5009867D5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9070FFA7285112B5009867D5 /* Assets.xcassets */; }; + 9070FFAB285112B5009867D5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9070FFAA285112B5009867D5 /* Preview Assets.xcassets */; }; + 9070FFBF285112B5009867D5 /* HostAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9070FFBE285112B5009867D5 /* HostAppUITests.swift */; }; + 9070FFC1285112B5009867D5 /* HostAppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9070FFC0285112B5009867D5 /* HostAppUITestsLaunchTests.swift */; }; + 909308C5297DC49C00F3CC6E /* LivenessCheckErrorContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909308C2297DC49C00F3CC6E /* LivenessCheckErrorContentView.swift */; }; + 909308C6297DC49C00F3CC6E /* LivenessResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909308C3297DC49C00F3CC6E /* LivenessResultView.swift */; }; + 909308C7297DC49C00F3CC6E /* LivenessResultContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909308C4297DC49C00F3CC6E /* LivenessResultContentView.swift */; }; + 909308CC297DC4E700F3CC6E /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909308CA297DC4E700F3CC6E /* Color+Hex.swift */; }; + 909308CD297DC4E700F3CC6E /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909308CB297DC4E700F3CC6E /* UIColor+Hex.swift */; }; + 909308D1297EE67100F3CC6E /* ExampleLivenessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909308D0297EE67100F3CC6E /* ExampleLivenessView.swift */; }; + 90FDF2A5299BDF3E0002CE7D /* CreateSessionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90FDF2A4299BDF3E0002CE7D /* CreateSessionResponse.swift */; }; + 973619222BA378200003A590 /* FaceLiveness in Frameworks */ = {isa = PBXBuildFile; productRef = 973619212BA378200003A590 /* FaceLiveness */; }; + 973619252BA378690003A590 /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 973619232BA378690003A590 /* amplifyconfiguration.json */; }; + 973619262BA378690003A590 /* awsconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 973619242BA378690003A590 /* awsconfiguration.json */; }; + 97D1A8E92BA3757700FF1368 /* AWSAPIPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 97D1A8E82BA3757700FF1368 /* AWSAPIPlugin */; }; + 97D1A8EB2BA3757700FF1368 /* AWSCognitoAuthPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 97D1A8EA2BA3757700FF1368 /* AWSCognitoAuthPlugin */; }; + 97D1A8ED2BA3757700FF1368 /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = 97D1A8EC2BA3757700FF1368 /* Amplify */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 9070FFB1285112B5009867D5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9070FF98285112B4009867D5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9070FF9F285112B4009867D5; + remoteInfo = HostApp; + }; + 9070FFBB285112B5009867D5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9070FF98285112B4009867D5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9070FF9F285112B4009867D5; + remoteInfo = HostApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 87BCB9642D67C09D0055559D /* CredsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredsProvider.swift; sourceTree = ""; }; + 900129D4298ACA9100AE3524 /* HostApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HostApp.entitlements; sourceTree = ""; }; + 90236C76299D6D40009FD1A7 /* HostAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostAppApp.swift; sourceTree = ""; }; + 90493F812992D64000CFE674 /* LivenessResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessResult.swift; sourceTree = ""; }; + 90493FB0299577FE00CFE674 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 904CC73C2996E650002E0753 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; + 906AB82129E9F432007FFC81 /* View+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Background.swift"; sourceTree = ""; }; + 906AB82329E9F48C007FFC81 /* StartSessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartSessionView.swift; sourceTree = ""; }; + 906AB82529E9F554007FFC81 /* Color+DynamicColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+DynamicColors.swift"; sourceTree = ""; }; + 906AB82729EA04B5007FFC81 /* ExampleLivenessViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleLivenessViewModel.swift; sourceTree = ""; }; + 906AB82929EA0927007FFC81 /* StartSessionView+PresentationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StartSessionView+PresentationState.swift"; sourceTree = ""; }; + 906AB82B29EA0BFD007FFC81 /* StartSessionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartSessionViewModel.swift; sourceTree = ""; }; + 906AB82F29EA0DC5007FFC81 /* LivenessResultContentView+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LivenessResultContentView+Result.swift"; sourceTree = ""; }; + 9070FFA0285112B4009867D5 /* HostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9070FFA7285112B5009867D5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9070FFAA285112B5009867D5 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 9070FFB0285112B5009867D5 /* HostAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HostAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9070FFBA285112B5009867D5 /* HostAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HostAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9070FFBE285112B5009867D5 /* HostAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostAppUITests.swift; sourceTree = ""; }; + 9070FFC0285112B5009867D5 /* HostAppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostAppUITestsLaunchTests.swift; sourceTree = ""; }; + 909308C2297DC49C00F3CC6E /* LivenessCheckErrorContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessCheckErrorContentView.swift; sourceTree = ""; }; + 909308C3297DC49C00F3CC6E /* LivenessResultView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessResultView.swift; sourceTree = ""; }; + 909308C4297DC49C00F3CC6E /* LivenessResultContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessResultContentView.swift; sourceTree = ""; }; + 909308CA297DC4E700F3CC6E /* Color+Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = ""; }; + 909308CB297DC4E700F3CC6E /* UIColor+Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; + 909308D0297EE67100F3CC6E /* ExampleLivenessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleLivenessView.swift; sourceTree = ""; }; + 90FDF2A4299BDF3E0002CE7D /* CreateSessionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSessionResponse.swift; sourceTree = ""; }; + 973619232BA378690003A590 /* amplifyconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = amplifyconfiguration.json; sourceTree = ""; }; + 973619242BA378690003A590 /* awsconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = awsconfiguration.json; sourceTree = ""; }; + 97D1A8EE2BA375AA00FF1368 /* amplify-ui-swift-liveness */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "amplify-ui-swift-liveness"; path = ..; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 9070FF9D285112B4009867D5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 973619222BA378200003A590 /* FaceLiveness in Frameworks */, + 97D1A8ED2BA3757700FF1368 /* Amplify in Frameworks */, + 97D1A8E92BA3757700FF1368 /* AWSAPIPlugin in Frameworks */, + 97D1A8EB2BA3757700FF1368 /* AWSCognitoAuthPlugin in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9070FFAD285112B5009867D5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9070FFB7285112B5009867D5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 90215EED291E9FB60050F2AD /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 9070FF97285112B4009867D5 = { + isa = PBXGroup; + children = ( + 973619232BA378690003A590 /* amplifyconfiguration.json */, + 973619242BA378690003A590 /* awsconfiguration.json */, + 97D1A8EE2BA375AA00FF1368 /* amplify-ui-swift-liveness */, + 9070FFA2285112B4009867D5 /* HostApp */, + 9070FFBD285112B5009867D5 /* HostAppUITests */, + 9070FFA1285112B4009867D5 /* Products */, + 90215EED291E9FB60050F2AD /* Frameworks */, + A5A9AF5054D0FF13505B212A /* AmplifyConfig */, + ); + sourceTree = ""; + }; + 9070FFA1285112B4009867D5 /* Products */ = { + isa = PBXGroup; + children = ( + 9070FFA0285112B4009867D5 /* HostApp.app */, + 9070FFB0285112B5009867D5 /* HostAppTests.xctest */, + 9070FFBA285112B5009867D5 /* HostAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 9070FFA2285112B4009867D5 /* HostApp */ = { + isa = PBXGroup; + children = ( + 90FDF2A3299BDF340002CE7D /* Model */, + 90493FB0299577FE00CFE674 /* Info.plist */, + 900129D4298ACA9100AE3524 /* HostApp.entitlements */, + 909308C9297DC4D500F3CC6E /* Utilities */, + 909308C8297DC4CA00F3CC6E /* Views */, + 90236C76299D6D40009FD1A7 /* HostAppApp.swift */, + 9070FFA7285112B5009867D5 /* Assets.xcassets */, + 9070FFA9285112B5009867D5 /* Preview Content */, + ); + path = HostApp; + sourceTree = ""; + }; + 9070FFA9285112B5009867D5 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 9070FFAA285112B5009867D5 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 9070FFBD285112B5009867D5 /* HostAppUITests */ = { + isa = PBXGroup; + children = ( + 9070FFBE285112B5009867D5 /* HostAppUITests.swift */, + 9070FFC0285112B5009867D5 /* HostAppUITestsLaunchTests.swift */, + ); + path = HostAppUITests; + sourceTree = ""; + }; + 909308C8297DC4CA00F3CC6E /* Views */ = { + isa = PBXGroup; + children = ( + 87BCB9642D67C09D0055559D /* CredsProvider.swift */, + 904CC73C2996E650002E0753 /* RootView.swift */, + 906AB82329E9F48C007FFC81 /* StartSessionView.swift */, + 906AB82B29EA0BFD007FFC81 /* StartSessionViewModel.swift */, + 906AB82929EA0927007FFC81 /* StartSessionView+PresentationState.swift */, + 909308D0297EE67100F3CC6E /* ExampleLivenessView.swift */, + 906AB82729EA04B5007FFC81 /* ExampleLivenessViewModel.swift */, + 909308C2297DC49C00F3CC6E /* LivenessCheckErrorContentView.swift */, + 909308C4297DC49C00F3CC6E /* LivenessResultContentView.swift */, + 906AB82F29EA0DC5007FFC81 /* LivenessResultContentView+Result.swift */, + 909308C3297DC49C00F3CC6E /* LivenessResultView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 909308C9297DC4D500F3CC6E /* Utilities */ = { + isa = PBXGroup; + children = ( + 909308CA297DC4E700F3CC6E /* Color+Hex.swift */, + 909308CB297DC4E700F3CC6E /* UIColor+Hex.swift */, + 906AB82129E9F432007FFC81 /* View+Background.swift */, + 906AB82529E9F554007FFC81 /* Color+DynamicColors.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 90FDF2A3299BDF340002CE7D /* Model */ = { + isa = PBXGroup; + children = ( + 90493F812992D64000CFE674 /* LivenessResult.swift */, + 90FDF2A4299BDF3E0002CE7D /* CreateSessionResponse.swift */, + ); + path = Model; + sourceTree = ""; + }; + A5A9AF5054D0FF13505B212A /* AmplifyConfig */ = { + isa = PBXGroup; + children = ( + ); + name = AmplifyConfig; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 9070FF9F285112B4009867D5 /* HostApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9070FFC4285112B5009867D5 /* Build configuration list for PBXNativeTarget "HostApp" */; + buildPhases = ( + 9070FF9C285112B4009867D5 /* Sources */, + 9070FF9D285112B4009867D5 /* Frameworks */, + 9070FF9E285112B4009867D5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = HostApp; + packageProductDependencies = ( + 97D1A8E82BA3757700FF1368 /* AWSAPIPlugin */, + 97D1A8EA2BA3757700FF1368 /* AWSCognitoAuthPlugin */, + 97D1A8EC2BA3757700FF1368 /* Amplify */, + 973619212BA378200003A590 /* FaceLiveness */, + ); + productName = HostApp; + productReference = 9070FFA0285112B4009867D5 /* HostApp.app */; + productType = "com.apple.product-type.application"; + }; + 9070FFAF285112B5009867D5 /* HostAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9070FFC7285112B5009867D5 /* Build configuration list for PBXNativeTarget "HostAppTests" */; + buildPhases = ( + 9070FFAC285112B5009867D5 /* Sources */, + 9070FFAD285112B5009867D5 /* Frameworks */, + 9070FFAE285112B5009867D5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 9070FFB2285112B5009867D5 /* PBXTargetDependency */, + ); + name = HostAppTests; + productName = HostAppTests; + productReference = 9070FFB0285112B5009867D5 /* HostAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 9070FFB9285112B5009867D5 /* HostAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9070FFCA285112B5009867D5 /* Build configuration list for PBXNativeTarget "HostAppUITests" */; + buildPhases = ( + 9070FFB6285112B5009867D5 /* Sources */, + 9070FFB7285112B5009867D5 /* Frameworks */, + 9070FFB8285112B5009867D5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 9070FFBC285112B5009867D5 /* PBXTargetDependency */, + ); + name = HostAppUITests; + productName = HostAppUITests; + productReference = 9070FFBA285112B5009867D5 /* HostAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9070FF98285112B4009867D5 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1340; + LastUpgradeCheck = 1340; + TargetAttributes = { + 9070FF9F285112B4009867D5 = { + CreatedOnToolsVersion = 13.4.1; + }; + 9070FFAF285112B5009867D5 = { + CreatedOnToolsVersion = 13.4.1; + TestTargetID = 9070FF9F285112B4009867D5; + }; + 9070FFB9285112B5009867D5 = { + CreatedOnToolsVersion = 13.4.1; + TestTargetID = 9070FF9F285112B4009867D5; + }; + }; + }; + buildConfigurationList = 9070FF9B285112B4009867D5 /* Build configuration list for PBXProject "HostApp" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9070FF97285112B4009867D5; + packageReferences = ( + 8704823D2D6CE3FE00DF249F /* XCRemoteSwiftPackageReference "amplify-ui-swift-liveness" */, + ); + productRefGroup = 9070FFA1285112B4009867D5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 9070FF9F285112B4009867D5 /* HostApp */, + 9070FFAF285112B5009867D5 /* HostAppTests */, + 9070FFB9285112B5009867D5 /* HostAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9070FF9E285112B4009867D5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 973619262BA378690003A590 /* awsconfiguration.json in Resources */, + 9070FFAB285112B5009867D5 /* Preview Assets.xcassets in Resources */, + 9070FFA8285112B5009867D5 /* Assets.xcassets in Resources */, + 973619252BA378690003A590 /* amplifyconfiguration.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9070FFAE285112B5009867D5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9070FFB8285112B5009867D5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 9070FF9C285112B4009867D5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 906AB82829EA04B5007FFC81 /* ExampleLivenessViewModel.swift in Sources */, + 906AB83029EA0DC5007FFC81 /* LivenessResultContentView+Result.swift in Sources */, + 87BCB9652D67C0A40055559D /* CredsProvider.swift in Sources */, + 909308CD297DC4E700F3CC6E /* UIColor+Hex.swift in Sources */, + 90493F822992D64000CFE674 /* LivenessResult.swift in Sources */, + 906AB82229E9F432007FFC81 /* View+Background.swift in Sources */, + 906AB82C29EA0BFD007FFC81 /* StartSessionViewModel.swift in Sources */, + 909308C5297DC49C00F3CC6E /* LivenessCheckErrorContentView.swift in Sources */, + 906AB82A29EA0927007FFC81 /* StartSessionView+PresentationState.swift in Sources */, + 909308C6297DC49C00F3CC6E /* LivenessResultView.swift in Sources */, + 90FDF2A5299BDF3E0002CE7D /* CreateSessionResponse.swift in Sources */, + 909308D1297EE67100F3CC6E /* ExampleLivenessView.swift in Sources */, + 909308C7297DC49C00F3CC6E /* LivenessResultContentView.swift in Sources */, + 906AB82629E9F554007FFC81 /* Color+DynamicColors.swift in Sources */, + 90236C77299D6D41009FD1A7 /* HostAppApp.swift in Sources */, + 909308CC297DC4E700F3CC6E /* Color+Hex.swift in Sources */, + 906AB82429E9F48C007FFC81 /* StartSessionView.swift in Sources */, + 904CC73D2996E650002E0753 /* RootView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9070FFAC285112B5009867D5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9070FFB6285112B5009867D5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9070FFC1285112B5009867D5 /* HostAppUITestsLaunchTests.swift in Sources */, + 9070FFBF285112B5009867D5 /* HostAppUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 9070FFB2285112B5009867D5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9070FF9F285112B4009867D5 /* HostApp */; + targetProxy = 9070FFB1285112B5009867D5 /* PBXContainerItemProxy */; + }; + 9070FFBC285112B5009867D5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9070FF9F285112B4009867D5 /* HostApp */; + targetProxy = 9070FFBB285112B5009867D5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 9070FFC2285112B5009867D5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG LANDMARK_DEBUG_MODE"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 9070FFC3285112B5009867D5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 9070FFC5285112B5009867D5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = HostApp/HostApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"HostApp/Preview Content\""; + DEVELOPMENT_TEAM = RP2SA7L4ZN; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = HostApp/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Liveness; + INFOPLIST_KEY_NSCameraUsageDescription = "\"Allow camera permission to record video\""; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazonaws.mobile.amplify.liveness.testing.hostapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG LANDMARK_DEBUG_MODE"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 9070FFC6285112B5009867D5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = HostApp/HostApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"HostApp/Preview Content\""; + DEVELOPMENT_TEAM = RP2SA7L4ZN; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = HostApp/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Liveness; + INFOPLIST_KEY_NSCameraUsageDescription = "\"Allow camera permission to record video\""; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazonaws.mobile.amplify.liveness.testing.hostapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 9070FFC8285112B5009867D5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = G3Q4R66M4R; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.coffee.HostAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HostApp.app/HostApp"; + }; + name = Debug; + }; + 9070FFC9285112B5009867D5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = G3Q4R66M4R; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.coffee.HostAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HostApp.app/HostApp"; + }; + name = Release; + }; + 9070FFCB285112B5009867D5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = G3Q4R66M4R; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.coffee.HostAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = HostApp; + }; + name = Debug; + }; + 9070FFCC285112B5009867D5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = G3Q4R66M4R; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.coffee.HostAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = HostApp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9070FF9B285112B4009867D5 /* Build configuration list for PBXProject "HostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9070FFC2285112B5009867D5 /* Debug */, + 9070FFC3285112B5009867D5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9070FFC4285112B5009867D5 /* Build configuration list for PBXNativeTarget "HostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9070FFC5285112B5009867D5 /* Debug */, + 9070FFC6285112B5009867D5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9070FFC7285112B5009867D5 /* Build configuration list for PBXNativeTarget "HostAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9070FFC8285112B5009867D5 /* Debug */, + 9070FFC9285112B5009867D5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9070FFCA285112B5009867D5 /* Build configuration list for PBXNativeTarget "HostAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9070FFCB285112B5009867D5 /* Debug */, + 9070FFCC285112B5009867D5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 8704823D2D6CE3FE00DF249F /* XCRemoteSwiftPackageReference "amplify-ui-swift-liveness" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/aws-amplify/amplify-ui-swift-liveness"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 973619212BA378200003A590 /* FaceLiveness */ = { + isa = XCSwiftPackageProductDependency; + productName = FaceLiveness; + }; + 97D1A8E82BA3757700FF1368 /* AWSAPIPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSAPIPlugin; + }; + 97D1A8EA2BA3757700FF1368 /* AWSCognitoAuthPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSCognitoAuthPlugin; + }; + 97D1A8EC2BA3757700FF1368 /* Amplify */ = { + isa = XCSwiftPackageProductDependency; + productName = Amplify; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 9070FF98285112B4009867D5 /* Project object */; +} diff --git a/HostApp/HostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/HostApp/HostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/HostApp/HostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..d736e668 --- /dev/null +++ b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,68 @@ +{ + "pins" : [ + { + "identity" : "amplify-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift", + "state" : { + "revision" : "7b1d5ee05a23ea24c7458dffed1c563c8501042d", + "version" : "2.45.4" + } + }, + { + "identity" : "amplify-swift-utils-notifications", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", + "state" : { + "revision" : "959eec669ba97c7d923b963c3e66ca8a0b2737f6", + "version" : "1.1.1" + } + }, + { + "identity" : "aws-crt-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-crt-swift", + "state" : { + "revision" : "3f844bef042cc0a4c3381f7090414ce3f9a7e935", + "version" : "0.37.0" + } + }, + { + "identity" : "aws-sdk-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-sdk-swift", + "state" : { + "revision" : "c6c1064da9bfccb119a7a8ab9ba636fb3bbfa6f5", + "version" : "1.0.47" + } + }, + { + "identity" : "smithy-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/smithy-lang/smithy-swift", + "state" : { + "revision" : "3cd9f181b3ba8ff71da43bf53c09f8de6790a4ad", + "version" : "0.96.0" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version" : "1.6.2" + } + } + ], + "version" : 2 +} diff --git a/HostApp/HostApp.xcodeproj/xcshareddata/xcschemes/HostApp.xcscheme b/HostApp/HostApp.xcodeproj/xcshareddata/xcschemes/HostApp.xcscheme new file mode 100644 index 00000000..ee175798 --- /dev/null +++ b/HostApp/HostApp.xcodeproj/xcshareddata/xcschemes/HostApp.xcscheme @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HostApp/HostApp/Assets.xcassets/AccentColor.colorset/Contents.json b/HostApp/HostApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/HostApp/HostApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HostApp/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/HostApp/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/HostApp/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HostApp/HostApp/Assets.xcassets/Contents.json b/HostApp/HostApp/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/HostApp/HostApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HostApp/HostApp/HostApp.entitlements b/HostApp/HostApp/HostApp.entitlements new file mode 100644 index 00000000..16ebdce6 --- /dev/null +++ b/HostApp/HostApp/HostApp.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.amazonaws.mobile.amplify.liveness.testing.hostapp + + + diff --git a/HostApp/HostApp/HostAppApp.swift b/HostApp/HostApp/HostAppApp.swift new file mode 100644 index 00000000..9a96d9f0 --- /dev/null +++ b/HostApp/HostApp/HostAppApp.swift @@ -0,0 +1,72 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness +import Amplify +import AWSCognitoAuthPlugin +import AWSAPIPlugin + +@main +struct HostAppApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + func increaseBrightness() { + UIScreen.main.brightness = 1.0 + } + + var body: some Scene { + WindowGroup { + RootView() + } + } + + init() { + do { + Amplify.Logging.logLevel = .verbose + let auth = AWSCognitoAuthPlugin() + let api = AWSAPIPlugin() + try Amplify.add(plugin: auth) + //try Amplify.add(plugin: api) + try Amplify.configure() + } catch { + print("Error configuring Amplify", error) + } + } +} + +class AppDelegate: NSObject, UIApplicationDelegate { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + if connectingSceneSession.role == .windowApplication { + configuration.delegateClass = SceneDelegate.self + } + return configuration + } +} + +class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + if #available(iOS 15.0, *) { + self.window = (scene as? UIWindowScene)?.keyWindow + } else { + self.window = (scene as? UIWindowScene)?.windows + .first(where: \.isKeyWindow) + } + } +} + diff --git a/HostApp/HostApp/Info.plist b/HostApp/HostApp/Info.plist new file mode 100644 index 00000000..b72ec3ba --- /dev/null +++ b/HostApp/HostApp/Info.plist @@ -0,0 +1,15 @@ + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + myapp + + + + + diff --git a/HostApp/HostApp/Model/CreateSessionResponse.swift b/HostApp/HostApp/Model/CreateSessionResponse.swift new file mode 100644 index 00000000..f2f44e37 --- /dev/null +++ b/HostApp/HostApp/Model/CreateSessionResponse.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct CreateSessionResponse: Codable { + let sessionId: String +} diff --git a/HostApp/HostApp/Model/LivenessResult.swift b/HostApp/HostApp/Model/LivenessResult.swift new file mode 100644 index 00000000..226bc30f --- /dev/null +++ b/HostApp/HostApp/Model/LivenessResult.swift @@ -0,0 +1,25 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct LivenessResult: Codable { + let auditImageBytes: String? + let confidenceScore: Double + let isLive: Bool +} + +extension LivenessResult: CustomDebugStringConvertible { + var debugDescription: String { + """ + LivenessResult + - confidenceScore: \(confidenceScore) + - isLive: \(isLive) + - auditImageBytes: \(auditImageBytes == nil ? "nil" : "") + """ + } +} diff --git a/HostApp/HostApp/Preview Content/Preview Assets.xcassets/Contents.json b/HostApp/HostApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/HostApp/HostApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HostApp/HostApp/Utilities/Color+DynamicColors.swift b/HostApp/HostApp/Utilities/Color+DynamicColors.swift new file mode 100644 index 00000000..71afbf63 --- /dev/null +++ b/HostApp/HostApp/Utilities/Color+DynamicColors.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension Color { + static func dynamicColors(light: UIColor, dark: UIColor) -> Color { + Color( + UIColor( + dynamicProvider: { traitCollection in + switch traitCollection.userInterfaceStyle { + case .dark: return dark + default: return light + } + } + ) + ) + } +} diff --git a/HostApp/HostApp/Utilities/Color+Hex.swift b/HostApp/HostApp/Utilities/Color+Hex.swift new file mode 100644 index 00000000..0136a714 --- /dev/null +++ b/HostApp/HostApp/Utilities/Color+Hex.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import UIKit + +extension Color { + static func hex(_ hex: String) -> Color { + Color(UIColor.hex(hex)) + } +} diff --git a/HostApp/HostApp/Utilities/UIColor+Hex.swift b/HostApp/HostApp/Utilities/UIColor+Hex.swift new file mode 100644 index 00000000..678773d1 --- /dev/null +++ b/HostApp/HostApp/Utilities/UIColor+Hex.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit + +extension UIColor { + static func hex(_ hex: String) -> UIColor { + assert(hex.hasPrefix("#")) + + let hex = String(hex.dropFirst()) + assert(hex.count == 6) + + let scanner = Scanner(string: hex) + var hexNumber: UInt64 = 0 + + precondition(scanner.scanHexInt64(&hexNumber)) + let r, g, b, a: CGFloat + r = CGFloat((hexNumber & 0xFF0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x00FF00) >> 8) / 255 + b = CGFloat((hexNumber & 0x0000FF)) / 255 + a = 1.0 + + return UIColor(red: r, green: g, blue: b, alpha: a) + } +} diff --git a/HostApp/HostApp/Utilities/View+Background.swift b/HostApp/HostApp/Utilities/View+Background.swift new file mode 100644 index 00000000..4ffb074c --- /dev/null +++ b/HostApp/HostApp/Utilities/View+Background.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension View { + @ViewBuilder func _background( + alignment: Alignment = .center, + @ViewBuilder _ content: () -> Content + ) -> some View { + if #available(iOS 15.0, *) { + background(alignment: alignment, content: content) + } else { + background(content(), alignment: alignment) + } + } +} diff --git a/HostApp/HostApp/Views/CredsProvider.swift b/HostApp/HostApp/Views/CredsProvider.swift new file mode 100644 index 00000000..8c33e1ad --- /dev/null +++ b/HostApp/HostApp/Views/CredsProvider.swift @@ -0,0 +1,22 @@ +// +// CredsProvider.swift +// HostApp +// +// Created by victor julio on 20/02/25. +// + +//import Amplify +//import AWSPluginsCore +//import AwsCommonRuntimeKit +// +// +//struct CredsProvider: AWSCredentialsProvider { +// func fetchAWSCredentials() async throws -> AWSCredentials { +// // Fetch the credentials +// let provider = try CredentialsProvider(source: .static(accessKey: accessKey, +// secret: secret, +// sessionToken: sessionToken)) +// return provider +// +// } +//} diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift new file mode 100644 index 00000000..122f26c1 --- /dev/null +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -0,0 +1,97 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness + + +struct ExampleLivenessView: View { + @Binding var isPresented: Bool + @ObservedObject var viewModel: ExampleLivenessViewModel + + init(sessionID: String, isPresented: Binding) { + self.viewModel = .init(sessionID: sessionID) + self._isPresented = isPresented + } + + + var body: some View { + switch viewModel.presentationState { + case .liveness: + FaceLivenessDetectorView( + sessionID: viewModel.sessionID, + region: "us-east-1", + isPresented: Binding( + get: { viewModel.presentationState == .liveness }, + set: { _ in } + ), + onCompletion: { result in + DispatchQueue.main.async { + switch result { + case .success: + withAnimation { viewModel.presentationState = .result } + case .failure(.sessionNotFound), .failure(.cameraPermissionDenied), .failure(.accessDenied): + viewModel.presentationState = .liveness + isPresented = false + case .failure(.userCancelled): + viewModel.presentationState = .liveness + isPresented = false + case .failure(.sessionTimedOut): + viewModel.presentationState = .error(.sessionTimedOut) + case .failure(.socketClosed): + viewModel.presentationState = .error(.socketClosed) + case .failure(.countdownNoFace), .failure(.countdownFaceTooClose), .failure(.countdownMultipleFaces): + viewModel.presentationState = .error(.countdownFaceTooClose) + case .failure(.invalidSignature): + viewModel.presentationState = .error(.invalidSignature) + case .failure(.cameraNotAvailable): + viewModel.presentationState = .error(.cameraNotAvailable) + case .failure(.validation): + viewModel.presentationState = .error(.validation) + case .failure(.faceInOvalMatchExceededTimeLimitError): + viewModel.presentationState = .error(.faceInOvalMatchExceededTimeLimitError) + case .failure(_): + viewModel.presentationState = .error(.unknown) + } + } + } + ) + .id(isPresented) + case .result: + LivenessResultView( + sessionID: viewModel.sessionID, + onTryAgain: { isPresented = false }, + content: { + LivenessResultContentView(fetchResults: viewModel.fetchLivenessResult) + } + ) + .animation(.default, value: viewModel.presentationState) + case .error(let detectionError): + LivenessResultView( + sessionID: viewModel.sessionID, + onTryAgain: { isPresented = false }, + content: { + switch detectionError { + case .socketClosed: + LivenessCheckErrorContentView.sessionTimeOut + case .sessionTimedOut: + LivenessCheckErrorContentView.faceMatchTimeOut + case .countdownNoFace, .countdownFaceTooClose, .countdownMultipleFaces: + LivenessCheckErrorContentView.failedDuringCountdown + case .invalidSignature: + LivenessCheckErrorContentView.invalidSignature + case .cameraNotAvailable: + LivenessCheckErrorContentView.cameraNotAvailable + default: + LivenessCheckErrorContentView.unexpected + } + } + ) + .animation(.default, value: viewModel.presentationState) + } + } +} diff --git a/HostApp/HostApp/Views/ExampleLivenessViewModel.swift b/HostApp/HostApp/Views/ExampleLivenessViewModel.swift new file mode 100644 index 00000000..2650e07d --- /dev/null +++ b/HostApp/HostApp/Views/ExampleLivenessViewModel.swift @@ -0,0 +1,42 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness +import Amplify + +class ExampleLivenessViewModel: ObservableObject { + @Published var presentationState = PresentationState.liveness + let sessionID: String + + init(sessionID: String) { + self.sessionID = sessionID + } + + func fetchLivenessResult() async throws -> LivenessResultContentView.Result { + guard let url = URL(string: "https://c18b-181-236-137-100.ngrok-free.app/liveness/result/\(sessionID)") else { + throw URLError(.badURL) + } + + // Configura la petición GET + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // Realiza la llamada asíncrona al endpoint + let (data, _) = try await URLSession.shared.data(for: request) + + // Decodifica la respuesta en el modelo LivenessResultContentView.Result + let livenessResult = try JSONDecoder().decode(LivenessResult.self, from: data) + let score = LivenessResultContentView.Result(livenessResult: livenessResult) + return score + } + + enum PresentationState: Equatable { + case liveness, result, error(FaceLivenessDetectionError) + } +} diff --git a/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift b/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift new file mode 100644 index 00000000..866c2c1c --- /dev/null +++ b/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift @@ -0,0 +1,69 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessCheckErrorContentView: View { + let name: String + let description: String + + var body: some View { + VStack(alignment: .leading) { + HStack { + Image(systemName: "exclamationmark.circle.fill") + .foregroundColor(.hex("#950404")) + Text("Error: \(name)") + .fontWeight(.semibold) + } + .padding(.bottom, 4) + Text(description) + } + } +} + +extension LivenessCheckErrorContentView { + static let mock = LivenessCheckErrorContentView( + name: "Time out", + description: "Face didn't fit inside oval in time limit. Try again and completely fill the oval with face in it." + ) + + static let unexpected = LivenessCheckErrorContentView( + name: "An unexpected error ocurred", + description: "Please try again." + ) + + static let faceMatchTimeOut = LivenessCheckErrorContentView( + name: "Time out", + description: "Face did not fill oval in time limit. Try again and completely fill the oval with face in it." + ) + + static let sessionTimeOut = LivenessCheckErrorContentView( + name: "Connection interrupted", + description: "Your connection was unexpectedly closed." + ) + + static let failedDuringCountdown = LivenessCheckErrorContentView( + name: "Check failed during countdown", + description: "Avoid moving closer during countdown and ensure only one face is in front of camera." + ) + + static let invalidSignature = LivenessCheckErrorContentView( + name: "The signature on the request is invalid.", + description: "Ensure the device time is correct and try again." + ) + + static let cameraNotAvailable = LivenessCheckErrorContentView( + name: "The camera could not be started.", + description: "There might be a hardware issue with the camera." + ) +} + +struct LivenessCheckErrorContentView_Previews: PreviewProvider { + static var previews: some View { + LivenessCheckErrorContentView.mock + } +} diff --git a/HostApp/HostApp/Views/LivenessResultContentView+Result.swift b/HostApp/HostApp/Views/LivenessResultContentView+Result.swift new file mode 100644 index 00000000..3f57982f --- /dev/null +++ b/HostApp/HostApp/Views/LivenessResultContentView+Result.swift @@ -0,0 +1,80 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension LivenessResultContentView { + struct Result { + let text: String + let value: String + let valueTextColor: Color + let valueBackgroundColor: Color + let auditImage: Data? + let isLive: Bool + + init(livenessResult: LivenessResult) { + guard livenessResult.confidenceScore > 0 else { + text = "" + value = "" + valueTextColor = .clear + valueBackgroundColor = .clear + auditImage = nil + isLive = false + return + } + isLive = livenessResult.isLive + let truncated = String(format: "%.4f", livenessResult.confidenceScore) + value = truncated + if livenessResult.isLive { + valueTextColor = .hex("#365E3D") + valueBackgroundColor = .hex("#D6F5DB") + text = "Check successful" + } else { + valueTextColor = .hex("#660000") + valueBackgroundColor = .hex("#F5BCBC") + text = "Check unsuccessful" + } + auditImage = livenessResult.auditImageBytes.flatMap{ + Data(base64Encoded: $0) + } + } + } + + struct Score { + let resultText: String + let value: String + let valueTextColor: Color + let valueBackgroundColor: Color + + init( + value: Double, + colorRule: (Double) -> (Color, Color, String) = colorRule + ) { + let truncated = String(format: "%.4f", value) + let (textColor, backgroundColor, resultText) = colorRule(value) + self.resultText = resultText + self.value = truncated + self.valueTextColor = textColor + self.valueBackgroundColor = backgroundColor + } + } +} + +fileprivate func colorRule(v: Double) -> (Color, Color, String) { + let textColor, backgroundColor: Color + let resultText: String + if v >= 70 { + textColor = .hex("#365E3D") + backgroundColor = .hex("#D6F5DB") + resultText = "Check successful" + } else { + textColor = .hex("#660000") + backgroundColor = .hex("#F5BCBC") + resultText = "Check unsuccessful" + } + return (textColor, backgroundColor, resultText) +} diff --git a/HostApp/HostApp/Views/LivenessResultContentView.swift b/HostApp/HostApp/Views/LivenessResultContentView.swift new file mode 100644 index 00000000..de2ecff7 --- /dev/null +++ b/HostApp/HostApp/Views/LivenessResultContentView.swift @@ -0,0 +1,113 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessResultContentView: View { + @State var result: Result = .init(livenessResult: .init(auditImageBytes: nil, confidenceScore: -1, isLive: false)) + let fetchResults: () async throws -> Result + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text("Result:") + Text(result.text) + .fontWeight(.semibold) + } + + HStack { + Text("Liveness confidence score:") + Text(result.value) + .foregroundColor(result.valueTextColor) + .padding(6) + .background(result.valueBackgroundColor) + .cornerRadius(8) + } + + if let image = result.auditImage { + Image(uiImage: .init(data: image) ?? UIImage()) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 300) + .background(Color.secondary.opacity(0.1)) + } else { + Image(systemName: "person.fill") + .font(.system(size: 128)) + .frame(maxWidth: .infinity, idealHeight: 268) + .background(Color.secondary.opacity(0.1)) + } + + if !result.isLive { + steps() + .padding() + .background( + Rectangle() + .foregroundColor( + .dynamicColors( + light: .hex("#ECECEC"), + dark: .darkGray + ) + ) + .cornerRadius(6)) + } + } + .padding(.bottom, 16) + .onAppear { + Task { + do { + self.result = try await fetchResults() + } catch { + print("Error fetching result", error) + } + } + } + } + + private func steps() -> some View { + func step(number: Int, text: String) -> some View { + HStack(alignment: .top) { + Text("\(number).") + Text(text) + } + } + + return VStack( + alignment: .leading, + spacing: 8 + ) { + Text("Tips to pass the video check:") + .fontWeight(.semibold) + + step(number: 1, text: "Avoid very bright lighting conditions, such as direct sunlight.") + .accessibilityElement(children: .combine) + + step(number: 2, text: "Remove sunglasses, mask, hat, or anything blocking your face.") + .accessibilityElement(children: .combine) + } + } +} + + +extension LivenessResultContentView { + static let mock = LivenessResultContentView( + fetchResults: { + .init( + livenessResult: .init( + auditImageBytes: nil, + confidenceScore: 99.8329, + isLive: true + ) + ) + } + ) +} + +struct LivenessResultContentView_Previews: PreviewProvider { + static var previews: some View { + LivenessResultContentView.mock + } +} diff --git a/HostApp/HostApp/Views/LivenessResultView.swift b/HostApp/HostApp/Views/LivenessResultView.swift new file mode 100644 index 00000000..23ba99fe --- /dev/null +++ b/HostApp/HostApp/Views/LivenessResultView.swift @@ -0,0 +1,149 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessResultView: View { + let title: String + let sessionID: String + let content: Content + let onTryAgain: () -> Void + @State var displayingCopiedNotification = false + + init( + title: String = "Liveness Result", + sessionID: String, + onTryAgain: @escaping () -> Void, + @ViewBuilder content: () -> Content + ) { + self.title = title + self.sessionID = sessionID + self.content = content() + self.onTryAgain = onTryAgain + } + + var body: some View { + VStack { + ScrollView { + VStack(alignment: .leading) { + Text(title) + .font(.system(size: 34, weight: .semibold)) + .padding(.bottom, 8) + + sessionIDBox + .padding(.bottom, 16) + + content + } + .padding() + } + + if displayingCopiedNotification { + Text("Copied Session ID") + .foregroundColor(.dynamicColors(light: .white, dark: .black)) + .padding(8) + .background(Color.dynamicColors(light: .darkGray, dark: .lightGray)) + .cornerRadius(6) + .onAppear { + Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + withAnimation { + displayingCopiedNotification = false + } + } + } + } + tryAgainButton + } + } + + private func copySessionID() { + withAnimation { + displayingCopiedNotification = true + } + UIPasteboard.general.string = sessionID + } + + private var sessionIDBox: some View { + HStack { + VStack(alignment: .leading) { + Text("Session ID:") + .fontWeight(.semibold) + Text(sessionID) + } + Spacer() + Button( + action: copySessionID, + label: { + Image(systemName: "square.on.square") + .foregroundColor(.primary) + .frame(width: 20, height: 20) + } + ) + .frame(width: 44, height: 44) + } + .padding() + .background( + Rectangle() + .foregroundColor( + .dynamicColors( + light: .hex("#ECECEC"), + dark: .darkGray + ) + ) + .cornerRadius(6) + ) + } + + private var tryAgainButton: some View { + Button( + action: onTryAgain, + label: { + Text("Try Again") + .foregroundColor( + .dynamicColors(light: .white, dark: .black) + ) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { + Color.dynamicColors(light: .hex("#047D95"), dark: .hex("#7dd6e8")) + } + .cornerRadius(14) + .padding(.leading) + .padding(.trailing) + .padding(.bottom, 16) + } +} + +extension LivenessResultView where Content == LivenessResultContentView { + static var sessionID: String { + String(UUID().uuidString.flatMap { $0.lowercased() }) + } + + static var mock: Self { + .init( + sessionID: sessionID, + onTryAgain: {}, + content: { LivenessResultContentView.mock } + ) + } +} + +struct LivenessCheckView_Previews: PreviewProvider { + static let sessionID = String(UUID().uuidString.flatMap { $0.lowercased() }) + static var previews: some View { + LivenessResultView( + sessionID: sessionID, + onTryAgain: {}, + content: { + LivenessCheckErrorContentView.mock + } + ) + } +} + diff --git a/HostApp/HostApp/Views/RootView.swift b/HostApp/HostApp/Views/RootView.swift new file mode 100644 index 00000000..7600f1b4 --- /dev/null +++ b/HostApp/HostApp/Views/RootView.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct RootView: View { + @EnvironmentObject var sceneDelegate: SceneDelegate + @State var sessionID = "" + @State var isPresentingContainerView = false + + var body: some View { + if isPresentingContainerView { + ExampleLivenessView( + sessionID: sessionID, + isPresented: $isPresentingContainerView + ) + } else { + StartSessionView( + sessionID: $sessionID, + isPresentingContainerView: $isPresentingContainerView + ) + .background(Color.dynamicColors(light: .white, dark: .secondarySystemBackground)) + .edgesIgnoringSafeArea(.all) + } + } +} diff --git a/HostApp/HostApp/Views/StartSessionView+PresentationState.swift b/HostApp/HostApp/Views/StartSessionView+PresentationState.swift new file mode 100644 index 00000000..035a2f77 --- /dev/null +++ b/HostApp/HostApp/Views/StartSessionView+PresentationState.swift @@ -0,0 +1,55 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension StartSessionView { + struct PresentationState: Equatable { + let buttonText: String + let buttonBackgroundColor: Color + let buttonAction: () -> Void + let buttonEnabled: Bool + + static let loading = PresentationState( + buttonText: "...", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: {}, + buttonEnabled: false + ) + + static func signedIn(action: @escaping () -> Void) -> PresentationState { + PresentationState( + buttonText: "Sign Out", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: action, + buttonEnabled: true + ) + } + + static func signedOut(action: @escaping () -> Void) -> PresentationState { + PresentationState( + buttonText: "Sign In", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: action, + buttonEnabled: true + ) + } + + static func == (lhs: StartSessionView.PresentationState, rhs: StartSessionView.PresentationState) -> Bool { + lhs.buttonText == rhs.buttonText + } + } +} diff --git a/HostApp/HostApp/Views/StartSessionView.swift b/HostApp/HostApp/Views/StartSessionView.swift new file mode 100644 index 00000000..a3a7c2f5 --- /dev/null +++ b/HostApp/HostApp/Views/StartSessionView.swift @@ -0,0 +1,96 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import Amplify + +struct StartSessionView: View { + @EnvironmentObject var sceneDelegate: SceneDelegate + @ObservedObject var viewModel = StartSessionViewModel() + @Binding var sessionID: String + @Binding var isPresentingContainerView: Bool + @State private var showAlert = false + + var body: some View { + VStack { + Spacer() + button( + text: viewModel.presentationState.buttonText, + backgroundColor: viewModel.presentationState.buttonBackgroundColor, + action: viewModel.presentationState.buttonAction, + enabled: viewModel.presentationState.buttonEnabled + ) + + button( + text: "Create Liveness Session", + backgroundColor: .dynamicColors( + light: .hex("#047D95"), + dark: .hex("#7dd6e8") + ), + action: { + viewModel.createSession { sessionId, err in + if let sessionId = sessionId { + sessionID = sessionId + isPresentingContainerView = true + } + + showAlert = err != nil + } + }, + enabled: true + ) + .alert(isPresented: $showAlert) { + Alert( + title: Text("Error Creating Liveness Session"), + message: Text("Unable to create a liveness session id. Please try again."), + dismissButton: .default( + Text("OK"), + action: { + isPresentingContainerView = false + } + ) + ) + } + + Spacer() + HStack { + Spacer() + Text("v0.1.19") + .font(.callout) + .padding() + } + .padding() + } + .onAppear { viewModel.setup() } + } + + func button( + text: String, + backgroundColor: Color, + action: @escaping () -> Void, + enabled: Bool + ) -> some View { + Button( + action: action, + label: { + Text(text) + .foregroundColor(.dynamicColors(light: .white, dark: .black)) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { + backgroundColor.opacity(enabled ? 1.0 : 0.6) + } + .cornerRadius(14) + .padding(.leading) + .padding(.trailing) + .padding(.bottom, 16) + .disabled(!enabled) + } +} + diff --git a/HostApp/HostApp/Views/StartSessionViewModel.swift b/HostApp/HostApp/Views/StartSessionViewModel.swift new file mode 100644 index 00000000..dc2b2ad3 --- /dev/null +++ b/HostApp/HostApp/Views/StartSessionViewModel.swift @@ -0,0 +1,103 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import Amplify +import Foundation + +class StartSessionViewModel: ObservableObject { + @Published var presentationState: StartSessionView.PresentationState = .loading + var window: UIWindow? + + var isSignedIn: Bool { + presentationState == .signedIn {} + } + + func setup() { + Task { @MainActor in + presentationState = .loading + do { + let session = try await Amplify.Auth.fetchAuthSession() + //print("the session") + //print(session) + //print("okok") + presentationState = session.isSignedIn + ? .signedIn(action: signOut) + : .signedOut(action: signIn) + } catch { + presentationState = .signedOut(action: signIn) + print("Error fetching auth session", error) + } + + } + } + + struct StartResponse: Codable { + let sid: String + } + + func createSession(_ completion: @escaping (String?, Error?) -> Void) { + Task { @MainActor in + let currentPresentationState = presentationState + presentationState = .loading + + do { + // Configura la URL y la petición + guard let url = URL(string: "https://c18b-181-236-137-100.ngrok-free.app/liveness/start") else { + throw URLError(.badURL) + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // Si necesitas enviar un body (en este ejemplo se envía un JSON vacío) + let body: [String: Any] = [:] + request.httpBody = try? JSONSerialization.data(withJSONObject: body) + + // Realiza la llamada asíncrona al API + let (data, _) = try await URLSession.shared.data(for: request) + + // Decodifica la respuesta JSON y extrae el sId + let decoder = JSONDecoder() + let startResponse = try decoder.decode(StartResponse.self, from: data) + print("respone: \(startResponse)") + let sessionId = startResponse.sid + + presentationState = currentPresentationState + completion(sessionId, nil) + } catch { + presentationState = currentPresentationState + print("Error creating session:", error) + completion(nil, error) + } + } + } + + func signIn() { + Task { @MainActor in + presentationState = .loading + do { + let signInResult = try await Amplify.Auth.signInWithWebUI(presentationAnchor: window) + if signInResult.isSignedIn { + presentationState = .signedIn(action: signOut) + } + } catch { + print("Error signing in with web UI", error) + } + + } + } + + func signOut() { + Task { @MainActor in + presentationState = .loading + _ = await Amplify.Auth.signOut() + presentationState = .signedOut(action: signIn) + } + } +} diff --git a/HostApp/HostAppUITests/HostAppUITests.swift b/HostApp/HostAppUITests/HostAppUITests.swift new file mode 100644 index 00000000..812cc948 --- /dev/null +++ b/HostApp/HostAppUITests/HostAppUITests.swift @@ -0,0 +1,41 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +class HostAppUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/HostApp/HostAppUITests/HostAppUITestsLaunchTests.swift b/HostApp/HostAppUITests/HostAppUITestsLaunchTests.swift new file mode 100644 index 00000000..8caed7c5 --- /dev/null +++ b/HostApp/HostAppUITests/HostAppUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +class HostAppUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/HostApp/README.md b/HostApp/README.md new file mode 100644 index 00000000..1234c14b --- /dev/null +++ b/HostApp/README.md @@ -0,0 +1,117 @@ +AWS Amplify + +--- + +# Amplify UI Liveness Sample App + +Amplify UI FaceLivenessDetector provides a UI component for Amazon Rekognition Face Liveness that helps developers verify that only real users, not bad actors using spoofs, can access your services. + +More information on setting up and using the FaceLivenessDetector is in the [Amplify UI Face Liveness documentation](https://ui.docs.amplify.aws/swift/connected-components/liveness). + +## Running the App + +Prerequisite: The host app requires backend resources to be configured with Amplify Auth category for authentication and Amplify API for starting/creating Liveness sessions. + +1. Install Xcode version 12.0 or higher +2. clone the Amplify UI Swift Liveness repository: +``` +git clone https://github.com/aws-amplify/amplify-ui-swift-liveness.git +``` +3. Change directory to `HostApp` +``` +cd amplify-ui-swift-livenes/HostApp +``` +4. Open HostApp.xcodeproj in Xcode + +5. The sample app depends on a real device camera to capture videos. Build and deploy the app onto a real device. + +6. When the app is launch, click the Sign in button to sign up and then sign in. + +7. Once signed in and authenticated, the "Create Liveness Session" is enabled. Click the button to generate and get a session id from your backend. + +8. Once a session id is created, the Liveness Check screen is displayed. Follow the instructions and click on Start video check button to begin liveness verification. + +## Provision AWS Backend Resources + +1. Follow the [instructions](https://docs.amplify.aws/start/getting-started/installation/q/integration/ios/) to sign up for an AWS account and set up the Amplify CLI. +2. Initialize Amplify in the project by running the following command from the project directory: +``` +amplify init +``` +Provide the responses shown after each of the following prompts. +``` +? Enter a name for the project: `LivenessHostApp` +The following configuration will be applied: + +Project information +| Name: LivenessHostApp +| Environment: dev +| Default editor: Visual Studio Code +| App type: ios + +? Initialize the project with the above configuration? No +? Enter a name for the environment: `dev` +? Choose your default editor: `Xcode (macOS only)` +? Choose the type of app that you're building: `ios` +? Select the authentication method you want to use: +`AWS profile` +? Please choose the profile you want to use +`default` +``` +Wait until provisioning is finished. Upon successfully running `amplify init`, you will see a configuration file created called `amplifyconfiguration.json`. This file will be bundled into your application so that the Amplify libraries know how to reach your provisioned backend resources at runtime. You can verify by checking that the file is included in the `Copy Bundle Resources` Build Phases for the `HostApp` target in Xcode. + +3. Configure Auth Category + +The Amplify Auth category provides an interface for authenticating a user and also provides the necessary authorization to other Amplify categories. It comes with default, built-in support for Amazon Cognito User Pools and Identity Pools. From your project directory, run the following command to add the Amplify Auth category: +``` +amplify add auth +``` +Provide the responses shown after each of the following prompts. +``` +? Do you want to use the default authentication and security configuration? + `Default configuration with Social Provider (Federation)` +? How do you want users to be able to sign in? + `Username` +? Do you want to configure advanced settings? + `No, I am done.` +? What domain name prefix you want us to create for you? + `(default)` +? Enter your redirect signin URI: + `myapp://` +? Do you want to add another redirect signin URI + `No` +? Enter your redirect signout URI: + `myapp://` +? Do you want to add another redirect signout URI + `No` +? Select the social providers you want to configure for your user pool: + `` +``` +4. Once finished, run `amplify push` to publish your changes. + Upon completion, `amplifyconfiguration.json` should be updated to reference these provisioned backend resources. +5. Follow the steps below to create an inline policy to enable authenticated app users to access Rekognition, which powers the FaceLivenessDetector. + 1. Go to AWS IAM console, then Roles + 2. Select the newly created `authRole` for the project (`amplify----authRole`). + 3. Choose **Add Permissions**, then select **Create Inline Policy**, then choose **JSON** and paste the following: + + ``` + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "rekognition:StartFaceLivenessSession", + "Resource": "*" + } + ] + } + ``` + + 4. Choose **Review Policy** + 5. Name the policy + 6. Choose **Create Policy** + +6. Set up a backend to create the liveness session and retrieve the liveness session results. The liveness sample app is set up to use API Gateway endpoints for creating and retrieving the liveness session. Follow the [Amazon Rekognition Liveness guide](https://docs.aws.amazon.com/rekognition/latest/dg/face-liveness-programming-api.html) to set up your backend. + + 1. Edit the [StartSessionViewModel.swift](https://github.com/aws-amplify/amplify-ui-swift-liveness/blob/main/HostApp/HostApp/Views/StartSessionViewModel.swift) in your project as necessary to generate the session id from your backend. + 2. Edit the [ExampleLivenessViewModel.swift](https://github.com/aws-amplify/amplify-ui-swift-liveness/blob/main/HostApp/HostApp/Views/ExampleLivenessViewModel.swift) in your project as necessary to get the liveness result from your backend. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..67db8588 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..616fc588 --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..d736e668 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,68 @@ +{ + "pins" : [ + { + "identity" : "amplify-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift", + "state" : { + "revision" : "7b1d5ee05a23ea24c7458dffed1c563c8501042d", + "version" : "2.45.4" + } + }, + { + "identity" : "amplify-swift-utils-notifications", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", + "state" : { + "revision" : "959eec669ba97c7d923b963c3e66ca8a0b2737f6", + "version" : "1.1.1" + } + }, + { + "identity" : "aws-crt-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-crt-swift", + "state" : { + "revision" : "3f844bef042cc0a4c3381f7090414ce3f9a7e935", + "version" : "0.37.0" + } + }, + { + "identity" : "aws-sdk-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-sdk-swift", + "state" : { + "revision" : "c6c1064da9bfccb119a7a8ab9ba636fb3bbfa6f5", + "version" : "1.0.47" + } + }, + { + "identity" : "smithy-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/smithy-lang/smithy-swift", + "state" : { + "revision" : "3cd9f181b3ba8ff71da43bf53c09f8de6790a4ad", + "version" : "0.96.0" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version" : "1.6.2" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..a302e437 --- /dev/null +++ b/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AmplifyUILiveness", + defaultLocalization: "en", + platforms: [.iOS(.v14)], + products: [ + .library( + name: "FaceLiveness", + targets: ["FaceLiveness"]), + ], + dependencies: [ + .package(url: "https://github.com/aws-amplify/amplify-swift", exact: "2.45.4") + ], + targets: [ + .target( + name: "FaceLiveness", + dependencies: [ + .product(name: "AWSPluginsCore", package: "amplify-swift"), + .product(name: "AWSCognitoAuthPlugin", package: "amplify-swift"), + .product(name: "AWSPredictionsPlugin", package: "amplify-swift") + ], + resources: [ + .process("Resources/Base.lproj"), + .copy("Resources/face_detection_short_range.mlmodelc") + ] + ), + .testTarget( + name: "FaceLivenessTests", + dependencies: ["FaceLiveness"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 00000000..8b41a81f --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +AWS Amplify + +--- + +# Amplify UI Liveness for SwiftUI + +[![GitHub](https://img.shields.io/github/license/aws-amplify/amplify-ui-swift-liveness)](LICENSE) +[![Discord](https://img.shields.io/discord/308323056592486420?logo=discord)](https://discord.gg/jWVbPfC) +[![Open Bugs](https://img.shields.io/github/issues/aws-amplify/amplify-ui-swift-liveness/bug?color=d73a4a&label=bugs)](https://github.com/aws-amplify/amplify-ui-swift-liveness/issues?q=is%3Aissue+is%3Aopen+label%3Abug) +[![Feature Requests](https://img.shields.io/github/issues/aws-amplify/amplify-ui-swift-liveness/feature-request?color=ff9001&label=feature%20requests)](https://github.com/aws-amplify/amplify-ui-swift-liveness/issues?q=is%3Aissue+label%3Afeature-request+is%3Aopen) + +Amplify UI FaceLivenessDetector provides a UI component for Amazon Rekognition Face Liveness that helps developers verify that only real users, not bad actors using spoofs, can access your services. + +More information on setting up and using the FaceLivenessDetector is in the [Amplify UI Face Liveness documentation](https://ui.docs.amplify.aws/swift/connected-components/liveness). + +## Contributing + +- [CONTRIBUTING.md](/CONTRIBUTING.md) + +## Security + +See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. + +## License + +This project is licensed under the Apache-2.0 License. diff --git a/Sources/FaceLiveness/AV/LivenessAVAssetWriter.swift b/Sources/FaceLiveness/AV/LivenessAVAssetWriter.swift new file mode 100644 index 00000000..f0a024be --- /dev/null +++ b/Sources/FaceLiveness/AV/LivenessAVAssetWriter.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation + +final class LivenessAVAssetWriter: AVAssetWriter { + init() { + super.init(contentType: .mpeg4Movie) + outputFileTypeProfile = .mpeg4CMAFCompliant + preferredOutputSegmentInterval = CMTime( + seconds: 1, + preferredTimescale: 1 + ) + initialSegmentStartTime = CMTime(seconds: 0, preferredTimescale: 240) + } +} diff --git a/Sources/FaceLiveness/AV/LivenessAVAssetWriterInput.swift b/Sources/FaceLiveness/AV/LivenessAVAssetWriterInput.swift new file mode 100644 index 00000000..22dc5b7f --- /dev/null +++ b/Sources/FaceLiveness/AV/LivenessAVAssetWriterInput.swift @@ -0,0 +1,27 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation + +fileprivate let videoWidth = 480 +fileprivate let videoHeight = 640 +fileprivate let bitRate = 1_000_000 + +class LivenessAVAssetWriterInput: AVAssetWriterInput { + init() { + super.init( + mediaType: .video, + outputSettings: [ + AVVideoCodecKey: AVVideoCodecType.h264, + AVVideoWidthKey: videoWidth, + AVVideoHeightKey: videoHeight, + AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: bitRate] + ], + sourceFormatHint: nil + ) + } +} diff --git a/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift b/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift new file mode 100644 index 00000000..1d67e913 --- /dev/null +++ b/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift @@ -0,0 +1,39 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation + +struct LivenessCaptureDevice { + let avCaptureDevice: AVCaptureDevice? + var preset: AVCaptureSession.Preset = .vga640x480 + var fps: Double = 30 + var exposure: AVCaptureDevice.ExposureMode = .continuousAutoExposure + var whiteBalance: AVCaptureDevice.WhiteBalanceMode = .continuousAutoWhiteBalance + var focus: AVCaptureDevice.FocusMode = .continuousAutoFocus + + func configure() throws { + guard let avCaptureDevice else { throw LivenessCaptureSessionError.cameraUnavailable } + try avCaptureDevice.lockForConfiguration() + defer { avCaptureDevice.unlockForConfiguration() } + + let fps = CMTimeScale(fps) + let frameDuration = CMTime(value: 1, timescale: fps) + avCaptureDevice.activeVideoMinFrameDuration = frameDuration + avCaptureDevice.activeVideoMaxFrameDuration = frameDuration + if avCaptureDevice.isExposureModeSupported(exposure) { + avCaptureDevice.exposureMode = exposure + } + + if avCaptureDevice.isFocusModeSupported(focus) { + avCaptureDevice.focusMode = focus + } + + if avCaptureDevice.isWhiteBalanceModeSupported(whiteBalance) { + avCaptureDevice.whiteBalanceMode = whiteBalance + } + } +} diff --git a/Sources/FaceLiveness/AV/LivenessCaptureSession.swift b/Sources/FaceLiveness/AV/LivenessCaptureSession.swift new file mode 100644 index 00000000..ee8db673 --- /dev/null +++ b/Sources/FaceLiveness/AV/LivenessCaptureSession.swift @@ -0,0 +1,140 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import AVFoundation + +class LivenessCaptureSession { + let captureDevice: LivenessCaptureDevice + private let captureQueue = DispatchQueue(label: "com.amazonaws.faceliveness.cameracapturequeue") + private let configurationQueue = DispatchQueue(label: "com.amazonaws.faceliveness.sessionconfiguration", qos: .userInteractive) + let outputDelegate: AVCaptureVideoDataOutputSampleBufferDelegate + var captureSession: AVCaptureSession? + + var outputSampleBufferCapturer: OutputSampleBufferCapturer? { + return outputDelegate as? OutputSampleBufferCapturer + } + + init(captureDevice: LivenessCaptureDevice, outputDelegate: AVCaptureVideoDataOutputSampleBufferDelegate) { + self.captureDevice = captureDevice + self.outputDelegate = outputDelegate + } + + func configureCamera(frame: CGRect) throws -> CALayer { + try configureCamera() + + guard let captureSession = captureSession else { + throw LivenessCaptureSessionError.captureSessionUnavailable + } + + let previewLayer = previewLayer( + frame: frame, + for: captureSession + ) + + return previewLayer + } + + func configureCamera() throws { + guard let camera = captureDevice.avCaptureDevice + else { throw LivenessCaptureSessionError.cameraUnavailable } + + let cameraInput = try AVCaptureDeviceInput(device: camera) + let videoOutput = AVCaptureVideoDataOutput() + + teardownExistingSession(input: cameraInput) + captureSession = AVCaptureSession() + + guard let captureSession = captureSession else { + throw LivenessCaptureSessionError.captureSessionUnavailable + } + + try setupInput(cameraInput, for: captureSession) + captureSession.sessionPreset = captureDevice.preset + try setupOutput(videoOutput, for: captureSession) + try captureDevice.configure() + + videoOutput.setSampleBufferDelegate( + outputDelegate, + queue: captureQueue + ) + } + + func startSession() { + guard let session = captureSession else { return } + configurationQueue.async { + session.startRunning() + } + } + + func stopRunning() { + guard let session = captureSession else { return } + defer { + captureSession = nil + } + + if session.isRunning { + session.stopRunning() + } + + for input in session.inputs { + session.removeInput(input) + } + + for output in session.outputs { + session.removeOutput(output) + } + } + + private func teardownExistingSession(input: AVCaptureDeviceInput) { + stopRunning() + captureSession?.removeInput(input) + } + + private func setupInput( + _ input: AVCaptureDeviceInput, + for captureSession: AVCaptureSession + ) throws { + if captureSession.canAddInput(input) { + captureSession.addInput(input) + } else { + throw LivenessCaptureSessionError.captureSessionInputUnavailable + } + } + + private func setupOutput( + _ output: AVCaptureVideoDataOutput, + for captureSession: AVCaptureSession + ) throws { + if captureSession.canAddOutput(output) { + captureSession.addOutput(output) + } else { + throw LivenessCaptureSessionError.captureSessionOutputUnavailable + } + output.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA + ] + + output.connections + .filter(\.isVideoOrientationSupported) + .forEach { + $0.videoOrientation = .portrait + $0.isVideoMirrored = true + } + } + + private func previewLayer( + frame: CGRect, + for captureSession: AVCaptureSession + ) -> AVCaptureVideoPreviewLayer { + let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.videoGravity = .resizeAspectFill + previewLayer.connection?.videoOrientation = .portrait + previewLayer.frame = frame + return previewLayer + } +} diff --git a/Sources/FaceLiveness/AV/LivenessCaptureSessionError.swift b/Sources/FaceLiveness/AV/LivenessCaptureSessionError.swift new file mode 100644 index 00000000..58ca3d21 --- /dev/null +++ b/Sources/FaceLiveness/AV/LivenessCaptureSessionError.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct LivenessCaptureSessionError: Error, Equatable { + let code: UInt8 + + static let deviceInputUnavailable = Self(code: 1) + static let cameraUnavailable = Self(code: 2) + static let captureSessionUnavailable = Self(code: 3) + static let captureSessionInputUnavailable = Self(code: 5) + static let captureSessionOutputUnavailable = Self(code: 6) +} diff --git a/Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift b/Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift new file mode 100644 index 00000000..23f6defb --- /dev/null +++ b/Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation +import CoreImage + +class OutputSampleBufferCapturer: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { + let faceDetector: FaceDetector + let videoChunker: VideoChunker + + init(faceDetector: FaceDetector, videoChunker: VideoChunker) { + self.faceDetector = faceDetector + self.videoChunker = videoChunker + } + + func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { + videoChunker.consume(sampleBuffer) + + guard let imageBuffer = sampleBuffer.imageBuffer + else { return } + + faceDetector.detectFaces(from: imageBuffer) + } +} diff --git a/Sources/FaceLiveness/AV/VideoChunkProcessor.swift b/Sources/FaceLiveness/AV/VideoChunkProcessor.swift new file mode 100644 index 00000000..bd2e6151 --- /dev/null +++ b/Sources/FaceLiveness/AV/VideoChunkProcessor.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +protocol VideoSegmentProcessor: AnyObject { + func process(initalSegment: Data, currentSeparableSegment: Data) +} diff --git a/Sources/FaceLiveness/AV/VideoChunker+AssetWriterDelegate.swift b/Sources/FaceLiveness/AV/VideoChunker+AssetWriterDelegate.swift new file mode 100644 index 00000000..18ad59af --- /dev/null +++ b/Sources/FaceLiveness/AV/VideoChunker+AssetWriterDelegate.swift @@ -0,0 +1,40 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation + +extension VideoChunker { + class AssetWriterDelegate: NSObject, AVAssetWriterDelegate { + private var initialSegmentData: Data? + private weak var segmentProcessor: VideoSegmentProcessor? + + func set(segmentProcessor: VideoSegmentProcessor) { + self.segmentProcessor = segmentProcessor + } + + func assetWriter( + _ writer: AVAssetWriter, + didOutputSegmentData segmentData: Data, + segmentType: AVAssetSegmentType, + segmentReport: AVAssetSegmentReport? + ) { + if segmentType == .initialization { + assert(initialSegmentData == nil, "Received second initialization segment.") + initialSegmentData = segmentData + return + } + + guard let initialSegmentData else { + return assertionFailure( + "Received seperable segment before receiving initialization segment." + ) + } + + segmentProcessor?.process(initalSegment: initialSegmentData, currentSeparableSegment: segmentData) + } + } +} diff --git a/Sources/FaceLiveness/AV/VideoChunker+State.swift b/Sources/FaceLiveness/AV/VideoChunker+State.swift new file mode 100644 index 00000000..7996a598 --- /dev/null +++ b/Sources/FaceLiveness/AV/VideoChunker+State.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension VideoChunker { + enum State { + case pending + case writing + case awaitingSingleFrame + case complete + } +} diff --git a/Sources/FaceLiveness/AV/VideoChunker.swift b/Sources/FaceLiveness/AV/VideoChunker.swift new file mode 100644 index 00000000..962cb798 --- /dev/null +++ b/Sources/FaceLiveness/AV/VideoChunker.swift @@ -0,0 +1,83 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation +import CoreImage +import UIKit + +final class VideoChunker { + var state = State.pending + let assetWriter: AVAssetWriter + let assetWriterDelegate: AssetWriterDelegate + let assetWriterInput: AVAssetWriterInput + let pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor + var startTimeSeconds: Double? + var provideSingleFrame: ((UIImage) -> Void)? + + init( + assetWriter: AVAssetWriter, + assetWriterDelegate: AssetWriterDelegate, + assetWriterInput: AVAssetWriterInput + ) { + self.assetWriter = assetWriter + self.assetWriterDelegate = assetWriterDelegate + self.assetWriterInput = assetWriterInput + self.pixelBufferAdaptor = .init(assetWriterInput: assetWriterInput) + self.assetWriterInput.expectsMediaDataInRealTime = true + self.assetWriter.delegate = assetWriterDelegate + self.assetWriter.add(assetWriterInput) + } + + func start() { + guard state == .pending else { return } + assetWriter.startWriting() + assetWriter.startSession(atSourceTime: .zero) + state = .writing + } + + func finish(singleFrame: @escaping (UIImage) -> Void) { + self.provideSingleFrame = singleFrame + state = .awaitingSingleFrame + + // explicitly calling `endSession` is unnecessary + if assetWriter.status != .completed { + assetWriter.finishWriting {} + } + } + + func consume(_ buffer: CMSampleBuffer) { + if state == .awaitingSingleFrame { + guard let imageBuffer = buffer.imageBuffer else { return } + let singleFrame = singleFrame(from: imageBuffer) + provideSingleFrame?(singleFrame) + state = .complete + } + + guard state == .writing else { return } + + if assetWriterInput.isReadyForMoreMediaData { + let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer).seconds + if startTimeSeconds == nil { startTimeSeconds = timestamp } + guard let startTimeSeconds else { + return + } + let presentationTime = CMTime(seconds: timestamp - startTimeSeconds, preferredTimescale: 600) + guard let imageBuffer = buffer.imageBuffer else { return } + + pixelBufferAdaptor.append( + imageBuffer, + withPresentationTime: presentationTime + ) + } + } + + private func singleFrame(from buffer: CVPixelBuffer) -> UIImage { + let ciImage = CIImage(cvPixelBuffer: buffer) + let uiImage = UIImage(ciImage: ciImage) + return uiImage + } +} diff --git a/Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift b/Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift new file mode 100644 index 00000000..d6879848 --- /dev/null +++ b/Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift @@ -0,0 +1,93 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct DetectedFace { + var boundingBox: CGRect + + let leftEye: CGPoint + let rightEye: CGPoint + let nose: CGPoint + let mouth: CGPoint + let rightEar: CGPoint + let leftEar: CGPoint + + let confidence: Float + + func boundingBoxFromLandmarks(ovalRect: CGRect) -> CGRect { + let alpha = 2.0 + let gamma = 1.8 + let ow = (alpha * pupilDistance + gamma * faceHeight) / 2 + var cx = (eyeCenterX + nose.x) / 2 + + if ovalRect != CGRect.zero { + let ovalTop = ovalRect.minY + let ovalHeight = ovalRect.maxY - ovalRect.minY + if eyeCenterY > (ovalTop + ovalHeight) / 2 { + cx = eyeCenterX + } + } + + let faceWidth = ow + let faceHeight = 1.618 * faceWidth + let faceBoxBottom = boundingBox.maxY + let faceBoxTop = faceBoxBottom - faceHeight + let faceBoxLeft = min(cx - ow / 2, rightEar.x) + let faceBoxRight = max(cx + ow / 2, leftEar.x) + let width = faceBoxRight - faceBoxLeft + let height = faceBoxBottom - faceBoxTop + let rect = CGRect(x: faceBoxLeft, y: faceBoxTop, width: width, height: height) + return rect + } + + var faceDistance: CGFloat { + sqrt(pow(rightEye.x - leftEye.x, 2) + pow(rightEye.y - leftEye.y, 2)) + } + + var pupilDistance: CGFloat { + sqrt(pow(leftEye.x - rightEye.x, 2) + pow(leftEye.y - rightEye.y, 2)) + } + + var eyeCenterX: CGFloat { + (leftEye.x + rightEye.x) / 2 + } + + var eyeCenterY: CGFloat { + (leftEye.y + rightEye.y) / 2 + } + + var faceHeight: CGFloat { + sqrt(pow(eyeCenterX - mouth.x, 2) + pow(eyeCenterY - mouth.y, 2)) + } + + func normalize(width: CGFloat, height: CGFloat) -> DetectedFace { + let boundingBox = CGRect( + x: boundingBox.minX * width, + y: boundingBox.minY * height, + width: boundingBox.width * width, + height: boundingBox.height * height + ) + let leftEye = CGPoint(x: leftEye.x * width, y: leftEye.y * height) + let rightEye = CGPoint(x: rightEye.x * width, y: rightEye.y * height) + let nose = CGPoint(x: nose.x * width, y: nose.y * height) + let mouth = CGPoint(x: mouth.x * width, y: mouth.y * height) + let rightEar = CGPoint(x: rightEar.x * width, y: rightEar.y * height) + let leftEar = CGPoint(x: leftEar.x * width, y: leftEar.y * height) + + return DetectedFace( + boundingBox: boundingBox, + leftEye: leftEye, + rightEye: rightEye, + nose: nose, + mouth: mouth, + rightEar: rightEar, + leftEar: leftEar, + confidence: confidence + ) + } +} diff --git a/Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift b/Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift new file mode 100644 index 00000000..d9430720 --- /dev/null +++ b/Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift @@ -0,0 +1,214 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Vision +import CoreML +import Accelerate +import CoreGraphics +import CoreImage +import VideoToolbox + +enum FaceDetectorShortRange {} + +extension FaceDetectorShortRange { + final class Model: FaceDetector { + var model: MLModel + let confidenceScoreThreshold: Float = 0.7 + let weightedNonMaxSuppressionThreshold: Float = 0.3 + + init(_ model: MLModel) { + self.model = model + } + + convenience init() throws { + try self.init( + face_detection_short_range( + configuration: .init() + ).model + ) + } + + weak var detectionResultHandler: FaceDetectionResultHandler? + + func setResultHandler(detectionResultHandler: FaceDetectionResultHandler) { + self.detectionResultHandler = detectionResultHandler + } + + func detectFaces(from buffer: CVPixelBuffer) { + let faces = prediction(for: buffer) + let observationResult: FaceDetectionResult + switch faces.count { + case 1: + observationResult = .singleFace(faces[0]) + case 2...: + observationResult = .multipleFaces + default: + observationResult = .noFace + } + + detectionResultHandler?.process(newResult: observationResult) + } + + func prediction(for buffer: CVPixelBuffer) -> [DetectedFace] { + var image: CGImage? + VTCreateCGImageFromCVPixelBuffer(buffer, options: nil, imageOut: &image) + guard let image = image else { return [] } + let imageHeight = Float32(image.height) + let imageWidth = Float32(image.width) + + let heightScale = max(imageHeight, imageWidth) / imageHeight + let widthScale = max(imageHeight, imageWidth) / imageWidth + let simdScale = SIMD2(Double(widthScale), Double(heightScale)) + let simdShift = SIMD2(Double(widthScale - 1) / 2, Double(heightScale - 1) / 2) + + guard let input = try? face_detection_short_rangeInput(imageWith: image) else { + return [] + } + + guard let output = try? model.prediction(from: input) else { + return [] + } + + guard let landmarksMultiArray = output.featureValue(for: "1477")?.multiArrayValue else { return [] } + + let landmarksCapacity = landmarksMultiArray.count / 16 + + let boundLandmarks = landmarksMultiArray.dataPointer.bindMemory( + to: SIMD16.self, + capacity: landmarksCapacity + ) + + let landmarks = [SIMD16]( + UnsafeBufferPointer( + start: boundLandmarks, + count: landmarksCapacity + ) + ) + + guard let confidenceScoresMultiArray = output.featureValue(for: "1011")?.multiArrayValue else { return [] } + + let confidenceScoresCapacity = confidenceScoresMultiArray.count + + let boundConfidenceScores = confidenceScoresMultiArray.dataPointer.bindMemory( + to: Float32.self, + capacity: confidenceScoresCapacity + ) + + let confidenceScores = [Float32]( + UnsafeBufferPointer( + start: boundConfidenceScores, + count: confidenceScoresCapacity + ) + ) + + var passingConfidenceScoresIndices = confidenceScores + .enumerated() + .filter { $0.element >= confidenceScoreThreshold } + .sorted(by: { + $0.element > $1.element + }) + .map(\.offset) + + var faces = [DetectedFace]() + + while passingConfidenceScoresIndices.count > 0 { + var overlappingOutputs = [SIMD16]() + var overlappingConfidenceScore = Float32(0) + var nonOverlappingIndices = [Int]() + for index in 0..= weightedNonMaxSuppressionThreshold { + overlappingOutputs.append( + confidenceScores[passingConfidenceScoresIndices[index]] * landmarks[passingConfidenceScoresIndices[index]] + ) + overlappingConfidenceScore += confidenceScores[passingConfidenceScoresIndices[index]] + } else { + nonOverlappingIndices.append(passingConfidenceScoresIndices[index]) + } + } + + passingConfidenceScoresIndices = nonOverlappingIndices + let averageResult = overlappingOutputs.reduce(SIMD16(repeating: 0), +) / overlappingConfidenceScore + + var faceResult = [SIMD2]() + for i in 0..<8 { + let faceL = SIMD2( + Double(averageResult[2 * i]), + Double(averageResult[2 * i + 1]) + ) * simdScale - simdShift + faceResult.append(faceL) + } + + let minX = faceResult[0].x + let minY = faceResult[0].y + let maxX = faceResult[1].x + let maxY = faceResult[1].y + let rightEye = faceResult[2] + let leftEye = faceResult[3] + let nose = faceResult[4] + let mouth = faceResult[5] + let rightEar = faceResult[6] + let leftEar = faceResult[7] + + + + let boundingBox = CGRect( + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + ) + + let face = DetectedFace( + boundingBox: boundingBox, + leftEye: .init(x: leftEye.x, y: leftEye.y), + rightEye: .init(x: rightEye.x, y: rightEye.y), + nose: .init(x: nose.x, y: nose.y), + mouth: .init(x: mouth.x, y: mouth.y), + rightEar: .init(x: rightEar.x, y: rightEar.y), + leftEar: .init(x: leftEar.x, y: leftEar.y), + confidence: overlappingConfidenceScore / Float(overlappingOutputs.count) + ) + + faces.append(face) + } + + return faces + } + + func intersectionOverUnion(_ b1: SIMD16, _ b2: SIMD16) -> Float { + func points(for box: SIMD16) -> (minX: Float, minY: Float, maxX: Float, maxY: Float) { + (box[1], box[0], box[3], box[2]) + } + + let b1 = points(for: b1) + let b2 = points(for: b2) + + let areaB1 = (b1.maxY - b1.minY) * (b1.maxX - b1.minX) + if areaB1 <= 0 { return 0 } + + let areaB2 = (b2.maxY - b2.minY) * (b2.maxX - b2.minX) + if areaB2 <= 0 { return 0 } + + let intersectionMinX = max(b1.minX, b2.minX) + let intersectionMinY = max(b1.minY, b2.minY) + let intersectionMaxX = min(b1.maxX, b2.maxX) + let intersectionMaxY = min(b1.maxY, b2.maxY) + + let intersectionArea = max(0, intersectionMaxY - intersectionMinY) * + max(0, intersectionMaxX - intersectionMinX) + + return intersectionArea / (areaB1 + areaB2 - intersectionArea) + } + } +} diff --git a/Sources/FaceLiveness/FaceDetection/BlazeFace/face_detection_short_range.swift b/Sources/FaceLiveness/FaceDetection/BlazeFace/face_detection_short_range.swift new file mode 100644 index 00000000..147f8905 --- /dev/null +++ b/Sources/FaceLiveness/FaceDetection/BlazeFace/face_detection_short_range.swift @@ -0,0 +1,300 @@ +// +// face_detection_short_range.swift +// +// This file was automatically generated and should not be edited. +// + +import CoreML + + +/// Model Prediction Input Type +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public class face_detection_short_rangeInput : MLFeatureProvider { + + /// image as color (kCVPixelFormatType_32BGRA) image buffer, 128 pixels wide by 128 pixels high + public var image: CVPixelBuffer + + public var featureNames: Set { + get { + return ["image"] + } + } + + public func featureValue(for featureName: String) -> MLFeatureValue? { + if (featureName == "image") { + return MLFeatureValue(pixelBuffer: image) + } + return nil + } + + public init(image: CVPixelBuffer) { + self.image = image + } + + public convenience init(imageWith image: CGImage) throws { + self.init(image: try MLFeatureValue(cgImage: image, pixelsWide: 128, pixelsHigh: 128, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!) + } + + public convenience init(imageAt image: URL) throws { + self.init(image: try MLFeatureValue(imageAt: image, pixelsWide: 128, pixelsHigh: 128, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!) + } + + public func setImage(with image: CGImage) throws { + self.image = try MLFeatureValue(cgImage: image, pixelsWide: 128, pixelsHigh: 128, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue! + } + + public func setImage(with image: URL) throws { + self.image = try MLFeatureValue(imageAt: image, pixelsWide: 128, pixelsHigh: 128, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue! + } + +} + + +/// Model Prediction Output Type +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public class face_detection_short_rangeOutput : MLFeatureProvider { + + /// Source provided by CoreML + private let provider : MLFeatureProvider + + /// 1477 as multidimensional array of floats + public var _1477: MLMultiArray { + return self.provider.featureValue(for: "1477")!.multiArrayValue! + } + + /// 1477 as multidimensional array of floats + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + public var _1477ShapedArray: MLShapedArray { + return MLShapedArray(self._1477) + } + + /// 1011 as multidimensional array of floats + public var _1011: MLMultiArray { + return self.provider.featureValue(for: "1011")!.multiArrayValue! + } + + /// 1011 as multidimensional array of floats + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + public var _1011ShapedArray: MLShapedArray { + return MLShapedArray(self._1011) + } + + public var featureNames: Set { + return self.provider.featureNames + } + + public func featureValue(for featureName: String) -> MLFeatureValue? { + return self.provider.featureValue(for: featureName) + } + + public init(_1477: MLMultiArray, _1011: MLMultiArray) { + self.provider = try! MLDictionaryFeatureProvider(dictionary: ["1477" : MLFeatureValue(multiArray: _1477), "1011" : MLFeatureValue(multiArray: _1011)]) + } + + public init(features: MLFeatureProvider) { + self.provider = features + } +} + + +/// Class for model loading and prediction +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public class face_detection_short_range { + public let model: MLModel + + /// URL of model assuming it was installed in the same bundle as this class + class var urlOfModelInThisBundle : URL { + let bundle = Bundle.module + return bundle.url(forResource: "face_detection_short_range", withExtension:"mlmodelc")! + } + + /** + Construct face_detection_short_range instance with an existing MLModel object. + + Usually the application does not use this initializer unless it makes a subclass of face_detection_short_range. + Such application may want to use `MLModel(contentsOfURL:configuration:)` and `face_detection_short_range.urlOfModelInThisBundle` to create a MLModel object to pass-in. + + - parameters: + - model: MLModel object + */ + init(model: MLModel) { + self.model = model + } + + /** + Construct face_detection_short_range instance by automatically loading the model from the app's bundle. + */ + @available(*, deprecated, message: "Use init(configuration:) instead and handle errors appropriately.") + public convenience init() { + try! self.init(contentsOf: type(of:self).urlOfModelInThisBundle) + } + + /** + Construct a model with configuration + + - parameters: + - configuration: the desired model configuration + + - throws: an NSError object that describes the problem + */ + public convenience init(configuration: MLModelConfiguration) throws { + try self.init(contentsOf: type(of:self).urlOfModelInThisBundle, configuration: configuration) + } + + /** + Construct face_detection_short_range instance with explicit path to mlmodelc file + - parameters: + - modelURL: the file url of the model + + - throws: an NSError object that describes the problem + */ + public convenience init(contentsOf modelURL: URL) throws { + try self.init(model: MLModel(contentsOf: modelURL)) + } + + /** + Construct a model with URL of the .mlmodelc directory and configuration + + - parameters: + - modelURL: the file url of the model + - configuration: the desired model configuration + + - throws: an NSError object that describes the problem + */ + public convenience init(contentsOf modelURL: URL, configuration: MLModelConfiguration) throws { + try self.init(model: MLModel(contentsOf: modelURL, configuration: configuration)) + } + + /** + Construct face_detection_short_range instance asynchronously with optional configuration. + + Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread. + + - parameters: + - configuration: the desired model configuration + - handler: the completion handler to be called when the model loading completes successfully or unsuccessfully + */ + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) + public class func load(configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result) -> Void) { + return self.load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration, completionHandler: handler) + } + + /** + Construct face_detection_short_range instance asynchronously with optional configuration. + + Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread. + + - parameters: + - configuration: the desired model configuration + */ + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + public class func load(configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> face_detection_short_range { + return try await self.load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration) + } + + /** + Construct face_detection_short_range instance asynchronously with URL of the .mlmodelc directory with optional configuration. + + Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread. + + - parameters: + - modelURL: the URL to the model + - configuration: the desired model configuration + - handler: the completion handler to be called when the model loading completes successfully or unsuccessfully + */ + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) + public class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result) -> Void) { + MLModel.load(contentsOf: modelURL, configuration: configuration) { result in + switch result { + case .failure(let error): + handler(.failure(error)) + case .success(let model): + handler(.success(face_detection_short_range(model: model))) + } + } + } + + /** + Construct face_detection_short_range instance asynchronously with URL of the .mlmodelc directory with optional configuration. + + Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread. + + - parameters: + - modelURL: the URL to the model + - configuration: the desired model configuration + */ + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + public class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> face_detection_short_range { + let model = try await MLModel.load(contentsOf: modelURL, configuration: configuration) + return face_detection_short_range(model: model) + } + + /** + Make a prediction using the structured interface + + - parameters: + - input: the input to the prediction as face_detection_short_rangeInput + + - throws: an NSError object that describes the problem + + - returns: the result of the prediction as face_detection_short_rangeOutput + */ + public func prediction(input: face_detection_short_rangeInput) throws -> face_detection_short_rangeOutput { + return try self.prediction(input: input, options: MLPredictionOptions()) + } + + /** + Make a prediction using the structured interface + + - parameters: + - input: the input to the prediction as face_detection_short_rangeInput + - options: prediction options + + - throws: an NSError object that describes the problem + + - returns: the result of the prediction as face_detection_short_rangeOutput + */ + public func prediction(input: face_detection_short_rangeInput, options: MLPredictionOptions) throws -> face_detection_short_rangeOutput { + let outFeatures = try model.prediction(from: input, options:options) + return face_detection_short_rangeOutput(features: outFeatures) + } + + /** + Make a prediction using the convenience interface + + - parameters: + - image as color (kCVPixelFormatType_32BGRA) image buffer, 128 pixels wide by 128 pixels high + + - throws: an NSError object that describes the problem + + - returns: the result of the prediction as face_detection_short_rangeOutput + */ + public func prediction(image: CVPixelBuffer) throws -> face_detection_short_rangeOutput { + let input_ = face_detection_short_rangeInput(image: image) + return try self.prediction(input: input_) + } + + /** + Make a batch prediction using the structured interface + + - parameters: + - inputs: the inputs to the prediction as [face_detection_short_rangeInput] + - options: prediction options + + - throws: an NSError object that describes the problem + + - returns: the result of the prediction as [face_detection_short_rangeOutput] + */ + public func predictions(inputs: [face_detection_short_rangeInput], options: MLPredictionOptions = MLPredictionOptions()) throws -> [face_detection_short_rangeOutput] { + let batchIn = MLArrayBatchProvider(array: inputs) + let batchOut = try model.predictions(from: batchIn, options: options) + var results : [face_detection_short_rangeOutput] = [] + results.reserveCapacity(inputs.count) + for i in 0.. Instructor.Instruction { + guard let oval = ovalRect else { + return .none + } + + let intersection = intersectionOverUnion(boxA: face, boxB: oval) + let thresholds = Thresholds(oval: oval, challengeConfig: challengeConfig) + + if storage.initialIOU == nil { + storage.initialIOU = intersection + } + + let initialIOU = storage.initialIOU! + + let faceMatchPercentage = calculateFaceMatchPercentage( + intersection: intersection, + initialIOU: initialIOU, + thresholds: thresholds + ) + + let update: Instructor.Instruction + + if isMatch(face: face, oval: oval, intersection: intersection, thresholds: thresholds) { + update = .match + } else if isTooClose(face: face, oval: oval, intersection: intersection, thresholds: thresholds) { + update = .tooClose(nearnessPercentage: faceMatchPercentage) + } else { + update = .tooFar(nearnessPercentage: faceMatchPercentage) + } + + let instruction = instructor.instruction(for: update) + return instruction + } + + private func isTooClose(face: CGRect, oval: CGRect, intersection: Double, thresholds: Thresholds) -> Bool { + oval.minY - face.minY > thresholds.faceDetectionHeight + || face.maxY - oval.maxY > thresholds.faceDetectionHeight + || (oval.minX - face.minX > thresholds.faceDetectionWidth && face.maxX - oval.maxX > thresholds.faceDetectionWidth) + } + + private func isMatch(face: CGRect, oval: CGRect, intersection: Double, thresholds: Thresholds) -> Bool { + intersection > thresholds.intersection + && abs(oval.minX - face.minX) < thresholds.ovalMatchWidth + && abs(oval.maxX - face.maxX) < thresholds.ovalMatchWidth + && abs(oval.maxY - face.maxY) < thresholds.ovalMatchHeight + } + + private func calculateFaceMatchPercentage(intersection: Double, initialIOU: Double, thresholds: Thresholds) -> Double { + var faceMatchPercentage = (0.75 * (intersection - initialIOU)) / (thresholds.intersection - initialIOU) + 0.25 + faceMatchPercentage = max(min(1, faceMatchPercentage), 0) + return faceMatchPercentage + } + + private func intersectionOverUnion(boxA: CGRect, boxB: CGRect) -> Double { + let xA = max(boxA.minX, boxB.minX) + let yA = max(boxA.minY, boxB.minY) + let xB = min(boxA.maxX, boxB.maxX) + let yB = min(boxA.maxY, boxB.maxY) + + let intersectionArea = abs(max(0, xB - xA) * max(0, yB - yA)) + if intersectionArea == 0 { return 0 } + + let boxAArea = (boxA.maxY - boxA.minY) * (boxA.maxX - boxA.minX) + let boxBArea = (boxB.maxY - boxB.minY) * (boxB.maxX - boxB.minX) + + return intersectionArea / (boxAArea + boxBArea - intersectionArea) + } +} + +extension FaceInOvalMatching { + struct Thresholds { + let intersection: Double + let ovalMatchWidth: Double + let ovalMatchHeight: Double + let faceDetectionWidth: Double + let faceDetectionHeight: Double + + init(oval: CGRect, challengeConfig: FaceLivenessSession.OvalMatchChallenge) { + intersection = challengeConfig.oval.iouThreshold + ovalMatchWidth = oval.width * challengeConfig.oval.iouWidthThreshold + ovalMatchHeight = oval.height * challengeConfig.oval.iouHeightThreshold + faceDetectionWidth = oval.width * challengeConfig.face.iouWidthThreshold + faceDetectionHeight = oval.height * challengeConfig.face.iouHeightThreshold + } + } +} diff --git a/Sources/FaceLiveness/FaceDetection/Instructor.swift b/Sources/FaceLiveness/FaceDetection/Instructor.swift new file mode 100644 index 00000000..c8f7c53e --- /dev/null +++ b/Sources/FaceLiveness/FaceDetection/Instructor.swift @@ -0,0 +1,67 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +class Instructor { + init( + previousInstruction: Instructor.Instruction? = nil, + runningCount: Int = 0 + ) { + self.previousInstruction = previousInstruction + self.runningCount = runningCount + } + + enum Instruction: Equatable { + case `match` + case tooFarLeft( + text: String = "Move head right", + nearnessPercentage: Double + ) + case tooFarRight( + text: String = "Move head left", + nearnessPercentage: Double + ) + case tooClose( + text: String = "Move head farther.", + nearnessPercentage: Double + ) + case tooFar( + text: String = "Move head closer.", + nearnessPercentage: Double + ) + case none + + static func == (lhs: Instruction, rhs: Instruction) -> Bool { + switch (lhs, rhs) { + case (.match, .match): return true + case (.tooFarLeft, .tooFarLeft): return true + case (.tooFarRight, .tooFarRight): return true + case (.tooClose, .tooClose): return true + case (.tooFar, .tooFar): return true + default: return false + } + } + } + + var previousInstruction: Instruction? + var runningCount = 0 + + func instruction(for update: Instruction) -> Instruction { + if previousInstruction == update { + runningCount += 1 + if runningCount >= 15 { + return update + } + return .none + } else { + previousInstruction = update + runningCount = 0 + } + return .none + } +} diff --git a/Sources/FaceLiveness/Resources/Assets.xcassets/Contents.json b/Sources/FaceLiveness/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Sources/FaceLiveness/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/FaceLiveness/Resources/Base.lproj/Localizable.strings b/Sources/FaceLiveness/Resources/Base.lproj/Localizable.strings new file mode 100644 index 00000000..afe3aff2 --- /dev/null +++ b/Sources/FaceLiveness/Resources/Base.lproj/Localizable.strings @@ -0,0 +1,51 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +"amplify_ui_liveness_get_ready_page_title" = "Liveness Check"; +"amplify_ui_liveness_get_ready_photosensitivity_title" = "Photosensitivity Warning"; +"amplify_ui_liveness_get_ready_photosensitivity_description" = "This check flashes different colors. Use caution if you are photosensitive."; +"amplify_ui_liveness_get_ready_photosensitivity_icon_a11y" = "Photosensitivity Information"; +"amplify_ui_liveness_get_ready_photosensitivity_dialog_title" = "Photosensitivity warning"; +"amplify_ui_liveness_get_ready_photosensitivity_dialog_description" = "Some people may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition."; +"amplify_ui_liveness_get_ready_begin_check" = "Start video check"; + +"amplify_ui_liveness_challenge_recording_indicator_label" = "REC"; +"amplify_ui_liveness_challenge_instruction_hold_face_during_countdown" = "Hold face position during countdown."; +"amplify_ui_liveness_challenge_instruction_hold_face_during_freshness" = "Hold face in oval for colored lights."; +"amplify_ui_liveness_challenge_instruction_move_face_back" = "Move back"; +"amplify_ui_liveness_challenge_instruction_move_face_closer" = "Move closer"; +"amplify_ui_liveness_challenge_instruction_move_face_in_front_of_camera" = "Move face in front of camera"; +"amplify_ui_liveness_challenge_instruction_multiple_faces_detected" = "Only one face per check"; +"amplify_ui_liveness_challenge_instruction_hold_still" = "Hold still"; + +"amplify_ui_liveness_challenge_connecting" = "Connecting..."; +"amplify_ui_liveness_challenge_verifying" = "Verifying"; +"amplify_ui_liveness_challenge_cancel_a11y" = "Cancel Challenge"; + +"amplify_ui_liveness_camera_setting_alert_title" = "Change Your Camera Settings"; +"amplify_ui_liveness_camera_setting_alert_message" = "Allow camera permission in settings."; +"amplify_ui_liveness_camera_setting_alert_update_setting_button_text" = "Update Setting"; +"amplify_ui_liveness_camera_setting_alert_not_now_button_text" = "Not Now"; + +"amplify_ui_liveness_close_button_a11y" = "Close"; + +"amplify_ui_liveness_center_your_face_text" = "Center your face"; +"amplify_ui_liveness_camera_permission_page_title" = "Liveness Check"; +"amplify_ui_liveness_camera_permission_button_title" = "Change Camera Setting"; +"amplify_ui_liveness_camera_permission_button_header" = "Camera is not accessible"; +"amplify_ui_liveness_camera_permission_button_description" = "You may have to go into settings to grant camera permissions and close the app and retry."; + +"amplify_ui_liveness_face_not_prepared_reason_pendingCheck" = " "; +"amplify_ui_liveness_face_not_prepared_reason_not_in_oval" = "Move face to fit in oval"; +"amplify_ui_liveness_face_not_prepared_reason_move_face_closer" = "Move closer"; +"amplify_ui_liveness_face_not_prepared_reason_move_face_right" = "Move face right"; +"amplify_ui_liveness_face_not_prepared_reason_move_face_left" = "Move face left"; +"amplify_ui_liveness_face_not_prepared_reason_move_to_dimmer_area" = "Move to dimmer area"; +"amplify_ui_liveness_face_not_prepared_reason_move_to_brighter_area" = "Move to brighter area"; +"amplify_ui_liveness_face_not_prepared_reason_no_face" = "Move face in front of camera"; +"amplify_ui_liveness_face_not_prepared_reason_multiple_faces" = "Only one face per check"; +"amplify_ui_liveness_face_not_prepared_reason_face_too_close" = "Move face farther away"; diff --git a/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/analytics/coremldata.bin b/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/analytics/coremldata.bin new file mode 100644 index 0000000000000000000000000000000000000000..65c4f8b23889000c50c1e7c9ef547cc088d6094f GIT binary patch literal 441 zcmY+AL2rUU5QUqzmtLBt>7~D+O$t&{IdBlDLR7>8irm(9T(>N&vMA!Oud!>QbD8AL zd*3AQ_2z$k*p`U-P}RK5hI|3cfeKM3?xo>T?)-G4ITV8O;!dh9wS6`e#VLM7xk#+u zr9t!4$Zq_dljHKsAjpmS#Ex@4hIxo6@wCm*Y63^^DA;_G8A%FPXbqPvtZhCs|5shk zSa&cn- literal 0 HcmV?d00001 diff --git a/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/coremldata.bin b/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/coremldata.bin new file mode 100644 index 0000000000000000000000000000000000000000..c13ab7610edb0bb4973ae65bd0c592d13360d4eb GIT binary patch literal 309 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≫7dnbllx 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%0AoS!6e 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

6XcXsX1O$-(V~Ba>;OA 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

VfI@&)&;VOlUuy)O|q8iw$2p*AFKGGl(Fay-fR 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� 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 a#h$;W%-?fYP4TlcyZ=oYFLwP+m;VO{50^Ut literal 0 HcmV?d00001 diff --git a/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/model/coremldata.bin b/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/model/coremldata.bin new file mode 100644 index 0000000000000000000000000000000000000000..ef29f3785503c1c87df2dd5f198cdb3cf197ae61 GIT binary patch literal 200 qcmZQ%fB;q~otc}Mp31<`V9$tMf`NfliH7|+bdWBA!}7)k1{(mYfffP) literal 0 HcmV?d00001 diff --git a/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/neural_network_optionals/coremldata.bin b/Sources/FaceLiveness/Resources/face_detection_short_range.mlmodelc/neural_network_optionals/coremldata.bin new file mode 100644 index 0000000000000000000000000000000000000000..43f65327944a434d36d3e342c2b1a9aebfd18d49 GIT binary patch literal 40 PcmZQzKn0AbTsRK^06+i& literal 0 HcmV?d00001 diff --git a/Sources/FaceLiveness/Utilities/Backports/Background.swift b/Sources/FaceLiveness/Utilities/Backports/Background.swift new file mode 100644 index 00000000..4ffb074c --- /dev/null +++ b/Sources/FaceLiveness/Utilities/Backports/Background.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension View { + @ViewBuilder func _background( + alignment: Alignment = .center, + @ViewBuilder _ content: () -> Content + ) -> some View { + if #available(iOS 15.0, *) { + background(alignment: alignment, content: content) + } else { + background(content(), alignment: alignment) + } + } +} diff --git a/Sources/FaceLiveness/Utilities/Backports/Overlay.swift b/Sources/FaceLiveness/Utilities/Backports/Overlay.swift new file mode 100644 index 00000000..fc2f34b1 --- /dev/null +++ b/Sources/FaceLiveness/Utilities/Backports/Overlay.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension View { + @ViewBuilder func _overlay( + alignment: Alignment = .center, + @ViewBuilder content: () -> V + ) -> some View { + if #available(iOS 15.0, *) { + overlay(alignment: alignment, content: content) + } else { + overlay(content(), alignment: alignment) + } + } +} diff --git a/Sources/FaceLiveness/Utilities/BoundingBox+Normalize.swift b/Sources/FaceLiveness/Utilities/BoundingBox+Normalize.swift new file mode 100644 index 00000000..1078f6e7 --- /dev/null +++ b/Sources/FaceLiveness/Utilities/BoundingBox+Normalize.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +extension FaceLivenessSession.BoundingBox { + func normalize(within videoSize: CGSize) -> Self { + .init( + x: x / videoSize.width, + y: y / videoSize.height, + width: width / videoSize.width, + height: height / videoSize.height + ) + } +} diff --git a/Sources/FaceLiveness/Utilities/CGImage+Convert.swift b/Sources/FaceLiveness/Utilities/CGImage+Convert.swift new file mode 100644 index 00000000..7304392a --- /dev/null +++ b/Sources/FaceLiveness/Utilities/CGImage+Convert.swift @@ -0,0 +1,26 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import CoreGraphics +import VideoToolbox + +extension CGImage { + static func convert(from cvPixelBuffer: CVPixelBuffer?) -> CGImage? { + guard let pixelBuffer = cvPixelBuffer else { + return nil + } + + var image: CGImage? + VTCreateCGImageFromCVPixelBuffer( + pixelBuffer, + options: nil, + imageOut: &image + ) + + return image + } +} diff --git a/Sources/FaceLiveness/Utilities/Color+DynamicColors.swift b/Sources/FaceLiveness/Utilities/Color+DynamicColors.swift new file mode 100644 index 00000000..63896981 --- /dev/null +++ b/Sources/FaceLiveness/Utilities/Color+DynamicColors.swift @@ -0,0 +1,24 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import SwiftUI + +extension Color { + static func dynamicColors(light: UIColor, dark: UIColor) -> Color { + Color( + UIColor( + dynamicProvider: { traitCollection in + switch traitCollection.userInterfaceStyle { + case .dark: return dark + default: return light + } + } + ) + ) + } +} diff --git a/Sources/FaceLiveness/Utilities/Color+Hex.swift b/Sources/FaceLiveness/Utilities/Color+Hex.swift new file mode 100644 index 00000000..0136a714 --- /dev/null +++ b/Sources/FaceLiveness/Utilities/Color+Hex.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import UIKit + +extension Color { + static func hex(_ hex: String) -> Color { + Color(UIColor.hex(hex)) + } +} diff --git a/Sources/FaceLiveness/Utilities/Color+Liveness.swift b/Sources/FaceLiveness/Utilities/Color+Liveness.swift new file mode 100644 index 00000000..884deb4c --- /dev/null +++ b/Sources/FaceLiveness/Utilities/Color+Liveness.swift @@ -0,0 +1,55 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension Color { + static let livenessPrimaryBackground = Color.dynamicColors( + light: .hex("#FF8473"), + dark: .hex("#FF8473") + ) + + static let livenessPrimaryLabel = Color.dynamicColors( + light: .white, + dark: .white + ) + + static let livenessBackground = Color.dynamicColors( + light: .white, + dark: .hex("#0D1926") + ) + + static let livenessLabel = Color.dynamicColors( + light: .black, + dark: .white + ) + + static let livenessErrorBackground = Color.dynamicColors( + light: .hex("#950404"), + dark: .hex("#EF8F8F") + ) + + static let livenessErrorLabel = Color.dynamicColors( + light: .white, + dark: .black + ) + + static let livenessWarningBackground = Color.dynamicColors( + light: .hex("#B8CEF9"), + dark: .hex("#663300") + ) + + static let livenessWarningLabel = Color.dynamicColors( + light: .hex("#FFFFFF"), + dark: .hex("#FFFFFF") + ) + + static let livenessPreviewBorder = Color.dynamicColors( + light: .hex("#AEB3B7"), + dark: .white + ) +} diff --git a/Sources/FaceLiveness/Utilities/Date+TimestampMilliseconds.swift b/Sources/FaceLiveness/Utilities/Date+TimestampMilliseconds.swift new file mode 100644 index 00000000..4a19e14c --- /dev/null +++ b/Sources/FaceLiveness/Utilities/Date+TimestampMilliseconds.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension Date { + var timestampMilliseconds: UInt64 { + UInt64(timeIntervalSince1970 * 1_000) + } +} diff --git a/Sources/FaceLiveness/Utilities/Device.swift b/Sources/FaceLiveness/Utilities/Device.swift new file mode 100644 index 00000000..643d4d58 --- /dev/null +++ b/Sources/FaceLiveness/Utilities/Device.swift @@ -0,0 +1,69 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +@dynamicMemberLookup +struct Device { + let info: Info + + subscript(dynamicMember keyPath: KeyPath) -> T { + info[keyPath: keyPath] + } +} + +extension Device: Equatable { + public static func == (lhs: Device, rhs: Device) -> Bool { + return lhs.info == rhs.info + } +} + +extension Device { + static var current: Device { + Device(info: info) + } + + struct Info: Equatable, Hashable { + public let sysname: String + public let nodename: String + public let release: String + public let version: String + public let machine: String + } + + // https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/utsname.h.auto.html + static let info: Info = { + func value( + from p: UnsafeMutablePointer, + _ keyPath: KeyPath + ) -> String { + var property = p.pointee[keyPath: keyPath] + return withUnsafePointer(to: &property) { + $0.withMemoryRebound( + to: CChar.self, + capacity: 1, + String.init(cString:) + ) + } + } + + let sysInfo = UnsafeMutablePointer + .allocate(capacity: 1) + sysInfo.initialize(to: utsname()) + uname(sysInfo) + + return Info( + sysname: value(from: sysInfo, \.sysname), + nodename: value(from: sysInfo, \.nodename), + release: value(from: sysInfo, \.release), + version: value(from: sysInfo, \.version), + machine: value(from: sysInfo, \.machine) + ) + }() +} + +typealias utsname_prop = (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar) diff --git a/Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift b/Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift new file mode 100644 index 00000000..ccdf971f --- /dev/null +++ b/Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +extension FinalClientEvent { + init( + sessionConfiguration: FaceLivenessSession.SessionConfiguration, + initialClientEvent: InitialClientEvent, + videoSize: CGSize, + faceMatchedStart: UInt64, + faceMatchedEnd: UInt64, + videoEnd: UInt64 + ) { + let normalizedBoundingBox = sessionConfiguration + .ovalMatchChallenge + .oval.boundingBox + .normalize(within: videoSize) + + self.init( + initialClientEvent: initialClientEvent, + targetFace: .init( + initialEvent: .init( + boundingBox: normalizedBoundingBox, + startTimestamp: faceMatchedStart + ), + endTimestamp: faceMatchedEnd + ), + videoEndTimeStamp: videoEnd + ) + } +} diff --git a/Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift b/Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift new file mode 100644 index 00000000..b09bb7eb --- /dev/null +++ b/Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift @@ -0,0 +1,121 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +enum LocalizedStrings { + /// en = "Liveness Check" + static let get_ready_page_title = "amplify_ui_liveness_get_ready_page_title".localized() + + /// en = "Photosensitivity Warning" + static let get_ready_photosensitivity_title = "amplify_ui_liveness_get_ready_photosensitivity_title".localized() + + /// en = "This check displays colored lights. Use caution if you are photosensitive." + static let get_ready_photosensitivity_description = "amplify_ui_liveness_get_ready_photosensitivity_description".localized() + + /// en = "Photosensitivity Information" + static let get_ready_photosensitivity_icon_a11y = "amplify_ui_liveness_get_ready_photosensitivity_icon_a11y".localized() + + /// en = "Photosensitivity warning" + static let get_ready_photosensitivity_dialog_title = "amplify_ui_liveness_get_ready_photosensitivity_dialog_title".localized() + + /// en = "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." + static let get_ready_photosensitivity_dialog_description = "amplify_ui_liveness_get_ready_photosensitivity_dialog_description".localized() + + /// en = "Start video check" + static let get_ready_begin_check = "amplify_ui_liveness_get_ready_begin_check".localized() + + /// en = "REC" + static let challenge_recording_indicator_label = "amplify_ui_liveness_challenge_recording_indicator_label".localized() + + /// en = "Hold face in oval for colored lights." + static let challenge_instruction_hold_face_during_freshness = "amplify_ui_liveness_challenge_instruction_hold_face_during_freshness".localized() + + /// en = "Move back" + static let challenge_instruction_move_face_back = "amplify_ui_liveness_challenge_instruction_move_face_back".localized() + + /// en = "Move closer" + static let challenge_instruction_move_face_closer = "amplify_ui_liveness_challenge_instruction_move_face_closer".localized() + + /// en = "Move closer" + static let challenge_instruction_move_face = "amplify_ui_liveness_challenge_instruction_move_face".localized() + + /// en = "Hold still" + static let challenge_instruction_hold_still = "amplify_ui_liveness_challenge_instruction_hold_still".localized() + + /// en = "Only one face per check" + static let challenge_instruction_multiple_faces_detected = "amplify_ui_liveness_challenge_instruction_multiple_faces_detected".localized() + + /// en = "Connecting..." + static let challenge_connecting = "amplify_ui_liveness_challenge_connecting".localized() + + /// en = "Verifying" + static let challenge_verifying = "amplify_ui_liveness_challenge_verifying".localized() + + /// en = "Cancel Challenge" + static let challenge_cancel_a11y = "amplify_ui_liveness_challenge_cancel_a11y".localized() + + /// en = "Change Your Camera Settings" + static let camera_setting_alert_title = "amplify_ui_liveness_camera_setting_alert_title".localized() + + /// en = "Allow camera permission in settings." + static let camera_setting_alert_message = "amplify_ui_liveness_camera_setting_alert_message".localized() + + /// en = "Update Setting" + static let camera_setting_alert_update_setting_button_text = "amplify_ui_liveness_camera_setting_alert_update_setting_button_text".localized() + + /// en = "Not Now" + static let camera_setting_alert_not_now_button_text = "amplify_ui_liveness_camera_setting_alert_not_now_button_text".localized() + + /// en = "Close" + static let close_button_a11y = "amplify_ui_liveness_close_button_a11y".localized() + + /// en = "Center your face" + static let preview_center_your_face_text = "amplify_ui_liveness_center_your_face_text".localized() + + /// en = "Liveness check" + static let camera_permission_page_title = "amplify_ui_liveness_camera_permission_page_title".localized() + + /// en = "Change Camera Setting" + static let camera_permission_change_setting_button_title = "amplify_ui_liveness_camera_permission_button_title".localized() + + /// en = "Camera is not accessible" + static let camera_permission_change_setting_header = "amplify_ui_liveness_camera_permission_button_header".localized() + + /// en = "You may have to go into settings to grant camera permissions and close the app and retry" + static let camera_permission_change_setting_description = "amplify_ui_liveness_camera_permission_button_description".localized() + + /// en = "" + static let amplify_ui_liveness_face_not_prepared_reason_pendingCheck = "amplify_ui_liveness_face_not_prepared_reason_pendingCheck".localized() + + /// en = "Move face to fit in oval" + static let amplify_ui_liveness_face_not_prepared_reason_not_in_oval = "amplify_ui_liveness_face_not_prepared_reason_not_in_oval".localized() + + /// en = "Move closer" + static let amplify_ui_liveness_face_not_prepared_reason_move_face_closer = "amplify_ui_liveness_face_not_prepared_reason_move_face_closer".localized() + + /// en = "Move face right" + static let amplify_ui_liveness_face_not_prepared_reason_move_face_right = "amplify_ui_liveness_face_not_prepared_reason_move_face_right".localized() + + /// en = "Move face left" + static let amplify_ui_liveness_face_not_prepared_reason_move_face_left = "amplify_ui_liveness_face_not_prepared_reason_move_face_left".localized() + + /// en = "Move to dimmer area" + static let amplify_ui_liveness_face_not_prepared_reason_move_to_dimmer_area = "amplify_ui_liveness_face_not_prepared_reason_move_to_dimmer_area".localized() + + /// en = "Move to brighter area" + static let amplify_ui_liveness_face_not_prepared_reason_move_to_brighter_area = "amplify_ui_liveness_face_not_prepared_reason_move_to_brighter_area".localized() + + /// en = "Move face in front of camera" + static let amplify_ui_liveness_face_not_prepared_reason_no_face = "amplify_ui_liveness_face_not_prepared_reason_no_face".localized() + + /// en = "Ensure only one face is in front of camera" + static let amplify_ui_liveness_face_not_prepared_reason_multiple_faces = "amplify_ui_liveness_face_not_prepared_reason_multiple_faces".localized() + + /// en = "Move face farther away" + static let amplify_ui_liveness_face_not_prepared_reason_face_too_close = "amplify_ui_liveness_face_not_prepared_reason_face_too_close".localized() +} diff --git a/Sources/FaceLiveness/Utilities/String+Localizable.swift b/Sources/FaceLiveness/Utilities/String+Localizable.swift new file mode 100644 index 00000000..b81091dc --- /dev/null +++ b/Sources/FaceLiveness/Utilities/String+Localizable.swift @@ -0,0 +1,27 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension String { + /// Looks for a localized value using this value as the key. + /// If no localization is found in the current app's bundle, + /// it defaults to the one provided by Liveness + func localized(comment: String = "") -> String { + let defaultValue = NSLocalizedString(self, bundle: .module, comment: "") + return NSLocalizedString( + self, + bundle: .main, + value: defaultValue, + comment: "" + ) + } + + func localized(using arguments: CVarArg...) -> String { + return String(format: localized(), arguments) + } +} diff --git a/Sources/FaceLiveness/Utilities/UIColor+Hex.swift b/Sources/FaceLiveness/Utilities/UIColor+Hex.swift new file mode 100644 index 00000000..678773d1 --- /dev/null +++ b/Sources/FaceLiveness/Utilities/UIColor+Hex.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit + +extension UIColor { + static func hex(_ hex: String) -> UIColor { + assert(hex.hasPrefix("#")) + + let hex = String(hex.dropFirst()) + assert(hex.count == 6) + + let scanner = Scanner(string: hex) + var hexNumber: UInt64 = 0 + + precondition(scanner.scanHexInt64(&hexNumber)) + let r, g, b, a: CGFloat + r = CGFloat((hexNumber & 0xFF0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x00FF00) >> 8) / 255 + b = CGFloat((hexNumber & 0x0000FF)) / 255 + a = 1.0 + + return UIColor(red: r, green: g, blue: b, alpha: a) + } +} diff --git a/Sources/FaceLiveness/Utilities/UserAgent.swift b/Sources/FaceLiveness/Utilities/UserAgent.swift new file mode 100644 index 00000000..2451a93c --- /dev/null +++ b/Sources/FaceLiveness/Utilities/UserAgent.swift @@ -0,0 +1,90 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import InternalAmplifyCredentials + +struct UserAgentValues { + + static let libVersion = "1.3.4" + static let libName = "amplify-ui-swift-face-liveness" + + let amplifyVersion: String + let os: String + let osVersion: String + let swiftVersion: String + let unameMachine: String + let locale: String + let additionalMetadata: String + let lib: String? + + var userAgentString: String { + let string = "amplify-swift/\(amplifyVersion) api/rekognitionstreaming/\(amplifyVersion) os/\(os)/\(osVersion) lang/swift/\(swiftVersion) md/device/\(unameMachine) md/locale/\(locale)" + additionalMetadata + if let lib = lib { + return string + " \(lib)" + } else { + return string + } + } + + init( + amplifyVersion: String, + os: String, + osVersion: String, + swiftVersion: String, + unameMachine: String, + locale: String, + lib: String?, + additionalMetadata: KeyValuePairs + ) { + self.amplifyVersion = amplifyVersion + self.os = os + self.osVersion = osVersion + self.swiftVersion = swiftVersion + self.unameMachine = unameMachine + self.locale = locale + self.lib = lib + self.additionalMetadata = additionalMetadata.map { key, value in + "md/\(key)/\(value)" + }.joined(separator: " ") + } + + static func standard(additionalMetadata: KeyValuePairs = [:]) -> Self { + return .init( + amplifyVersion: AmplifyAWSServiceConfiguration.amplifyVersion, + os: UIDevice.current.systemName.replacingOccurrences(of: " ", with: "-"), + osVersion: UIDevice.current.systemVersion, + swiftVersion: Swift().version(), + unameMachine: Device.current.machine.replacingOccurrences(of: ",", with: "_"), + locale: Locale.current.identifier, + lib: "lib/\(Self.libName)/\(Self.libVersion)", + additionalMetadata: additionalMetadata + ) + } +} + + +// https://github.com/apple/swift-evolution/blob/main/proposals/0141-available-by-swift-version.md +fileprivate struct Swift { + func version() -> String { +#if swift(>=7.0) + return "unknown" +#elseif swift(>=6.0) + return "6.x" +#elseif swift(>=5.9) + return "5.9" +#elseif swift(>=5.8) + return "5.8" +#elseif swift(>=5.8) + return "5.8" +#elseif swift(>=5.7) + return "5.7" +#else + return "unknown" +#endif + } +} diff --git a/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift b/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift new file mode 100644 index 00000000..e5edbf3f --- /dev/null +++ b/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift @@ -0,0 +1,79 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct CameraPermissionView: View { + @Binding var displayingCameraPermissionsNeededAlert: Bool + + init( + displayingCameraPermissionsNeededAlert: Binding = .constant(false) + ) { + self._displayingCameraPermissionsNeededAlert = displayingCameraPermissionsNeededAlert + } + + var body: some View { + VStack(alignment: .leading) { + Spacer() + VStack { + Text(LocalizedStrings.camera_permission_change_setting_header) + .font(.title2) + .fontWeight(.medium) + .multilineTextAlignment(.center) + .padding(8) + + Text(LocalizedStrings.camera_permission_change_setting_description) + .multilineTextAlignment(.center) + .padding(8) + } + Spacer() + editPermissionButton + } + .alert(isPresented: $displayingCameraPermissionsNeededAlert) { + Alert( + title: Text(LocalizedStrings.camera_setting_alert_title), + message: Text(LocalizedStrings.camera_setting_alert_message), + primaryButton: .default( + Text(LocalizedStrings.camera_setting_alert_update_setting_button_text).bold(), + action: { + goToSettingsAppPage() + }), + secondaryButton: .default( + Text(LocalizedStrings.camera_setting_alert_not_now_button_text) + ) + ) + } + } + + private func goToSettingsAppPage() { + guard let settingsAppURL = URL(string: UIApplication.openSettingsURLString) + else { return } + UIApplication.shared.open(settingsAppURL, options: [:]) + } + + private var editPermissionButton: some View { + Button( + action: goToSettingsAppPage, + label: { + Text(LocalizedStrings.camera_permission_change_setting_button_title) + .foregroundColor(.livenessPrimaryLabel) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { Color.livenessPrimaryBackground } + .cornerRadius(14) + .padding([.leading, .trailing]) + .padding(.bottom, 16) + } +} + +struct CameraPermissionView_Previews: PreviewProvider { + static var previews: some View { + CameraPermissionView() + } +} diff --git a/Sources/FaceLiveness/Views/CloseButton.swift b/Sources/FaceLiveness/Views/CloseButton.swift new file mode 100644 index 00000000..dee45c40 --- /dev/null +++ b/Sources/FaceLiveness/Views/CloseButton.swift @@ -0,0 +1,36 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct CloseButton: View { + let action: () -> Void + + var body: some View { + Button( + action: action, + label: { + Image(systemName: "xmark") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.livenessLabel) + .frame(width: 44, height: 44) + .background(Color.livenessBackground) + .clipShape(Circle()) + .accessibilityLabel(Text(LocalizedStrings.close_button_a11y)) + } + ) + } +} + +struct CloseButton_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.gray + CloseButton(action: {}) + } + } +} diff --git a/Sources/FaceLiveness/Views/Freshness/DisplayColor+UIColor.swift b/Sources/FaceLiveness/Views/Freshness/DisplayColor+UIColor.swift new file mode 100644 index 00000000..a9cbb91b --- /dev/null +++ b/Sources/FaceLiveness/Views/Freshness/DisplayColor+UIColor.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import Amplify +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +extension FaceLivenessSession.DisplayColor { + var uiColor: UIColor { + return .init( + red: rgb.red, + green: rgb.green, + blue: rgb.blue, + alpha: 1 + ) + } +} diff --git a/Sources/FaceLiveness/Views/Freshness/Freshness.swift b/Sources/FaceLiveness/Views/Freshness/Freshness.swift new file mode 100644 index 00000000..2af2db00 --- /dev/null +++ b/Sources/FaceLiveness/Views/Freshness/Freshness.swift @@ -0,0 +1,168 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import Amplify +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +class Freshness { + var colorSequences: [FaceLivenessSession.DisplayColor] = [] + let tickRate: Double + let initialAlpha: CGFloat + let secondaryAlpha: CGFloat + var timer: Timer? = nil + + init( + tickRate: Double = 0.01, + initialAlpha: CGFloat = 0.9, + secondaryAlpha: CGFloat = 0.75 + ) { + self.tickRate = tickRate + self.initialAlpha = initialAlpha + self.secondaryAlpha = secondaryAlpha + } + + struct ColorEvent { + let currentColor: FaceLivenessSession.DisplayColor + let previousColor: FaceLivenessSession.DisplayColor + let sequenceNumber: Int + let colorStartTime: UInt64 + } + + func showColorSequences( + _ colorSequences: [FaceLivenessSession.DisplayColor], + width: CGFloat, + height: CGFloat, + view: FreshnessView, + onNewColor: @escaping (ColorEvent) -> Void, + onComplete: @escaping () -> Void + ) { + self.colorSequences = colorSequences + _showColorSequences( + colorIndex: 0, + previousColor: nil, + view: view, + height: height, + onNewColor: onNewColor, + onComplete: onComplete + ) + } + + + private func _showColorSequences( + colorIndex: Int, + previousColor: FaceLivenessSession.DisplayColor?, + view: FreshnessView, + height: CGFloat, + onNewColor: @escaping (ColorEvent) -> Void, + onComplete: @escaping () -> Void + ) { + if colorIndex >= colorSequences.count { + view.clearColors() + onComplete() + return + } + + let currentFreshnessColor = colorSequences[colorIndex] + + if !currentFreshnessColor.shouldScroll { + view.alpha = initialAlpha + view.backgroundColor = currentFreshnessColor.uiColor + + let previous = previousColor ?? currentFreshnessColor + onNewColor( + .init( + currentColor: currentFreshnessColor, + previousColor: previous, + sequenceNumber: colorIndex, + colorStartTime: Date().timestampMilliseconds + ) + ) + + DispatchQueue.main.asyncAfter( + deadline: .now() + Double(currentFreshnessColor.duration / 1_000) + ) { + onNewColor( + .init( + currentColor: self.colorSequences[colorIndex + 1], + previousColor: currentFreshnessColor, + sequenceNumber: colorIndex + 1, + colorStartTime: Date().timestampMilliseconds + ) + ) + self._showColorSequences( + colorIndex: colorIndex + 1, + previousColor: currentFreshnessColor, + view: view, + height: height, + onNewColor: onNewColor, + onComplete: onComplete + ) + } + } else { + view.alpha = secondaryAlpha + var newRectangleBottom: Double = 0 + let heightIncrease = height / Double(currentFreshnessColor.duration / 1_000) * tickRate + var msElapsed: Double = 0 + + if let previousColor { + view.oldRectangle.backgroundColor = previousColor.uiColor + } + + Timer.scheduledTimer(withTimeInterval: tickRate, repeats: true) { timer in + msElapsed += self.tickRate + if msElapsed >= Double(currentFreshnessColor.duration / 1_000) { + timer.invalidate() + view.fractionalRectangleBottom?.constant = 0 + view.oldRectangleTop?.constant = 0 + if colorIndex < self.colorSequences.count - 1 { + onNewColor( + .init( + currentColor: self.colorSequences[colorIndex + 1], + previousColor: currentFreshnessColor, + sequenceNumber: colorIndex + 1, + colorStartTime: Date().timestampMilliseconds + ) + ) + } + self._showColorSequences( + colorIndex: colorIndex + 1, + previousColor: currentFreshnessColor, + view: view, + height: height, + onNewColor: onNewColor, + onComplete: onComplete + ) + } else { + newRectangleBottom += CGFloat(heightIncrease) + view.fractionalRectangleBottom?.constant = newRectangleBottom + view.oldRectangleTop?.constant = newRectangleBottom + view.fractionalRectangle.backgroundColor = currentFreshnessColor.uiColor + if let previousColor { + view.oldRectangle.backgroundColor = previousColor.uiColor + } + } + } + } + } +} + +fileprivate extension ColorSequence { + private func normalize(rgb: Int) -> CGFloat { + CGFloat(rgb) / 255 + } + + var uiColor: UIColor { + assert(freshnessColor.rgb.count == 3, "Invalid input format. Expected `Array` with values [r, g, b]") + return .init( + red: normalize(rgb: freshnessColor.rgb[0]), + green: normalize(rgb: freshnessColor.rgb[1]), + blue: normalize(rgb: freshnessColor.rgb[2]), + alpha: 1 + ) + } +} diff --git a/Sources/FaceLiveness/Views/Freshness/FreshnessView.swift b/Sources/FaceLiveness/Views/Freshness/FreshnessView.swift new file mode 100644 index 00000000..7032ec0d --- /dev/null +++ b/Sources/FaceLiveness/Views/Freshness/FreshnessView.swift @@ -0,0 +1,49 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit + +final class FreshnessView: UIView { + let oldRectangle = UIView() + let fractionalRectangle = UIView() + + var fractionalRectangleBottom: NSLayoutConstraint? + var oldRectangleTop: NSLayoutConstraint? + + func clearColors() { + backgroundColor = .clear + oldRectangle.backgroundColor = .clear + fractionalRectangle.backgroundColor = .clear + } + + init() { + super.init(frame: .zero) + oldRectangle.translatesAutoresizingMaskIntoConstraints = false + fractionalRectangle.translatesAutoresizingMaskIntoConstraints = false + addSubview(oldRectangle) + addSubview(fractionalRectangle) + + fractionalRectangleBottom = fractionalRectangle.bottomAnchor.constraint(equalTo: topAnchor) + oldRectangleTop = oldRectangle.topAnchor.constraint(equalTo: topAnchor) + + fractionalRectangleBottom?.isActive = true + oldRectangleTop?.isActive = true + + NSLayoutConstraint.activate([ + oldRectangle.leadingAnchor.constraint(equalTo: leadingAnchor), + oldRectangle.bottomAnchor.constraint(equalTo: bottomAnchor), + oldRectangle.trailingAnchor.constraint(equalTo: trailingAnchor), + + fractionalRectangle.topAnchor.constraint(equalTo: topAnchor), + fractionalRectangle.leadingAnchor.constraint(equalTo: leadingAnchor), + fractionalRectangle.trailingAnchor.constraint(equalTo: trailingAnchor) + ]) + } + + required init?(coder: NSCoder) { nil } +} + diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewOutputSampleBufferDelegate.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewOutputSampleBufferDelegate.swift new file mode 100644 index 00000000..5b98ca19 --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewOutputSampleBufferDelegate.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation +import CoreImage + +class CameraPreviewOutputSampleBufferDelegate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { + + let updateBufferHandler: ((CVImageBuffer) -> Void) + + init(_ updateBufferHandler: @escaping (CVImageBuffer) -> Void) { + self.updateBufferHandler = updateBufferHandler + } + + func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { + if let buffer = sampleBuffer.imageBuffer { + updateBufferHandler(buffer) + } + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift new file mode 100644 index 00000000..2e8530f3 --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift @@ -0,0 +1,52 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct CameraPreviewView: View { + private static let previewWidthRatio = 0.6 + private static let previewHeightRatio = 0.55 + private static let previewXPositionRatio = 0.5 + private static let previewYPositionRatio = 0.6 + + @StateObject var model: CameraPreviewViewModel + + init(model: CameraPreviewViewModel = CameraPreviewViewModel()) { + self._model = StateObject(wrappedValue: model) + } + + var body: some View { + ZStack { + ImageFrameView(image: model.currentImageFrame) + .edgesIgnoringSafeArea(.all) + .mask( + GeometryReader { geometry in + Ellipse() + .frame(width: geometry.size.width*Self.previewWidthRatio, + height: geometry.size.height*Self.previewHeightRatio) + .position(x: geometry.size.width*Self.previewXPositionRatio, + y: geometry.size.height*Self.previewYPositionRatio) + }) + GeometryReader { geometry in + Ellipse() + .stroke(Color.livenessPreviewBorder, style: StrokeStyle(lineWidth: 3)) + .frame(width: geometry.size.width*Self.previewWidthRatio, + height: geometry.size.height*Self.previewHeightRatio) + .position(x: geometry.size.width*Self.previewXPositionRatio, + y: geometry.size.height*Self.previewYPositionRatio) + } + }.onDisappear { + model.stopSession() + } + } +} + +struct CameraPreviewView_Previews: PreviewProvider { + static var previews: some View { + CameraPreviewView() + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift new file mode 100644 index 00000000..b50173b0 --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift @@ -0,0 +1,65 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import CoreImage +import Combine +import AVFoundation +import Amplify + +class CameraPreviewViewModel: NSObject, ObservableObject { + @Published var currentImageFrame: CGImage? + @Published var buffer: CVPixelBuffer? + + var previewCaptureSession: LivenessCaptureSession? + + override init() { + super.init() + setupSubscriptions() + + let avCaptureDevice = AVCaptureDevice.DiscoverySession( + deviceTypes: [.builtInWideAngleCamera], + mediaType: .video, + position: .front + ).devices.first + + let outputDelegate = CameraPreviewOutputSampleBufferDelegate { [weak self] buffer in + self?.updateBuffer(buffer) + } + + self.previewCaptureSession = LivenessCaptureSession( + captureDevice: .init(avCaptureDevice: avCaptureDevice), + outputDelegate: outputDelegate + ) + + do { + try previewCaptureSession?.configureCamera() + previewCaptureSession?.startSession() + } catch { + Amplify.Logging.default.error("Error starting preview capture session with error: \(error)") + } + } + + func setupSubscriptions() { + self.$buffer + .receive(on: RunLoop.main) + .compactMap { + return CGImage.convert(from: $0) + } + .assign(to: &$currentImageFrame) + } + + func stopSession() { + previewCaptureSession?.stopRunning() + } + + func updateBuffer(_ buffer: CVImageBuffer) { + DispatchQueue.main.async { + self.buffer = buffer + } + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift new file mode 100644 index 00000000..e5641c2b --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift @@ -0,0 +1,79 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct GetReadyPageView: View { + let beginCheckButtonDisabled: Bool + let onBegin: () -> Void + + init( + onBegin: @escaping () -> Void, + beginCheckButtonDisabled: Bool = false + ) { + self.onBegin = onBegin + self.beginCheckButtonDisabled = beginCheckButtonDisabled + } + + var body: some View { + VStack { + ZStack { + CameraPreviewView() + VStack { + WarningBox( + + titleText: LocalizedStrings.get_ready_photosensitivity_title, + bodyText: LocalizedStrings.get_ready_photosensitivity_description, + popoverContent: { photosensitivityWarningPopoverContent } + ) + .accessibilityElement(children: .combine) + Text(LocalizedStrings.preview_center_your_face_text) + .font(.title) + .multilineTextAlignment(.center) + Spacer() + }.padding() + } + beginCheckButton + } + } + + private var beginCheckButton: some View { + Button( + action: onBegin, + label: { + Text(LocalizedStrings.get_ready_begin_check) + .foregroundColor(.livenessPrimaryLabel) + .frame(maxWidth: .infinity) + } + ) + .disabled(beginCheckButtonDisabled) + .frame(height: 52) + ._background {Color(UIColor.hex("#FF8473")) } + .cornerRadius(14) + .padding([.leading, .trailing]) + .padding(.bottom, 16) + } + + private var photosensitivityWarningPopoverContent: some View { + VStack { + Text(LocalizedStrings.get_ready_photosensitivity_dialog_title) + .background(Color.yellow) + .font(.system(size: 20, weight: .medium)) + .frame(alignment: .center) + .padding() + Text(LocalizedStrings.get_ready_photosensitivity_dialog_description) + .padding() + Spacer() + } + } +} + +struct GetReadyPageView_Previews: PreviewProvider { + static var previews: some View { + GetReadyPageView(onBegin: {}) + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift b/Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift new file mode 100644 index 00000000..6a38ae64 --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct ImageFrameView: View { + var image: CGImage? + + var body: some View { + if let image = image { + GeometryReader { geometry in + Image(decorative: image, scale: 1.0, orientation: .up) + .resizable() + .scaledToFill() + .frame( + width: geometry.size.width, + height: geometry.size.height, + alignment: .center) + .clipped() + } + } else { + Color.black + } + } +} + +struct ImageFrameView_Previews: PreviewProvider { + static var previews: some View { + ImageFrameView() + } +} diff --git a/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift new file mode 100644 index 00000000..ff02a3d6 --- /dev/null +++ b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift @@ -0,0 +1,111 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import Combine + +struct InstructionContainerView: View { + @ObservedObject var viewModel: FaceLivenessDetectionViewModel + + var body: some View { + switch viewModel.livenessState.state { + case .displayingFreshness: + InstructionView( + text: LocalizedStrings.challenge_instruction_hold_still, + backgroundColor: .livenessPrimaryBackground, + textColor: .livenessPrimaryLabel, + font: .title + ) + .onAppear { + UIAccessibility.post( + notification: .announcement, + argument: LocalizedStrings.challenge_instruction_hold_still + ) + } + + case .awaitingFaceInOvalMatch(.faceTooClose, _): + InstructionView( + text: LocalizedStrings.challenge_instruction_move_face_back, + backgroundColor: .livenessErrorBackground, + textColor: .livenessErrorLabel, + font: .title + ) + .onAppear { + UIAccessibility.post( + notification: .announcement, + argument: LocalizedStrings.challenge_instruction_move_face_back + ) + } + + case .awaitingFaceInOvalMatch(let reason, let percentage): + InstructionView( + text: .init(reason.localizedValue), + backgroundColor: .livenessPrimaryBackground, + textColor: .livenessPrimaryLabel, + font: .title + ) + + ProgressBarView( + emptyColor: .white, + borderColor: .hex("#AEB3B7"), + fillColor: .livenessPrimaryBackground, + indicatorColor: .livenessPrimaryBackground, + percentage: percentage + ) + .frame(width: 200, height: 30) + case .recording(ovalDisplayed: true): + InstructionView( + text: LocalizedStrings.challenge_instruction_move_face_closer, + backgroundColor: .livenessPrimaryBackground, + textColor: .livenessPrimaryLabel, + font: .title + ) + .onAppear { + UIAccessibility.post( + notification: .announcement, + argument: LocalizedStrings.challenge_instruction_move_face_closer + ) + } + + ProgressBarView( + emptyColor: .white, + borderColor: .hex("#AEB3B7"), + fillColor: .livenessPrimaryBackground, + indicatorColor: .livenessPrimaryBackground, + percentage: 0.2 + ) + .frame(width: 200, height: 30) + case .pendingFacePreparedConfirmation(let reason): + InstructionView( + text: .init(reason.localizedValue), + backgroundColor: .livenessPrimaryBackground, + textColor: .livenessPrimaryLabel, + font: .title + ) + case .completedDisplayingFreshness: + InstructionView( + text: LocalizedStrings.challenge_verifying, + backgroundColor: .livenessBackground + ) + .onAppear { + UIAccessibility.post( + notification: .announcement, + argument: LocalizedStrings.challenge_verifying + ) + } + case .faceMatched: + InstructionView( + text: LocalizedStrings.challenge_instruction_hold_still, + backgroundColor: .livenessPrimaryBackground, + textColor: .livenessPrimaryLabel, + font: .title + ) + default: + EmptyView() + } + } +} diff --git a/Sources/FaceLiveness/Views/Instruction/InstructionView.swift b/Sources/FaceLiveness/Views/Instruction/InstructionView.swift new file mode 100644 index 00000000..5311387b --- /dev/null +++ b/Sources/FaceLiveness/Views/Instruction/InstructionView.swift @@ -0,0 +1,24 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct InstructionView: View { + let text: String + let backgroundColor: Color + var textColor: Color = .livenessLabel + var font: Font = .body + + var body: some View { + Text(text) + .foregroundColor(textColor) + .font(font) + .padding(12) + .background(backgroundColor) + .cornerRadius(8) + } +} diff --git a/Sources/FaceLiveness/Views/Liveness/CameraView.swift b/Sources/FaceLiveness/Views/Liveness/CameraView.swift new file mode 100644 index 00000000..e984bce4 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/CameraView.swift @@ -0,0 +1,34 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import AVFoundation +import AWSPredictionsPlugin + +struct CameraView: UIViewControllerRepresentable { + @ObservedObject var faceLivenessDetectionViewModel: FaceLivenessDetectionViewModel + + init( + faceLivenessDetectionViewModel: FaceLivenessDetectionViewModel + ) { + self.faceLivenessDetectionViewModel = faceLivenessDetectionViewModel + } + + func makeUIViewController( + context: Context + ) -> _LivenessViewController { + let livenessViewController = _LivenessViewController( + viewModel: faceLivenessDetectionViewModel + ) + return livenessViewController + } + + func updateUIViewController( + _ uiViewController: _LivenessViewController, + context: Context + ) {} +} diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift new file mode 100644 index 00000000..e90a6f06 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift @@ -0,0 +1,138 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +public struct FaceLivenessDetectionError: Error, Equatable { + let code: UInt8 + public let message: String + public let recoverySuggestion: String + + public static let unknown = FaceLivenessDetectionError( + code: 0, + message: "An unknown error occurred.", + recoverySuggestion: "Please open an issue...." + ) + + public static let sessionNotFound = FaceLivenessDetectionError( + code: 1, + message: "Session not found.", + recoverySuggestion: "Enter a valid session ID." + ) + + public static let sessionTimedOut = FaceLivenessDetectionError( + code: 2, + message: "Session timed out. Did not receive final response from server within time limit.", + recoverySuggestion: "Try again." + ) + + public static let faceInOvalMatchExceededTimeLimitError = FaceLivenessDetectionError( + code: 3, + message: "Face did not match oval within time limit.", + recoverySuggestion: "Retry the face liveness check and prompt the user to follow the on screen instructions." + ) + + public static let accessDenied = FaceLivenessDetectionError( + code: 4, + message: "Not authorized to perform a face liveness check.", + recoverySuggestion: "Valid credentials are required for the face liveness check." + ) + + public static let cameraPermissionDenied = FaceLivenessDetectionError( + code: 5, + message: "Camera permissions have not been granted.", + recoverySuggestion: "Prompt the user to grant camera permission." + ) + + public static let userCancelled = FaceLivenessDetectionError( + code: 6, + message: "User cancelled the face liveness check.", + recoverySuggestion: "Retry the face liveness check." + ) + + public static let socketClosed = FaceLivenessDetectionError( + code: 7, + message: "Websocket connection unexpectedly closed", + recoverySuggestion: "" + ) + + public static let countdownFaceTooClose = FaceLivenessDetectionError( + code: 8, + message: "Check failed during countdown.", + recoverySuggestion: "User should not move closer during the countdown." + ) + + public static let countdownMultipleFaces = FaceLivenessDetectionError( + code: 9, + message: "Check failed during countdown.", + recoverySuggestion: "Multiple faces detected during the countdown." + ) + + public static let countdownNoFace = FaceLivenessDetectionError( + code: 10, + message: "Check failed during countdown.", + recoverySuggestion: "No face detected during the countdown." + ) + + public static let invalidRegion = FaceLivenessDetectionError( + code: 11, + message: "The region provided is invalid.", + recoverySuggestion: "Confirm that you are using a valid `region` and try again." + ) + + public static let validation = FaceLivenessDetectionError( + code: 12, + message: "The input fails to satisfy the constraints specified by the service.", + recoverySuggestion: """ + Retry the face liveness check and prompt the user to follow the on screen instructions. + """ + ) + + public static let internalServer = FaceLivenessDetectionError( + code: 13, + message: "Unexpected error during processing of request.", + recoverySuggestion: "" + ) + + public static let throttling = FaceLivenessDetectionError( + code: 14, + message: "A request was denied due to request throttling.", + recoverySuggestion: """ + Occurs when too many requests were made by a user (exceeding their service quota), + the service isn't able to scale, or a service-wide throttling was done to + recover from an operational event. + """ + ) + + public static let serviceQuotaExceeded = FaceLivenessDetectionError( + code: 15, + message: "Occurs when a request would cause a service quota to be exceeded.", + recoverySuggestion: "" + ) + + public static let serviceUnavailable = FaceLivenessDetectionError( + code: 16, + message: "Service-wide throttling to recover from an operational event or service is not able to scale.", + recoverySuggestion: "" + ) + + public static let invalidSignature = FaceLivenessDetectionError( + code: 17, + message: "The signature on the request is invalid.", + recoverySuggestion: "Ensure the device time is correct and try again." + ) + + public static let cameraNotAvailable = FaceLivenessDetectionError( + code: 18, + message: "The camera is not available.", + recoverySuggestion: "There might be a hardware issue." + ) + + public static func == (lhs: FaceLivenessDetectionError, rhs: FaceLivenessDetectionError) -> Bool { + lhs.code == rhs.code + } +} diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift new file mode 100644 index 00000000..81eacfe9 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -0,0 +1,284 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import AWSClientRuntime +import protocol AWSPluginsCore.AWSCredentialsProvider +import AWSPredictionsPlugin +import AVFoundation +import Amplify +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +public struct FaceLivenessDetectorView: View { + @StateObject var viewModel: FaceLivenessDetectionViewModel + @Binding var isPresented: Bool + @State var displayState: DisplayState = .awaitingCameraPermission + @State var displayingCameraPermissionsNeededAlert = false + + let disableStartView: Bool + let onCompletion: (Result) -> Void + + let sessionTask: Task + + public init( + sessionID: String, + credentialsProvider: AWSCredentialsProvider? = nil, + region: String, + disableStartView: Bool = false, + isPresented: Binding, + onCompletion: @escaping (Result) -> Void + ) { + self.disableStartView = disableStartView + self._isPresented = isPresented + self.onCompletion = onCompletion + + self.sessionTask = Task { + let session = try await AWSPredictionsPlugin.startFaceLivenessSession( + withID: sessionID, + credentialsProvider: credentialsProvider, + region: region, + options: .init(), + completion: map(detectionCompletion: onCompletion) + ) + return session + } + + let faceDetector = try! FaceDetectorShortRange.Model() + let faceInOvalStateMatching = FaceInOvalMatching( + instructor: Instructor() + ) + + let videoChunker = VideoChunker( + assetWriter: LivenessAVAssetWriter(), + assetWriterDelegate: VideoChunker.AssetWriterDelegate(), + assetWriterInput: LivenessAVAssetWriterInput() + ) + + let avCpatureDevice = AVCaptureDevice.DiscoverySession( + deviceTypes: [.builtInWideAngleCamera], + mediaType: .video, + position: .front + ).devices.first + + let captureSession = LivenessCaptureSession( + captureDevice: .init(avCaptureDevice: avCpatureDevice), + outputDelegate: OutputSampleBufferCapturer( + faceDetector: faceDetector, + videoChunker: videoChunker + ) + ) + + self._viewModel = StateObject( + wrappedValue: .init( + faceDetector: faceDetector, + faceInOvalMatching: faceInOvalStateMatching, + captureSession: captureSession, + videoChunker: videoChunker, + closeButtonAction: { onCompletion(.failure(.userCancelled)) }, + sessionID: sessionID + ) + ) + } + + init( + sessionID: String, + credentialsProvider: AWSCredentialsProvider? = nil, + region: String, + disableStartView: Bool = false, + isPresented: Binding, + onCompletion: @escaping (Result) -> Void, + captureSession: LivenessCaptureSession + ) { + self.disableStartView = disableStartView + self._isPresented = isPresented + self.onCompletion = onCompletion + + self.sessionTask = Task { + let session = try await AWSPredictionsPlugin.startFaceLivenessSession( + withID: sessionID, + credentialsProvider: credentialsProvider, + region: region, + options: .init(), + completion: map(detectionCompletion: onCompletion) + ) + return session + } + + let faceInOvalStateMatching = FaceInOvalMatching( + instructor: Instructor() + ) + + self._viewModel = StateObject( + wrappedValue: .init( + faceDetector: captureSession.outputSampleBufferCapturer!.faceDetector, + faceInOvalMatching: faceInOvalStateMatching, + captureSession: captureSession, + videoChunker: captureSession.outputSampleBufferCapturer!.videoChunker, + closeButtonAction: { onCompletion(.failure(.userCancelled)) }, + sessionID: sessionID + ) + ) + } + + public var body: some View { + switch displayState { + case .awaitingLivenessSession: + Color.clear + .onAppear { + Task { + do { + let newState = disableStartView + ? DisplayState.displayingLiveness + : DisplayState.displayingGetReadyView + guard self.displayState != newState else { return } + let session = try await sessionTask.value + viewModel.livenessService = session + viewModel.registerServiceEvents() + self.displayState = newState + } catch { + throw FaceLivenessDetectionError.accessDenied + } + } + } + + case .displayingGetReadyView: + GetReadyPageView( + onBegin: { + guard displayState != .displayingLiveness else { return } + displayState = .displayingLiveness + }, + beginCheckButtonDisabled: false + ) + .onAppear { + DispatchQueue.main.async { + UIScreen.main.brightness = 1.0 + } + } + case .displayingLiveness: + _FaceLivenessDetectionView( + viewModel: viewModel, + videoView: { + CameraView( + faceLivenessDetectionViewModel: viewModel + ) + } + ) + .onAppear { + DispatchQueue.main.async { + UIScreen.main.brightness = 1.0 + } + } + .onDisappear() { + viewModel.stopRecording() + } + .onReceive(viewModel.$livenessState) { output in + switch output.state { + case .completed: + isPresented = false + onCompletion(.success(())) + case .encounteredUnrecoverableError(let error): + let closeCode = error.webSocketCloseCode ?? .normalClosure + viewModel.livenessService?.closeSocket(with: closeCode) + isPresented = false + onCompletion(.failure(mapError(error))) + default: + break + } + } + case .awaitingCameraPermission: + CameraPermissionView(displayingCameraPermissionsNeededAlert: $displayingCameraPermissionsNeededAlert) + .onAppear { + checkCameraPermission() + } + } + } + + func mapError(_ livenessError: LivenessStateMachine.LivenessError) -> FaceLivenessDetectionError { + switch livenessError { + case .userCancelled, .viewResignation: + return .userCancelled + case .timedOut: + return .faceInOvalMatchExceededTimeLimitError + case .socketClosed: + return .socketClosed + case .cameraNotAvailable: + return .cameraNotAvailable + default: + return .cameraPermissionDenied + } + } + + private func requestCameraPermission() { + AVCaptureDevice.requestAccess( + for: .video, + completionHandler: { accessGranted in + guard accessGranted == true else { return } + displayState = .awaitingLivenessSession + } + ) + + } + + private func alertCameraAccessNeeded() { + displayingCameraPermissionsNeededAlert = true + } + + private func checkCameraPermission() { + let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) + switch cameraAuthorizationStatus { + case .notDetermined: + requestCameraPermission() + case .restricted, .denied: + alertCameraAccessNeeded() + case .authorized: + displayState = .awaitingLivenessSession + @unknown default: + break + } + } +} + +enum DisplayState { + case awaitingLivenessSession + case displayingGetReadyView + case displayingLiveness + case awaitingCameraPermission +} + +enum InstructionState { + case none + case display(text: String) +} + +private func map(detectionCompletion: @escaping (Result) -> Void) -> ((Result) -> Void) { + { result in + switch result { + case .success: + detectionCompletion(.success(())) + case .failure(.invalidRegion): + detectionCompletion(.failure(.invalidRegion)) + case .failure(.accessDenied): + detectionCompletion(.failure(.accessDenied)) + case .failure(.validation): + detectionCompletion(.failure(.validation)) + case .failure(.internalServer): + detectionCompletion(.failure(.internalServer)) + case .failure(.throttling): + detectionCompletion(.failure(.throttling)) + case .failure(.serviceQuotaExceeded): + detectionCompletion(.failure(.serviceQuotaExceeded)) + case .failure(.serviceUnavailable): + detectionCompletion(.failure(.serviceUnavailable)) + case .failure(.sessionNotFound): + detectionCompletion(.failure(.sessionNotFound)) + case .failure(.invalidSignature): + detectionCompletion(.failure(.invalidSignature)) + default: + detectionCompletion(.failure(.unknown)) + } + } +} diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift new file mode 100644 index 00000000..99e92ee2 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift @@ -0,0 +1,140 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import SwiftUI +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +fileprivate let initialFaceDistanceThreshold: CGFloat = 0.32 + +extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler { + func process(newResult: FaceDetectionResult) { + switch newResult { + case .noFace: + if case .pendingFacePreparedConfirmation = livenessState.state { + DispatchQueue.main.async { + self.livenessState.faceNotPrepared(reason: .noFace) + } + } + case .multipleFaces: + if case .pendingFacePreparedConfirmation = livenessState.state { + DispatchQueue.main.async { + self.livenessState.faceNotPrepared(reason: .multipleFaces) + } + } + case .singleFace(let face): + var normalizedFace = normalizeFace(face) + normalizedFace.boundingBox = normalizedFace.boundingBoxFromLandmarks(ovalRect: ovalRect) + + switch livenessState.state { + case .pendingFacePreparedConfirmation: + if face.faceDistance <= initialFaceDistanceThreshold { + DispatchQueue.main.async { + self.livenessState.awaitingRecording() + self.initializeLivenessStream() + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.livenessState.beginRecording() + } + return + } else { + DispatchQueue.main.async { + self.livenessState.faceNotPrepared(reason: .faceTooClose) + } + return + } + case .recording(ovalDisplayed: false): + drawOval(onComplete: { + self.sendInitialFaceDetectedEvent( + initialFace: normalizedFace.boundingBox, + videoStartTime: Date().timestampMilliseconds + ) + }) + case .recording(ovalDisplayed: true): + guard let sessionConfiguration = sessionConfiguration else { return } + let instruction = faceInOvalMatching.faceMatchState( + for: normalizedFace.boundingBox, + in: ovalRect, + challengeConfig: sessionConfiguration.ovalMatchChallenge + ) + + handleInstruction( + instruction, + colorSequences: sessionConfiguration.colorChallenge.colors + ) + case .awaitingFaceInOvalMatch: + guard let sessionConfiguration = sessionConfiguration else { return } + let instruction = faceInOvalMatching.faceMatchState( + for: normalizedFace.boundingBox, + in: ovalRect, + challengeConfig: sessionConfiguration.ovalMatchChallenge + ) + handleInstruction( + instruction, + colorSequences: sessionConfiguration.colorChallenge.colors + ) + default: break + + } + } + } + + func handleNoFaceFit(instruction: Instructor.Instruction, percentage: Double) { + self.livenessState.awaitingFaceMatch(with: instruction, nearnessPercentage: percentage) + if noFitStartTime == nil { + noFitStartTime = Date() + } + if let elapsedTime = noFitStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noFitTimeoutInterval { + handleSessionTimedOut() + } + } + + func handleNoFaceDetected() { + if noFitStartTime == nil { + noFitStartTime = Date() + } + if let elapsedTime = noFitStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noFitTimeoutInterval { + handleSessionTimedOut() + } + } + + func handleInstruction( + _ instruction: Instructor.Instruction, + colorSequences: [FaceLivenessSession.DisplayColor] + ) { + DispatchQueue.main.async { + switch instruction { + case .match: + self.livenessState.faceMatched() + self.faceMatchedTimestamp = Date().timestampMilliseconds + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.livenessViewControllerDelegate?.displayFreshness(colorSequences: colorSequences) + } + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + self.noFitStartTime = nil + + case .tooClose(_, let percentage), + .tooFar(_, let percentage), + .tooFarLeft(_, let percentage), + .tooFarRight(_, let percentage): + self.handleNoFaceFit(instruction: instruction, percentage: percentage) + case .none: + self.handleNoFaceDetected() + } + } + } + + private func handleSessionTimedOut() { + noFitStartTime = nil + DispatchQueue.main.async { + self.livenessState + .unrecoverableStateEncountered(.timedOut) + self.captureSession.stopRunning() + } + } +} diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+VideoSegmentProcessor.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+VideoSegmentProcessor.swift new file mode 100644 index 00000000..c2ed2b39 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+VideoSegmentProcessor.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension FaceLivenessDetectionViewModel: VideoSegmentProcessor { + func process(initalSegment: Data, currentSeparableSegment: Data) { + let chunk = chunk(initial: initalSegment, current: currentSeparableSegment) + sendVideoEvent(data: chunk, videoEventTime: .zero) + if !hasSentFinalVideoEvent, + case .completedDisplayingFreshness = livenessState.state { + DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 0.9) { + self.sendFinalVideoEvent() + } + } + } +} diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift new file mode 100644 index 00000000..7f834cb2 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift @@ -0,0 +1,364 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import SwiftUI +import AVFoundation +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +fileprivate let videoSize: CGSize = .init(width: 480, height: 640) +fileprivate let defaultNoFitTimeoutInterval: TimeInterval = 7 + +@MainActor +class FaceLivenessDetectionViewModel: ObservableObject { + @Published var readyForOval = false + @Published var isRecording = false + @Published var livenessState: LivenessStateMachine + + weak var livenessViewControllerDelegate: FaceLivenessViewControllerPresenter? + let captureSession: LivenessCaptureSession + var closeButtonAction: () -> Void + let videoChunker: VideoChunker + let sessionID: String + var livenessService: LivenessService? + let faceDetector: FaceDetector + let faceInOvalMatching: FaceInOvalMatching + let challengeID: String = UUID().uuidString + var colorSequences: [ColorSequence] = [] + var hasSentFinalVideoEvent = false + var hasSentFirstVideo = false + var layerRectConverted: (CGRect) -> CGRect = { $0 } + var sessionConfiguration: FaceLivenessSession.SessionConfiguration? + var normalizeFace: (DetectedFace) -> DetectedFace = { $0 } + var provideSingleFrame: ((UIImage) -> Void)? + var cameraViewRect = CGRect.zero + var ovalRect = CGRect.zero + var faceGuideRect: CGRect! + var initialClientEvent: InitialClientEvent? + var faceMatchedTimestamp: UInt64? + var noFitStartTime: Date? + + var noFitTimeoutInterval: TimeInterval { + if let sessionTimeoutMilliSec = sessionConfiguration?.ovalMatchChallenge.oval.ovalFitTimeout { + return TimeInterval(sessionTimeoutMilliSec/1_000) + } else { + return defaultNoFitTimeoutInterval + } + } + + init( + faceDetector: FaceDetector, + faceInOvalMatching: FaceInOvalMatching, + captureSession: LivenessCaptureSession, + videoChunker: VideoChunker, + stateMachine: LivenessStateMachine = .init(state: .initial), + closeButtonAction: @escaping () -> Void, + sessionID: String + ) { + self.closeButtonAction = closeButtonAction + self.videoChunker = videoChunker + self.livenessState = stateMachine + self.sessionID = sessionID + self.captureSession = captureSession + self.faceDetector = faceDetector + self.faceInOvalMatching = faceInOvalMatching + + self.closeButtonAction = { [weak self] in + guard let self else { return } + DispatchQueue.main.async { + self.stopRecording() + self.livenessState.unrecoverableStateEncountered(.userCancelled) + } + } + + faceDetector.setResultHandler(detectionResultHandler: self) + videoChunker.assetWriterDelegate.set(segmentProcessor: self) + + NotificationCenter.default.addObserver( + self, + selector: #selector(willResignActive), + name: UIScene.willDeactivateNotification, object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + func registerServiceEvents() { + livenessService?.register(onComplete: { [weak self] reason in + self?.stopRecording() + + switch reason { + case .disconnectionEvent: + DispatchQueue.main.async { + self?.livenessState.complete() + } + case .unexpectedClosure: + DispatchQueue.main.async { + self?.livenessState + .unrecoverableStateEncountered(.socketClosed) + } + } + }) + + livenessService?.register( + listener: { [weak self] _sessionConfiguration in + self?.sessionConfiguration = _sessionConfiguration + }, + on: .challenge + ) + } + + @objc func willResignActive(_ notification: Notification) { + guard self.livenessState.state != .initial else { return } + DispatchQueue.main.async { + self.stopRecording() + self.livenessState.unrecoverableStateEncountered(.viewResignation) + } + } + + func startSession() { + captureSession.startSession() + } + + func stopRecording() { + captureSession.stopRunning() + } + + func configureCamera(withinFrame frame: CGRect) -> CALayer? { + do { + let avLayer = try captureSession.configureCamera(frame: frame) + DispatchQueue.main.async { + self.livenessState.checkIsFacePrepared() + } + return avLayer + } catch { + DispatchQueue.main.async { + self.livenessState.unrecoverableStateEncountered( + self.generateLivenessError(from: error) + ) + } + return nil + } + } + + func drawOval(onComplete: @escaping () -> Void) { + guard livenessState.state == .recording(ovalDisplayed: false), + let ovalParameters = sessionConfiguration?.ovalMatchChallenge.oval + else { return } + + let scaleRatio = cameraViewRect.width / videoSize.width + let rect = CGRect( + x: ovalParameters.boundingBox.x, + y: ovalParameters.boundingBox.y, + width: ovalParameters.boundingBox.width, + height: ovalParameters.boundingBox.height + ) + + let normalizedOvalRect = CGRect( + x: rect.minX * scaleRatio, + y: rect.minY * scaleRatio, + width: rect.width * scaleRatio, + height: rect.height * scaleRatio + ) + + livenessViewControllerDelegate?.drawOvalInCanvas(normalizedOvalRect) + DispatchQueue.main.async { + self.livenessState.ovalDisplayed() + onComplete() + } + ovalRect = normalizedOvalRect + } + + + func initializeLivenessStream() { + do { + try livenessService?.initializeLivenessStream( + withSessionID: sessionID, + userAgent: UserAgentValues.standard().userAgentString + ) + } catch { + DispatchQueue.main.async { + self.livenessState.unrecoverableStateEncountered(.couldNotOpenStream) + } + } + } + + func sendColorDisplayedEvent( + _ event: Freshness.ColorEvent + ) { + let freshnessEvent = FreshnessEvent( + challengeID: challengeID, + color: event.currentColor.rgb._values, + sequenceNumber: event.sequenceNumber, + timestamp: event.colorStartTime, + previousColor: event.previousColor.rgb._values + ) + + do { + try livenessService?.send( + .freshness(event: freshnessEvent), + eventDate: { .init() } + ) + } catch { + DispatchQueue.main.async { + self.livenessState.unrecoverableStateEncountered(.unknown) + } + } + } + + func boundingBox(for cgRect: CGRect, relativeTo canvas: CGRect) -> FaceLivenessSession.BoundingBox { + .init( + x: cgRect.minX / cameraViewRect.width, + y: cgRect.minY / cameraViewRect.height, + width: cgRect.width / cameraViewRect.width, + height: cgRect.height / cameraViewRect.height + ) + } + + func sendInitialFaceDetectedEvent( + initialFace: CGRect, + videoStartTime: UInt64 + ) { + guard initialClientEvent == nil else { return } + videoChunker.start() + + let initialFace = FaceDetection( + boundingBox: boundingBox(for: initialFace, relativeTo: cameraViewRect), + startTimestamp: videoStartTime + ) + + let _initialClientEvent = InitialClientEvent( + challengeID: challengeID, + initialFaceLocation: initialFace, + videoStartTime: videoStartTime + ) + + initialClientEvent = _initialClientEvent + + do { + try livenessService?.send( + .initialFaceDetected(event: _initialClientEvent), + eventDate: { .init() } + ) + } catch { + DispatchQueue.main.async { + self.livenessState.unrecoverableStateEncountered(.unknown) + } + } + } + + func sendFinalEvent( + targetFaceRect: CGRect, + viewSize: CGSize, + faceMatchedEnd: UInt64 + ) { + guard + let sessionConfiguration, + let initialClientEvent, + let faceMatchedTimestamp + else { return } + + let finalClientEvent = FinalClientEvent( + sessionConfiguration: sessionConfiguration, + initialClientEvent: initialClientEvent, + videoSize: videoSize, + faceMatchedStart: faceMatchedTimestamp, + faceMatchedEnd: faceMatchedEnd, + videoEnd: Date().timestampMilliseconds + ) + + do { + try livenessService?.send( + .final(event: finalClientEvent), + eventDate: { .init() } + ) + + sendVideoEvent( + data: .init(), + videoEventTime: Date().timestampMilliseconds + ) + hasSentFinalVideoEvent = true + + } catch { + DispatchQueue.main.async { + self.livenessState.unrecoverableStateEncountered(.unknown) + } + } + } + + func sendFinalVideoEvent() { + sendFinalEvent( + targetFaceRect: faceGuideRect, + viewSize: videoSize, + faceMatchedEnd: Date().timestampMilliseconds + ) + + videoChunker.finish { [weak livenessViewControllerDelegate] image in + livenessViewControllerDelegate?.displaySingleFrame(uiImage: image) + } + } + + func handleFreshnessComplete(faceGuide: CGRect) { + DispatchQueue.main.async { + self.livenessState.completedDisplayingFreshness() + self.faceGuideRect = faceGuide + } + } + + func sendVideoEvent(data: Data, videoEventTime: UInt64) { + guard !hasSentFinalVideoEvent else { return } + let eventDate = Date() + let timestamp = eventDate.timestampMilliseconds + + let videoEvent = VideoEvent.init(chunk: data, timestamp: timestamp) + + do { + try livenessService?.send( + .video(event: videoEvent), + eventDate: { eventDate } + ) + } catch { + DispatchQueue.main.async { + self.livenessState.unrecoverableStateEncountered(.unknown) + } + } + } + + private func generateLivenessError(from captureSessionError: Error) -> LivenessStateMachine.LivenessError { + guard let captureSessionError = captureSessionError as? LivenessCaptureSessionError else { return .unknown } + + let livenessError: LivenessStateMachine.LivenessError + + switch captureSessionError { + case LivenessCaptureSessionError.cameraUnavailable, + LivenessCaptureSessionError.deviceInputUnavailable: + let authStatus = AVCaptureDevice.authorizationStatus(for: .video) + livenessError = authStatus == .authorized ? .cameraNotAvailable : .missingVideoPermission + case LivenessCaptureSessionError.captureSessionOutputUnavailable, + LivenessCaptureSessionError.captureSessionInputUnavailable: + + livenessError = .errorWithUnderlyingOSFramework + default: + livenessError = .unknown + } + + return livenessError + } + + func chunk(initial: Data, current: Data) -> Data { + let data: Data + if hasSentFirstVideo { + data = current + } else { + data = initial + current + hasSentFirstVideo = true + } + return data + } +} diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift new file mode 100644 index 00000000..5786620b --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +protocol FaceLivenessViewControllerPresenter: AnyObject { + func drawOvalInCanvas(_ ovalRect: CGRect) + func displayFreshness(colorSequences: [FaceLivenessSession.DisplayColor]) + func displaySingleFrame(uiImage: UIImage) +} diff --git a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift new file mode 100644 index 00000000..c59629c9 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift @@ -0,0 +1,177 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +struct LivenessStateMachine { + private(set) var state: State + + init(state: LivenessStateMachine.State) { + self.state = state + } + + mutating func checkIsFacePrepared() { + guard case .initial = state else { return } + state = .pendingFacePreparedConfirmation(.pendingCheck) + } + + mutating func faceNotPrepared(reason: FaceNotPreparedReason) { + guard case .pendingFacePreparedConfirmation = state else { return } + state = .pendingFacePreparedConfirmation(reason) + } + + mutating func awaitingFaceMatch(with instruction: Instructor.Instruction, nearnessPercentage: Double) { + let reason: FaceNotPreparedReason + let percentage: Double + switch instruction { + case .tooFar(_, let nearnessPercentage): + reason = .moveFaceCloser + percentage = nearnessPercentage + case .tooFarLeft(_, let nearnessPercentage): + reason = .moveFaceLeft + percentage = nearnessPercentage + case .tooFarRight(_, let nearnessPercentage): + reason = .moveFaceRight + percentage = nearnessPercentage + case .tooClose(_, let nearnessPercentage): + reason = .faceTooClose + percentage = nearnessPercentage + default: return + } + + state = .awaitingFaceInOvalMatch(reason, percentage) + } + + mutating func awaitingRecording() { + guard case .pendingFacePreparedConfirmation = state else { return } + state = .waitForRecording + } + + mutating func unrecoverableStateEncountered(_ error: LivenessError) { + switch state { + case .encounteredUnrecoverableError, .completed: + return + default: + state = .encounteredUnrecoverableError(error) + } + } + + mutating func beginRecording() { + state = .recording(ovalDisplayed: false) + } + + mutating func ovalDisplayed() { + state = .recording(ovalDisplayed: true) + } + + mutating func faceMatched() { + state = .faceMatched + } + + mutating func completedDisplayingFreshness() { + state = .completedDisplayingFreshness + } + + mutating func displayingFreshness() { + state = .displayingFreshness + } + + mutating func complete() { + state = .completed + } + + var shouldDisplayRecordingIcon: Bool { + switch state { + case .initial, .pendingFacePreparedConfirmation, .encounteredUnrecoverableError: + return false + default: return true + } + } + + enum State: Equatable { + case initial + case pendingFacePreparedConfirmation(FaceNotPreparedReason) + case recording(ovalDisplayed: Bool) + case awaitingFaceInOvalMatch(FaceNotPreparedReason, Double) + case faceMatched + case initialClientInfoEventSent + case displayingFreshness + case completedDisplayingFreshness + case completed + case awaitingDisconnectEvent + case disconnectEventReceived + case encounteredUnrecoverableError(LivenessError) + case waitForRecording + } + + enum FaceNotPreparedReason { + case pendingCheck + case notInOval + case moveFaceCloser + case moveFaceRight + case moveFaceLeft + case moveToDimmerArea + case moveToBrighterArea + case noFace + case multipleFaces + case faceTooClose + + var localizedValue: String { + switch self { + case .pendingCheck: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_pendingCheck + case .notInOval: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_not_in_oval + case .moveFaceCloser: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_move_face_closer + case .moveFaceRight: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_move_face_right + case .moveFaceLeft: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_move_face_left + case .moveToDimmerArea: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_move_to_dimmer_area + case .moveToBrighterArea: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_move_to_brighter_area + case .noFace: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_no_face + case .multipleFaces: + return LocalizedStrings.challenge_instruction_multiple_faces_detected + case .faceTooClose: + return LocalizedStrings.amplify_ui_liveness_face_not_prepared_reason_face_too_close + } + } + } + + struct LivenessError: Error, Equatable { + let code: UInt8 + let webSocketCloseCode: URLSessionWebSocketTask.CloseCode? + + static let unknown = LivenessError(code: 0, webSocketCloseCode: .unexpectedRuntimeError) + static let missingVideoPermission = LivenessError(code: 1, webSocketCloseCode: .missingVideoPermission) + static let errorWithUnderlyingOSFramework = LivenessError(code: 2, webSocketCloseCode: .unexpectedRuntimeError) + static let userCancelled = LivenessError(code: 3, webSocketCloseCode: .ovalFitUserClosedSession) + static let timedOut = LivenessError(code: 4, webSocketCloseCode: .ovalFitMatchTimeout) + static let couldNotOpenStream = LivenessError(code: 5, webSocketCloseCode: .unexpectedRuntimeError) + static let socketClosed = LivenessError(code: 6, webSocketCloseCode: .normalClosure) + static let viewResignation = LivenessError(code: 8, webSocketCloseCode: .viewClosure) + static let cameraNotAvailable = LivenessError(code: 9, webSocketCloseCode: .missingVideoPermission) + + static func == (lhs: LivenessError, rhs: LivenessError) -> Bool { + lhs.code == rhs.code + } + } +} + +extension URLSessionWebSocketTask.CloseCode { + static let ovalFitMatchTimeout = URLSessionWebSocketTask.CloseCode(rawValue: 4001) + static let ovalFitTimeOutNoFaceDetected = URLSessionWebSocketTask.CloseCode(rawValue: 4002) + static let ovalFitUserClosedSession = URLSessionWebSocketTask.CloseCode(rawValue: 4003) + static let viewClosure = URLSessionWebSocketTask.CloseCode(rawValue: 4004) + static let unexpectedRuntimeError = URLSessionWebSocketTask.CloseCode(rawValue: 4005) + static let missingVideoPermission = URLSessionWebSocketTask.CloseCode(rawValue: 4006) +} diff --git a/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift b/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift new file mode 100644 index 00000000..c274bde0 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift @@ -0,0 +1,176 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import AVFoundation +import Vision +import Amplify +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +final class _LivenessViewController: UIViewController { + let viewModel: FaceLivenessDetectionViewModel + var previewLayer: CALayer? + + let faceShapeLayer = CAShapeLayer() + var ovalExists = false + var ovalRect: CGRect? + var freshness = Freshness() + let freshnessView = FreshnessView() + var readyForOval = false + + init( + viewModel: FaceLivenessDetectionViewModel + ) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + viewModel.livenessViewControllerDelegate = self + + + viewModel.normalizeFace = { [weak self] face in + guard let self = self else { return face } + return DispatchQueue.main.sync { + face.normalize(width: self.view.frame.width, height: self.view.frame.width / 3 * 4) + } + } + } + + deinit { + guard let previewLayer = self.previewLayer else { return } + previewLayer.removeFromSuperlayer() + (previewLayer as? AVCaptureVideoPreviewLayer)?.session = nil + self.previewLayer = nil + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + layoutSubviews() + setupAVLayer() + } + + override func viewDidLayoutSubviews() { + previewLayer?.position = view.center + } + + private func layoutSubviews() { + freshnessView.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(freshnessView) + NSLayoutConstraint.activate([ + freshnessView.topAnchor.constraint(equalTo: view.topAnchor), + freshnessView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + freshnessView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + freshnessView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + freshnessView.clearColors() + } + + private func setupAVLayer() { + guard previewLayer == nil else { return } + let x = view.frame.minX + let y = view.frame.minY + let width = view.frame.width + let height = width / 3 * 4 + let cameraFrame = CGRect(x: x, y: y, width: width, height: height) + + guard let avLayer = viewModel.configureCamera(withinFrame: cameraFrame) else { + DispatchQueue.main.async { + self.viewModel.livenessState + .unrecoverableStateEncountered(.missingVideoPermission) + } + return + } + + avLayer.position = view.center + self.previewLayer = avLayer + if let previewLayer = self.previewLayer { + viewModel.cameraViewRect = previewLayer.frame + } + + DispatchQueue.main.async { + self.view.layer.insertSublayer(avLayer, at: 0) + self.view.layoutIfNeeded() + + self.viewModel.startSession() + } + } + + var runningFreshness = false + var hasSentClientInformationEvent = false + var challengeID = UUID().uuidString + var initialFace: FaceDetection? + var videoStartTimeStamp: UInt64? + var faceMatchStartTime: UInt64? + var faceGuideRect: CGRect? + var freshnessEventsComplete = false + var videoSentCount = 0 + var hasSentFinalEvent = false + var hasSentEmptyFinalVideoEvent = false + var ovalView: OvalView? + + + required init?(coder: NSCoder) { fatalError() } +} + +extension _LivenessViewController: FaceLivenessViewControllerPresenter { + func displaySingleFrame(uiImage: UIImage) { + DispatchQueue.main.async { + guard let previewLayer = self.previewLayer else { return } + let imageView = UIImageView(image: uiImage) + imageView.frame = previewLayer.frame + self.view.addSubview(imageView) + (previewLayer as? AVCaptureVideoPreviewLayer)?.session = nil + previewLayer.removeFromSuperlayer() + self.viewModel.stopRecording() + } + } + + func displayFreshness(colorSequences: [FaceLivenessSession.DisplayColor]) { + self.ovalView?.setNeedsDisplay() + DispatchQueue.main.async { + self.viewModel.livenessState.displayingFreshness() + } + self.freshness.showColorSequences( + colorSequences, + width: UIScreen.main.bounds.width, + height: UIScreen.main.bounds.height, + view: self.freshnessView, + onNewColor: { [weak self] colorEvent in + self?.viewModel.sendColorDisplayedEvent(colorEvent) + }, + onComplete: { [weak self] in + guard let self else { return } + self.freshnessView.removeFromSuperview() + + self.viewModel.handleFreshnessComplete( + faceGuide: self.faceGuideRect! + ) + } + ) + } + + func drawOvalInCanvas(_ ovalRect: CGRect) { + DispatchQueue.main.async { + guard let previewLayer = self.previewLayer else { return } + self.faceGuideRect = ovalRect + + let ovalView = OvalView( + frame: previewLayer.frame, + ovalFrame: ovalRect + ) + self.ovalView = ovalView + ovalView.center = previewLayer.position + self.view.insertSubview( + ovalView, + belowSubview: self.freshnessView + ) + + self.ovalRect = ovalRect + self.ovalExists = true + } + } +} diff --git a/Sources/FaceLiveness/Views/Liveness/_FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/_FaceLivenessDetectionView.swift new file mode 100644 index 00000000..5113bf54 --- /dev/null +++ b/Sources/FaceLiveness/Views/Liveness/_FaceLivenessDetectionView.swift @@ -0,0 +1,61 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct _FaceLivenessDetectionView: View { + let videoView: VideoView + @ObservedObject var viewModel: FaceLivenessDetectionViewModel + @Binding var displayResultsView: Bool + + init( + viewModel: FaceLivenessDetectionViewModel, + @ViewBuilder videoView: @escaping () -> VideoView + ) { + self.viewModel = viewModel + self.videoView = videoView() + + self._displayResultsView = .init( + get: { viewModel.livenessState.state == .completed }, + set: { _ in } + ) + } + + var body: some View { + ZStack { + Color.black + ZStack { + videoView + VStack { + HStack(alignment: .top) { + if viewModel.livenessState.shouldDisplayRecordingIcon { + RecordingButton() + .accessibilityHidden(true) + } + + Spacer() + + CloseButton( + action: viewModel.closeButtonAction + ) + } + .padding() + + InstructionContainerView( + viewModel: viewModel + ) + + Spacer() + } + .padding([.leading, .trailing]) + .aspectRatio(3/4, contentMode: .fit) + .frame(maxWidth: .infinity) + } + } + .edgesIgnoringSafeArea(.all) + } +} diff --git a/Sources/FaceLiveness/Views/OvalView.swift b/Sources/FaceLiveness/Views/OvalView.swift new file mode 100644 index 00000000..60a6e18a --- /dev/null +++ b/Sources/FaceLiveness/Views/OvalView.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import UIKit + +class OvalView: UIView { + let ovalFrame: CGRect + + init(frame: CGRect, ovalFrame: CGRect) { + self.ovalFrame = ovalFrame + super.init(frame: frame) + backgroundColor = .clear + } + + override func draw(_ rect: CGRect) { + let mask = UIBezierPath(rect: bounds) + let oval = UIBezierPath(ovalIn: ovalFrame) + mask.append(oval.reversing()) + + UIColor.white.withAlphaComponent(0.9).setFill() + mask.fill() + + UIColor.clear.setFill() + UIColor.white.setStroke() + oval.lineWidth = 8 + oval.stroke() + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/FaceLiveness/Views/ProgressBarView.swift b/Sources/FaceLiveness/Views/ProgressBarView.swift new file mode 100644 index 00000000..fbba9108 --- /dev/null +++ b/Sources/FaceLiveness/Views/ProgressBarView.swift @@ -0,0 +1,39 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct ProgressBarView: View { + let emptyColor: Color + let borderColor: Color + let fillColor: Color + let indicatorColor: Color + let percentage: Double + + var body: some View { + GeometryReader { proxy in + ZStack(alignment: .leading) { + Rectangle() + .border(borderColor, width: 1) + .cornerRadius(8, corners: .allCorners) + .frame( + width: proxy.size.width, + height: proxy.size.height - 8 + ) + .foregroundColor(emptyColor) + + Rectangle() + .cornerRadius(8, corners: .allCorners) + .frame( + width: min(percentage, 1) * proxy.size.width, + height: proxy.size.height - 8 + ) + .foregroundColor(fillColor) + } + } + } +} diff --git a/Sources/FaceLiveness/Views/RecordingButton.swift b/Sources/FaceLiveness/Views/RecordingButton.swift new file mode 100644 index 00000000..0a157278 --- /dev/null +++ b/Sources/FaceLiveness/Views/RecordingButton.swift @@ -0,0 +1,34 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct RecordingButton: View { + var body: some View { + VStack(alignment: .center) { + Circle() + .foregroundColor(.hex("#F92626")) + .frame(width: 17, height: 17) + Text(LocalizedStrings.challenge_recording_indicator_label) + .font(.system(size: 12)) + .fontWeight(.bold) + } + .padding([.top, .bottom], 12) + .padding([.leading, .trailing], 8) + .background(Color.livenessBackground) + .cornerRadius(8) + } +} + +struct RecordingButton_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.gray + RecordingButton() + } + } +} diff --git a/Sources/FaceLiveness/Views/RoundedCornerShape.swift b/Sources/FaceLiveness/Views/RoundedCornerShape.swift new file mode 100644 index 00000000..2cc77bd8 --- /dev/null +++ b/Sources/FaceLiveness/Views/RoundedCornerShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} + +extension View { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape(RoundedCorner(radius: radius, corners: corners)) + } +} diff --git a/Sources/FaceLiveness/Views/WarningBox.swift b/Sources/FaceLiveness/Views/WarningBox.swift new file mode 100644 index 00000000..8cf89319 --- /dev/null +++ b/Sources/FaceLiveness/Views/WarningBox.swift @@ -0,0 +1,60 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct WarningBox: View { + @State var isPresentingPopover = false + let titleText: String + let bodyText: String + let popoverContent: PopoverView + + init( + titleText: String, + bodyText: String, + @ViewBuilder popoverContent: () -> PopoverView + ) { + self.titleText = titleText + self.bodyText = bodyText + self.popoverContent = popoverContent() + } + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(titleText) + .fontWeight(.semibold) + .foregroundColor(.livenessWarningLabel) + + Text(bodyText) + .foregroundColor(.livenessWarningLabel) + } + Spacer() + Button( + action: { isPresentingPopover = true }, + label: { + Image(systemName: "info.circle") + .foregroundColor(.livenessWarningLabel) + .frame(width: 20, height: 20) + } + ) + .frame(width: 44, height: 44) + .popover( + isPresented: $isPresentingPopover, + attachmentAnchor: .point(.top), + arrowEdge: .bottom, + content: { popoverContent } + ) + } + .padding() + .background( + Rectangle() + .foregroundColor(Color(UIColor.hex("#FF8473"))) + .cornerRadius(6) + ) + } +} diff --git a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift new file mode 100644 index 00000000..7d69251b --- /dev/null +++ b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift @@ -0,0 +1,115 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SwiftUI +import AWSPluginsCore +@testable import FaceLiveness +@testable import AWSPredictionsPlugin +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +@MainActor +final class CredentialsProviderTestCase: XCTestCase { + var videoChunker: VideoChunker! + var viewModel: FaceLivenessDetectionViewModel! + var faceDetector: MockFaceDetector! + var livenessService: MockLivenessService! + + override func setUp() { + faceDetector = MockFaceDetector() + livenessService = MockLivenessService() + let videoChunker = VideoChunker( + assetWriter: LivenessAVAssetWriter(), + assetWriterDelegate: VideoChunker.AssetWriterDelegate(), + assetWriterInput: LivenessAVAssetWriterInput() + ) + let captureSession = LivenessCaptureSession( + captureDevice: .init(avCaptureDevice: nil), + outputDelegate: OutputSampleBufferCapturer( + faceDetector: faceDetector, + videoChunker: videoChunker + ) + ) + + let viewModel = FaceLivenessDetectionViewModel( + faceDetector: faceDetector, + faceInOvalMatching: .init(instructor: .init()), + captureSession: captureSession, + videoChunker: videoChunker, + closeButtonAction: {}, + sessionID: UUID().uuidString + ) + + self.videoChunker = videoChunker + self.viewModel = viewModel + } + + /// Given: A `FaceLivenessDetectorView` + /// When: The callsite provides an `AWSCredentialsProvider` conforming type that provides + /// an `AWSCredentials` conforming type + /// Then: The provided `accessKeyId` and `secretAccessKey` should + /// match that of credentials used by the underlying `SigV4Signer`. **And** the + /// sessionToken should be `nil` + func testUsesProvidedCredentialsProvider() async throws { + let accessKey = "AKIAIOSFODNN7EXAMPLE" + let secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + let credentialsProvider = MockCredentialsProvider { + MockAWSCredentials(accessKeyId: accessKey, secretAccessKey: secretKey) + } + + let liveness = FaceLivenessDetectorView( + sessionID: UUID().uuidString, + credentialsProvider: credentialsProvider, + region: "us-east-1", + isPresented: .constant(true), + onCompletion: { _ in } + ) + + let session = try await liveness.sessionTask.value + let credential = session.signer.credential + + XCTAssertEqual(accessKey, credential.accessKey) + XCTAssertEqual(secretKey, credential.secretKey) + XCTAssertNil(credential.sessionToken) + } + + + /// Given: A `FaceLivenessDetectorView` + /// When: The callsite provides an `AWSCredentialsProvider` conforming type that provides + /// an `AWSTemporaryCredentials` conforming type + /// Then: The provided `accessKeyId`, `secretAccessKey`, **and** `sessionToken` should + /// match that of credentials used by the underlying `SigV4Signer` + func testUsesProvidedCredentialsProvider_temporaryCredentials() async throws { + let accessKey = "AKIAIOSFODNN7EXAMPLE" + let secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + let sessionToken = "MOCK_SESSION_TOKEN" + + let credentialsProvider = MockCredentialsProvider { + MockAWSTemporaryCredentials( + sessionToken: sessionToken, + expiration: .distantFuture, + accessKeyId: accessKey, + secretAccessKey: secretKey + ) + } + + let liveness = FaceLivenessDetectorView( + sessionID: UUID().uuidString, + credentialsProvider: credentialsProvider, + region: "us-east-1", + isPresented: .constant(true), + onCompletion: { _ in } + ) + + let session = try await liveness.sessionTask.value + let credential = session.signer.credential + + XCTAssertEqual(accessKey, credential.accessKey) + XCTAssertEqual(secretKey, credential.secretKey) + XCTAssertEqual(sessionToken, credential.sessionToken) + } +} diff --git a/Tests/FaceLivenessTests/DetectedFaceTests.swift b/Tests/FaceLivenessTests/DetectedFaceTests.swift new file mode 100644 index 00000000..4bee8292 --- /dev/null +++ b/Tests/FaceLivenessTests/DetectedFaceTests.swift @@ -0,0 +1,126 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@testable import FaceLiveness + + +final class DetectedFaceTests: XCTestCase { + var detectedFace: DetectedFace! + var expectedNormalizeFace: DetectedFace! + let normalizeWidth = 414.0 + let normalizeHeight = 552.0 + + override func setUp() { + let boundingBox = CGRect( + x: 0.15805082494171963, + y: 0.3962942063808441, + width: 0.6549023386310235, + height: 0.49117204546928406 + ) + let leftEye = CGPoint(x: 0.6686329891870315, y: 0.48738187551498413) + let rightEye = CGPoint(x: 0.35714725227596134, y: 0.4664449691772461) + let nose = CGPoint(x: 0.5283648181467697, y: 0.5319401621818542) + let mouth = CGPoint(x: 0.5062596005080024, y: 0.689265251159668) + let rightEar = CGPoint(x: 0.1658528943614037, y: 0.5668278932571411) + let leftEar = CGPoint(x: 0.7898947484263203, y: 0.5973731875419617) + let confidence: Float = 0.94027895 + detectedFace = DetectedFace( + boundingBox: boundingBox, + leftEye: leftEye, + rightEye: rightEye, + nose: nose, + mouth: mouth, + rightEar: rightEar, + leftEar: leftEar, + confidence: confidence + ) + + let normalizedBoundingBox = CGRect( + x: 0.15805082494171963 * normalizeWidth, + y: 0.3962942063808441 * normalizeHeight, + width: 0.6549023386310235 * normalizeWidth, + height: 0.49117204546928406 * normalizeHeight + ) + let normalizedLeftEye = CGPoint( + x: 0.6686329891870315 * normalizeWidth, + y: 0.48738187551498413 * normalizeHeight + ) + let normalizedRightEye = CGPoint( + x: 0.35714725227596134 * normalizeWidth, + y: 0.4664449691772461 * normalizeHeight) + let normalizedNose = CGPoint( + x: 0.5283648181467697 * normalizeWidth, + y: 0.5319401621818542 * normalizeHeight + ) + let normalizedMouth = CGPoint( + x: 0.5062596005080024 * normalizeWidth, + y: 0.689265251159668 * normalizeHeight + ) + let normalizedRightEar = CGPoint( + x: 0.1658528943614037 * normalizeWidth, + y: 0.5668278932571411 * normalizeHeight + ) + let normalizedLeftEar = CGPoint( + x: 0.7898947484263203 * normalizeWidth, + y: 0.5973731875419617 * normalizeHeight + ) + + expectedNormalizeFace = DetectedFace( + boundingBox: normalizedBoundingBox, + leftEye: normalizedLeftEye, + rightEye: normalizedRightEye, + nose: normalizedNose, + mouth: normalizedMouth, + rightEar: normalizedRightEar, + leftEar: normalizedLeftEar, + confidence: confidence + ) + } + + /// Given: A `DetectedFace` + /// When: when the struct is initialized + /// Then: the calculated landmarks are available and calculated as expected + func testDetectedFaceLandmarks() { + XCTAssertEqual(detectedFace.eyeCenterX, 0.5128901207314964) + XCTAssertEqual(detectedFace.eyeCenterY, 0.4769134223461151) + XCTAssertEqual(detectedFace.faceDistance, 0.31218859419592454) + XCTAssertEqual(detectedFace.pupilDistance, 0.31218859419592454) + XCTAssertEqual(detectedFace.faceHeight, 0.21245532000610062) + } + + /// Given: A `DetectedFace` + /// When: when boundingBoxFromLandmarks is called + /// Then: the calculated bounding box is returned + func testDetectedFaceBoundingBoxFromLandmarks() { + let ovalRect = CGRect.zero + let expectedBoundingBox = CGRect( + x: 0.1658528943614037, + y: 0.072967669448238516, + width: 0.6240418540649166, + height: 0.8144985824018897 + ) + let boundingBox = detectedFace.boundingBoxFromLandmarks(ovalRect: ovalRect) + XCTAssertEqual(boundingBox.origin.x, expectedBoundingBox.origin.x) + XCTAssertEqual(boundingBox.origin.y, expectedBoundingBox.origin.y) + XCTAssertEqual(boundingBox.width, expectedBoundingBox.width) + XCTAssertEqual(boundingBox.height, expectedBoundingBox.height) + } + + /// Given: A `DetectedFace` + /// When: when normalize is called with a view dimension + /// Then: the normalized face calculates the correct landmark distances + func testDetectedFaceNormalize() { + let normalizedFace = detectedFace.normalize(width: normalizeWidth, height: normalizeHeight) + XCTAssertEqual(normalizedFace.eyeCenterX, expectedNormalizeFace.eyeCenterX) + XCTAssertEqual(normalizedFace.eyeCenterY, expectedNormalizeFace.eyeCenterY) + XCTAssertEqual(normalizedFace.faceDistance, expectedNormalizeFace.faceDistance) + XCTAssertEqual(normalizedFace.pupilDistance, expectedNormalizeFace.pupilDistance) + XCTAssertEqual(normalizedFace.faceHeight, expectedNormalizeFace.faceHeight) + } + +} diff --git a/Tests/FaceLivenessTests/LivenessTests.swift b/Tests/FaceLivenessTests/LivenessTests.swift new file mode 100644 index 00000000..da063930 --- /dev/null +++ b/Tests/FaceLivenessTests/LivenessTests.swift @@ -0,0 +1,177 @@ +import XCTest +import Combine +@testable import FaceLiveness +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +@MainActor +final class FaceLivenessDetectionViewModelTestCase: XCTestCase { + var videoChunker: VideoChunker! + var viewModel: FaceLivenessDetectionViewModel! + var faceDetector: MockFaceDetector! + var livenessService: MockLivenessService! + + override func setUp() { + faceDetector = MockFaceDetector() + livenessService = MockLivenessService() + let videoChunker = VideoChunker( + assetWriter: LivenessAVAssetWriter(), + assetWriterDelegate: VideoChunker.AssetWriterDelegate(), + assetWriterInput: LivenessAVAssetWriterInput() + ) + let captureSession = LivenessCaptureSession( + captureDevice: .init(avCaptureDevice: nil), + outputDelegate: OutputSampleBufferCapturer( + faceDetector: faceDetector, + videoChunker: videoChunker + ) + ) + + let viewModel = FaceLivenessDetectionViewModel( + faceDetector: faceDetector, + faceInOvalMatching: .init(instructor: .init()), + captureSession: captureSession, + videoChunker: videoChunker, + closeButtonAction: {}, + sessionID: UUID().uuidString + ) + + self.videoChunker = videoChunker + self.viewModel = viewModel + } + + override func tearDown() { + self.faceDetector = nil + self.livenessService = nil + self.videoChunker = nil + self.viewModel = nil + } + + /// Given: A `FaceLivenessDetectionViewModel` + /// When: The viewModel is first initialized + /// Then: The state is `.intitial` + func testInitialState() { + // This first call comes from the FaceLivenessDetectionViewModel's initializer + XCTAssertEqual(faceDetector.interactions, [ + "setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)" + ]) + XCTAssertEqual(livenessService.interactions, []) + + viewModel.livenessService = self.livenessService + XCTAssertEqual(viewModel.livenessState.state, .initial) + XCTAssertEqual(faceDetector.interactions, [ + "setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)" + ]) + XCTAssertEqual(livenessService.interactions, []) + } + + /// Given: A `FaceLivenessDetectionViewModel` + /// When: The viewModel is processes the happy path events + /// Then: The end state of this flow is `.faceMatched` + func testHappyPathToMatchedFace() async throws { + viewModel.livenessService = self.livenessService + + viewModel.livenessState.checkIsFacePrepared() + XCTAssertEqual(viewModel.livenessState.state, .pendingFacePreparedConfirmation(.pendingCheck)) + XCTAssertEqual(faceDetector.interactions, [ + "setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)" + ]) + XCTAssertEqual(livenessService.interactions, []) + + viewModel.initializeLivenessStream() + viewModel.process(newResult: .noFace) + XCTAssertEqual(videoChunker.state, .pending) + + viewModel.sendInitialFaceDetectedEvent( + initialFace: .zero, + videoStartTime: Date().timestampMilliseconds + ) + XCTAssertEqual(videoChunker.state, .writing) + + let initialSegment = Data([0, 1]) + var currentSegment = Data([25, 42]) + XCTAssertFalse(viewModel.hasSentFirstVideo) + let chunk = viewModel.chunk(initial: initialSegment, current: currentSegment) + XCTAssertEqual(chunk, initialSegment + currentSegment) + XCTAssertTrue(viewModel.hasSentFirstVideo) + + currentSegment = Data([42, 25]) + let subsequentChunk = viewModel.chunk(initial: initialSegment, current: currentSegment) + XCTAssertEqual(subsequentChunk, currentSegment) + + viewModel.livenessState.faceMatched() + XCTAssertEqual(viewModel.livenessState.state, .faceMatched) + XCTAssertEqual(faceDetector.interactions, [ + "setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)" + ]) + XCTAssertEqual(livenessService.interactions, [ + "initializeLivenessStream(withSessionID:userAgent:)" + ]) + } + + /// Given: A `FaceLivenessDetectionViewModel` + /// When: The viewModel is processes a single face result with a face distance less than the inital face distance + /// Then: The end state of this flow is `.recording(ovalDisplayed: false)` and initializeLivenessStream(withSessionID:userAgent:) is called + func testTransitionToRecordingState() async throws { + viewModel.livenessService = self.livenessService + + viewModel.livenessState.checkIsFacePrepared() + XCTAssertEqual(viewModel.livenessState.state, .pendingFacePreparedConfirmation(.pendingCheck)) + XCTAssertEqual(faceDetector.interactions, [ + "setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)" + ]) + XCTAssertEqual(livenessService.interactions, []) + + let boundingBox = CGRect(x: 0.26788579725878847, y: 0.40317180752754211, width: 0.45549795395626447, height: 0.34162446856498718) + let leftEye = CGPoint(x: 0.61124476128552629, y: 0.4918237030506134) + let rightEye = CGPoint(x: 0.38036393762719456, y: 0.48050540685653687) + let nose = CGPoint(x: 0.48489856674964926, y: 0.54713362455368042) + let mouth = CGPoint(x: 0.47411978167652435, y: 0.63170802593231201) + let leftEar = CGPoint(x: 0.7898947484263203, y: 0.5973731875419617) + let rightEar = CGPoint(x: 0.1658528943614037, y: 0.5668278932571411) + let detectedFace = DetectedFace(boundingBox: boundingBox, leftEye: leftEye, rightEye: rightEye, nose: nose, mouth: mouth, rightEar: rightEar, leftEar: leftEar, confidence: 0.971859633) + viewModel.process(newResult: .singleFace(detectedFace)) + try await Task.sleep(seconds: 1) + + XCTAssertEqual(viewModel.livenessState.state, .recording(ovalDisplayed: false)) + XCTAssertEqual(faceDetector.interactions, [ + "setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)" + ]) + XCTAssertEqual(livenessService.interactions, [ + "initializeLivenessStream(withSessionID:userAgent:)" + ]) + } + + /// Given: A `FaceLivenessDetectionViewModel` + /// When: The viewModel handles a no fit event over a client default time limit of 7 seconds + /// Then: The end state is `.encounteredUnrecoverableError(.timedOut)` + func testNoFitTimeoutCheck() async throws { + viewModel.livenessService = self.livenessService + self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2) + + XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut)) + try await Task.sleep(seconds: 6) + self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2) + XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut)) + try await Task.sleep(seconds: 1) + self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2) + try await Task.sleep(seconds: 1) + XCTAssertEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut)) + } + + /// Given: A `FaceLivenessDetectionViewModel` + /// When: The viewModel handles a no face detected event over a duration of 7 seconds + /// Then: The end state is `.encounteredUnrecoverableError(.timedOut)` + func testNoFaceDetectedTimeoutCheck() async throws { + viewModel.livenessService = self.livenessService + self.viewModel.handleNoFaceDetected() + + XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut)) + try await Task.sleep(seconds: 6) + self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2) + XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut)) + try await Task.sleep(seconds: 1) + self.viewModel.handleNoFaceDetected() + try await Task.sleep(seconds: 1) + XCTAssertEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut)) + } +} diff --git a/Tests/FaceLivenessTests/MockAWSCredentials.swift b/Tests/FaceLivenessTests/MockAWSCredentials.swift new file mode 100644 index 00000000..a9686d6c --- /dev/null +++ b/Tests/FaceLivenessTests/MockAWSCredentials.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import AWSPluginsCore + +struct MockAWSCredentials: AWSCredentials { + var accessKeyId: String + var secretAccessKey: String +} diff --git a/Tests/FaceLivenessTests/MockAWSTemporaryCredentials.swift b/Tests/FaceLivenessTests/MockAWSTemporaryCredentials.swift new file mode 100644 index 00000000..a41b5279 --- /dev/null +++ b/Tests/FaceLivenessTests/MockAWSTemporaryCredentials.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import AWSPluginsCore + +struct MockAWSTemporaryCredentials: AWSTemporaryCredentials { + var sessionToken: String + var expiration: Date + var accessKeyId: String + var secretAccessKey: String +} diff --git a/Tests/FaceLivenessTests/MockCredentialsProvider.swift b/Tests/FaceLivenessTests/MockCredentialsProvider.swift new file mode 100644 index 00000000..47e0423a --- /dev/null +++ b/Tests/FaceLivenessTests/MockCredentialsProvider.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import AWSPluginsCore + +struct MockCredentialsProvider: AWSCredentialsProvider { + let credentials: () -> AWSCredentials + + func fetchAWSCredentials() async throws -> AWSCredentials { + credentials() + } +} diff --git a/Tests/FaceLivenessTests/MockFaceDetectionResultHandler.swift b/Tests/FaceLivenessTests/MockFaceDetectionResultHandler.swift new file mode 100644 index 00000000..db7b1e9e --- /dev/null +++ b/Tests/FaceLivenessTests/MockFaceDetectionResultHandler.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import FaceLiveness + +final class MockFaceDetectionResultHandler { + var intereactions: [String] = [] +} + +extension MockFaceDetectionResultHandler: FaceDetectionResultHandler { + func process(newResult: FaceLiveness.FaceDetectionResult) { + intereactions.append(#function) + } +} diff --git a/Tests/FaceLivenessTests/MockFaceDetector.swift b/Tests/FaceLivenessTests/MockFaceDetector.swift new file mode 100644 index 00000000..cab80ca7 --- /dev/null +++ b/Tests/FaceLivenessTests/MockFaceDetector.swift @@ -0,0 +1,27 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import AVFoundation +@testable import FaceLiveness +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +final class MockFaceDetector { + var interactions: [String] = [] + var detectionResultHandler: FaceDetectionResultHandler = MockFaceDetectionResultHandler() +} + +extension MockFaceDetector: FaceDetector { + + func detectFaces(from buffer: CVPixelBuffer) { + interactions.append(#function) + } + + func setResultHandler(detectionResultHandler: FaceDetectionResultHandler) { + interactions.append("\(#function) (\(type(of: detectionResultHandler)))") + self.detectionResultHandler = detectionResultHandler + } +} diff --git a/Tests/FaceLivenessTests/MockLivenessService.swift b/Tests/FaceLivenessTests/MockLivenessService.swift new file mode 100644 index 00000000..2b4633d1 --- /dev/null +++ b/Tests/FaceLivenessTests/MockLivenessService.swift @@ -0,0 +1,70 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +@testable import FaceLiveness +@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin + +class MockLivenessService { + + var interactions: [String] = [] + + var onInitialClientEvent: (LivenessEvent, Date) -> Void = { _, _ in } + var onFaceDetectionEvent: (LivenessEvent, Date) -> Void = { _, _ in } + var onFinalClientEvent: (LivenessEvent, Date) -> Void = { _, _ in } + var onFreshnessEvent: (LivenessEvent, Date) -> Void = { _, _ in } + var onVideoEvent: (LivenessEvent, Date) -> Void = { _, _ in } + var onInitializeLivenessStream: (String, String) -> Void = { _, _ in } + var onServiceException: (FaceLivenessSessionError) -> Void = { _ in } + var onCloseSocket: (URLSessionWebSocketTask.CloseCode) -> Void = { _ in } +} + +extension MockLivenessService: LivenessService { + + func send(_ event: LivenessEvent, eventDate: () -> Date) { + interactions.append(#function) + + switch event { + case let initialClient as LivenessEvent: + onInitialClientEvent(initialClient, eventDate()) + case let faceDetection as LivenessEvent: + onFaceDetectionEvent(faceDetection, eventDate()) + case let finalClient as LivenessEvent: + onFinalClientEvent(finalClient, eventDate()) + case let freshness as LivenessEvent: + onFreshnessEvent(freshness, eventDate()) + case let video as LivenessEvent: + onVideoEvent(video, eventDate()) + default: break + } + } + + func initializeLivenessStream( + withSessionID sessionID: String, userAgent: String + ) throws { + interactions.append(#function) + onInitializeLivenessStream(sessionID, userAgent) + } + + func register( + onComplete: @escaping (ServerDisconnection) -> Void + ) { + interactions.append(#function) + } + + func register( + listener: @escaping (FaceLivenessSession.SessionConfiguration) -> Void, + on event: LivenessEventKind.Server + ) { + interactions.append(#function) + } + + func closeSocket(with code: URLSessionWebSocketTask.CloseCode) { + interactions.append(#function) + onCloseSocket(code) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Extension/FaceLivenessDetectorView+Mock.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/FaceLivenessDetectorView+Mock.swift new file mode 100644 index 00000000..cc8bcbe7 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/FaceLivenessDetectorView+Mock.swift @@ -0,0 +1,45 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import protocol AWSPluginsCore.AWSCredentialsProvider +import AVFoundation +@testable import FaceLiveness + +extension FaceLivenessDetectorView { + static func getMockFaceLivenessDetectorView ( + sessionID: String, + credentialsProvider: AWSCredentialsProvider? = nil, + region: String, + isPresented: Binding, + onCompletion: @escaping (Result) -> Void + ) -> FaceLivenessDetectorView { + + let avCaptureDevice = AVCaptureDevice.DiscoverySession( + deviceTypes: [.builtInWideAngleCamera], + mediaType: .video, + position: .front + ).devices.first + let captureDevice = LivenessCaptureDevice(avCaptureDevice: avCaptureDevice) + + let faceDetector = try! FaceDetectorShortRange.Model() + + let videoChunker = VideoChunker( + assetWriter: LivenessAVAssetWriter(), + assetWriterDelegate: VideoChunker.AssetWriterDelegate(), + assetWriterInput: LivenessAVAssetWriterInput() + ) + + let outputDelegate = OutputSampleBufferCapturer(faceDetector: faceDetector, videoChunker: videoChunker + ) + let inputUrl = Bundle.main.url(forResource: "mock", withExtension: "mov")! + let captureSession = MockLivenessCaptureSession(captureDevice: captureDevice, outputDelegate: outputDelegate, inputFile: inputUrl) + let detectorView = FaceLivenessDetectorView(sessionID: sessionID, region: region, isPresented: isPresented, onCompletion: onCompletion, captureSession: captureSession) + + return detectorView + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift new file mode 100644 index 00000000..8445e5ca --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift @@ -0,0 +1,134 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import AVFoundation +@testable import FaceLiveness + +final class MockLivenessCaptureSession: LivenessCaptureSession { + private var videoRenderView: VideoRenderView? + private var displayLink: CADisplayLink? + private var playerItemOutput: AVPlayerItemVideoOutput? + private let videoFileReadingQueue = DispatchQueue(label: "com.amazonaws.faceliveness.cameracapturequeue") + private var videoFileFrameDuration = CMTime.invalid + private let inputFile: URL + + init(captureDevice: LivenessCaptureDevice, + outputDelegate: OutputSampleBufferCapturer, + inputFile: URL + ) { + self.inputFile = inputFile + super.init(captureDevice: captureDevice, outputDelegate: outputDelegate) + } + + override func stopRunning() { + videoRenderView?.player?.pause() + displayLink?.invalidate() + } + + override func configureCamera(frame: CGRect) throws -> CALayer { + videoRenderView = VideoRenderView(frame: frame) + let asset = AVAsset(url: inputFile) + // Setup display link + let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:))) + displayLink.preferredFramesPerSecond = 0 + displayLink.isPaused = true + displayLink.add(to: RunLoop.current, forMode: .default) + captureSession = AVCaptureSession() + guard let track = asset.tracks(withMediaType: .video).first else { + throw LivenessCaptureSessionError.captureSessionInputUnavailable + } + + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + let settings = [ + String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ] + let output = AVPlayerItemVideoOutput(pixelBufferAttributes: settings) + playerItem.add(output) + player.actionAtItemEnd = .pause + player.play() + + self.displayLink = displayLink + self.playerItemOutput = output + self.videoRenderView?.player = player + + videoFileFrameDuration = track.minFrameDuration + displayLink.isPaused = false + guard let previewLayer = videoRenderView?.layer else { + throw LivenessCaptureSessionError.captureSessionOutputUnavailable + } + return previewLayer + } + + @objc + private func handleDisplayLink(_ displayLink: CADisplayLink) { + guard let output = playerItemOutput else { + return + } + + videoFileReadingQueue.async { + let nextTimeStamp = displayLink.timestamp + displayLink.duration + let itemTime = output.itemTime(forHostTime: nextTimeStamp) + guard output.hasNewPixelBuffer(forItemTime: itemTime) else { + return + } + guard let pixelBuffer = output.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil) else { + return + } + + var sampleBuffer: CMSampleBuffer? + var formatDescription: CMVideoFormatDescription? + CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &formatDescription) + let duration = self.videoFileFrameDuration + var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: itemTime, decodeTimeStamp: itemTime) + CMSampleBufferCreateForImageBuffer(allocator: nil, + imageBuffer: pixelBuffer, + dataReady: true, + makeDataReadyCallback: nil, + refcon: nil, + formatDescription: formatDescription!, + sampleTiming: &timingInfo, + sampleBufferOut: &sampleBuffer) + if let sampleBuffer = sampleBuffer { + self.outputSampleBufferCapturer?.videoChunker.consume(sampleBuffer) + guard let imageBuffer = sampleBuffer.imageBuffer + else { return } + self.outputSampleBufferCapturer?.faceDetector.detectFaces(from: imageBuffer) + } + } + } +} + +class VideoRenderView: UIView { + private var renderLayer: AVPlayerLayer! + + var player: AVPlayer? { + get { + return renderLayer.player + } + set { + renderLayer.player = newValue + } + } + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } + + override init(frame: CGRect) { + super.init(frame: frame) + renderLayer = layer as? AVPlayerLayer + renderLayer.videoGravity = .resizeAspect + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Info.plist b/Tests/IntegrationTestApp/IntegrationTestApp/Info.plist new file mode 100644 index 00000000..b72ec3ba --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Info.plist @@ -0,0 +1,15 @@ + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + myapp + + + + + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.entitlements b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.entitlements new file mode 100644 index 00000000..f6ea6035 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.camera + + com.apple.security.files.user-selected.read-only + + + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.swift b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.swift new file mode 100644 index 00000000..4e506b4e --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.swift @@ -0,0 +1,71 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness +import Amplify +import AWSCognitoAuthPlugin +import AWSAPIPlugin + +@main +struct IntegrationTestApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + func increaseBrightness() { + UIScreen.main.brightness = 1.0 + } + + var body: some Scene { + WindowGroup { + RootView() + } + } + + init() { + do { + let auth = AWSCognitoAuthPlugin() + let api = AWSAPIPlugin() + try Amplify.add(plugin: auth) + try Amplify.add(plugin: api) + try Amplify.configure() + } catch { + print("Error configuring Amplify", error) + } + } +} + +class AppDelegate: NSObject, UIApplicationDelegate { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + if connectingSceneSession.role == .windowApplication { + configuration.delegateClass = SceneDelegate.self + } + return configuration + } +} + +class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + if #available(iOS 15.0, *) { + self.window = (scene as? UIWindowScene)?.keyWindow + } else { + self.window = (scene as? UIWindowScene)?.windows + .first(where: \.isKeyWindow) + } + } +} + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Localizable.xcstrings b/Tests/IntegrationTestApp/IntegrationTestApp/Localizable.xcstrings new file mode 100644 index 00000000..8a470103 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Localizable.xcstrings @@ -0,0 +1,5 @@ +{ + "sourceLanguage" : "en", + "strings" : {}, + "version" : "1.0" +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Model/CreateSessionResponse.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Model/CreateSessionResponse.swift new file mode 100644 index 00000000..f2f44e37 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Model/CreateSessionResponse.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct CreateSessionResponse: Codable { + let sessionId: String +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Model/LivenessResult.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Model/LivenessResult.swift new file mode 100644 index 00000000..226bc30f --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Model/LivenessResult.swift @@ -0,0 +1,25 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct LivenessResult: Codable { + let auditImageBytes: String? + let confidenceScore: Double + let isLive: Bool +} + +extension LivenessResult: CustomDebugStringConvertible { + var debugDescription: String { + """ + LivenessResult + - confidenceScore: \(confidenceScore) + - isLive: \(isLive) + - auditImageBytes: \(auditImageBytes == nil ? "nil" : "") + """ + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+DynamicColors.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+DynamicColors.swift new file mode 100644 index 00000000..71afbf63 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+DynamicColors.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension Color { + static func dynamicColors(light: UIColor, dark: UIColor) -> Color { + Color( + UIColor( + dynamicProvider: { traitCollection in + switch traitCollection.userInterfaceStyle { + case .dark: return dark + default: return light + } + } + ) + ) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+Hex.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+Hex.swift new file mode 100644 index 00000000..0136a714 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+Hex.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import UIKit + +extension Color { + static func hex(_ hex: String) -> Color { + Color(UIColor.hex(hex)) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/UIColor+Hex.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/UIColor+Hex.swift new file mode 100644 index 00000000..678773d1 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/UIColor+Hex.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit + +extension UIColor { + static func hex(_ hex: String) -> UIColor { + assert(hex.hasPrefix("#")) + + let hex = String(hex.dropFirst()) + assert(hex.count == 6) + + let scanner = Scanner(string: hex) + var hexNumber: UInt64 = 0 + + precondition(scanner.scanHexInt64(&hexNumber)) + let r, g, b, a: CGFloat + r = CGFloat((hexNumber & 0xFF0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x00FF00) >> 8) / 255 + b = CGFloat((hexNumber & 0x0000FF)) / 255 + a = 1.0 + + return UIColor(red: r, green: g, blue: b, alpha: a) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/View+Background.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/View+Background.swift new file mode 100644 index 00000000..4ffb074c --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/View+Background.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension View { + @ViewBuilder func _background( + alignment: Alignment = .center, + @ViewBuilder _ content: () -> Content + ) -> some View { + if #available(iOS 15.0, *) { + background(alignment: alignment, content: content) + } else { + background(content(), alignment: alignment) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessView.swift new file mode 100644 index 00000000..7e57e8cf --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessView.swift @@ -0,0 +1,81 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness + +struct ExampleLivenessView: View { + @Binding var isPresented: Bool + @ObservedObject var viewModel: ExampleLivenessViewModel + + init(sessionID: String, isPresented: Binding) { + self.viewModel = .init(sessionID: sessionID) + self._isPresented = isPresented + } + + var body: some View { + switch viewModel.presentationState { + case .liveness: + FaceLivenessDetectorView.getMockFaceLivenessDetectorView( + sessionID: viewModel.sessionID, + region: "us-east-1", + isPresented: Binding( + get: { viewModel.presentationState == .liveness }, + set: { _ in } + ), + onCompletion: { result in + switch result { + case .success: + withAnimation { viewModel.presentationState = .result } + case .failure(.sessionNotFound), .failure(.cameraPermissionDenied), .failure(.accessDenied), .failure(.validation): + viewModel.presentationState = .liveness + isPresented = false + case .failure(.userCancelled): + viewModel.presentationState = .liveness + isPresented = false + case .failure(.sessionTimedOut): + viewModel.presentationState = .error(.sessionTimedOut) + case .failure(.socketClosed): + viewModel.presentationState = .error(.socketClosed) + case .failure(.countdownNoFace), .failure(.countdownFaceTooClose), .failure(.countdownMultipleFaces): + viewModel.presentationState = .error(.countdownFaceTooClose) + default: + viewModel.presentationState = .liveness + } + } + ) + .id(isPresented) + case .result: + LivenessResultView( + sessionID: viewModel.sessionID, + onTryAgain: { isPresented = false }, + content: { + LivenessResultContentView(fetchResults: viewModel.fetchLivenessResult) + } + ) + .animation(.default, value: viewModel.presentationState) + case .error(let detectionError): + LivenessResultView( + sessionID: viewModel.sessionID, + onTryAgain: { isPresented = false }, + content: { + switch detectionError { + case .socketClosed: + LivenessCheckErrorContentView.sessionTimeOut + case .sessionTimedOut: + LivenessCheckErrorContentView.faceMatchTimeOut + case .countdownNoFace, .countdownFaceTooClose, .countdownMultipleFaces: + LivenessCheckErrorContentView.failedDuringCountdown + default: + LivenessCheckErrorContentView.unexpected + } + } + ) + .animation(.default, value: viewModel.presentationState) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessViewModel.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessViewModel.swift new file mode 100644 index 00000000..a04571bc --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessViewModel.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness +import Amplify + +class ExampleLivenessViewModel: ObservableObject { + @Published var presentationState = PresentationState.liveness + let sessionID: String + + init(sessionID: String) { + self.sessionID = sessionID + } + + func fetchLivenessResult() async throws -> LivenessResultContentView.Result { + let request = RESTRequest( + apiName: "liveness", + path: "/liveness/\(sessionID)" + ) + + let data = try await Amplify.API.get(request: request) + let result = try JSONDecoder().decode(LivenessResult.self, from: data) + let score = LivenessResultContentView.Result(livenessResult: result) + return score + } + + enum PresentationState: Equatable { + case liveness, result, error(FaceLivenessDetectionError) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessCheckErrorContentView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessCheckErrorContentView.swift new file mode 100644 index 00000000..c3415318 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessCheckErrorContentView.swift @@ -0,0 +1,59 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessCheckErrorContentView: View { + let name: String + let description: String + + var body: some View { + VStack(alignment: .leading) { + HStack { + Image(systemName: "exclamationmark.circle.fill") + .foregroundColor(.hex("#950404")) + Text("Error: \(name)") + .fontWeight(.semibold) + } + .padding(.bottom, 4) + Text(description) + } + } +} + +extension LivenessCheckErrorContentView { + static let mock = LivenessCheckErrorContentView( + name: "Time out", + description: "Face didn't fit inside oval in time limit. Try again and completely fill the oval with face in it." + ) + + static let unexpected = LivenessCheckErrorContentView( + name: "An unexpected error ocurred", + description: "Please try again." + ) + + static let faceMatchTimeOut = LivenessCheckErrorContentView( + name: "Time out", + description: "Face did not fill oval in time limit. Try again and completely fill the oval with face in it." + ) + + static let sessionTimeOut = LivenessCheckErrorContentView( + name: "Connection interrupted", + description: "Your connection was unexpectedly closed." + ) + + static let failedDuringCountdown = LivenessCheckErrorContentView( + name: "Check failed during countdown", + description: "Avoid moving closer during countdown and ensure only one face is in front of camera." + ) +} + +struct LivenessCheckErrorContentView_Previews: PreviewProvider { + static var previews: some View { + LivenessCheckErrorContentView.mock + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift new file mode 100644 index 00000000..3f57982f --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift @@ -0,0 +1,80 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension LivenessResultContentView { + struct Result { + let text: String + let value: String + let valueTextColor: Color + let valueBackgroundColor: Color + let auditImage: Data? + let isLive: Bool + + init(livenessResult: LivenessResult) { + guard livenessResult.confidenceScore > 0 else { + text = "" + value = "" + valueTextColor = .clear + valueBackgroundColor = .clear + auditImage = nil + isLive = false + return + } + isLive = livenessResult.isLive + let truncated = String(format: "%.4f", livenessResult.confidenceScore) + value = truncated + if livenessResult.isLive { + valueTextColor = .hex("#365E3D") + valueBackgroundColor = .hex("#D6F5DB") + text = "Check successful" + } else { + valueTextColor = .hex("#660000") + valueBackgroundColor = .hex("#F5BCBC") + text = "Check unsuccessful" + } + auditImage = livenessResult.auditImageBytes.flatMap{ + Data(base64Encoded: $0) + } + } + } + + struct Score { + let resultText: String + let value: String + let valueTextColor: Color + let valueBackgroundColor: Color + + init( + value: Double, + colorRule: (Double) -> (Color, Color, String) = colorRule + ) { + let truncated = String(format: "%.4f", value) + let (textColor, backgroundColor, resultText) = colorRule(value) + self.resultText = resultText + self.value = truncated + self.valueTextColor = textColor + self.valueBackgroundColor = backgroundColor + } + } +} + +fileprivate func colorRule(v: Double) -> (Color, Color, String) { + let textColor, backgroundColor: Color + let resultText: String + if v >= 70 { + textColor = .hex("#365E3D") + backgroundColor = .hex("#D6F5DB") + resultText = "Check successful" + } else { + textColor = .hex("#660000") + backgroundColor = .hex("#F5BCBC") + resultText = "Check unsuccessful" + } + return (textColor, backgroundColor, resultText) +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift new file mode 100644 index 00000000..eb2cbbd0 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift @@ -0,0 +1,121 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessResultContentView: View { + @State var result: Result = .init(livenessResult: .init(auditImageBytes: nil, confidenceScore: -1, isLive: false)) + let fetchResults: () async throws -> Result + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text("Result:") + Text(result.text) + .fontWeight(.semibold) + .foregroundColor(result.valueTextColor) + .padding(6) + .background(result.valueBackgroundColor) + .cornerRadius(8) + + } + .padding(.bottom, 12) + + HStack { + Text("Liveness confidence score:") + Text(result.value) + .foregroundColor(result.valueTextColor) + .padding(6) + .background(result.valueBackgroundColor) + .cornerRadius(8) + } + + if let image = result.auditImage { + Image(uiImage: .init(data: image) ?? UIImage()) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 300) + .background(Color.secondary.opacity(0.1)) + } else { + Image(systemName: "person.fill") + .font(.system(size: 128)) + .frame(maxWidth: .infinity, idealHeight: 268) + .background(Color.secondary.opacity(0.1)) + } + + if !result.isLive { + steps() + .padding() + .background( + Rectangle() + .foregroundColor( + .dynamicColors( + light: .hex("#ECECEC"), + dark: .darkGray + ) + ) + .cornerRadius(6)) + } + } + .padding(.bottom, 16) + .onAppear { + Task { + do { + self.result = try await fetchResults() + } catch { + print("Error fetching result", error) + } + } + } + } + + private func steps() -> some View { + func step(number: Int, text: String) -> some View { + HStack(alignment: .top) { + Text("\(number).") + Text(text) + } + } + + return VStack( + alignment: .leading, + spacing: 8 + ) { + Text("Tips to pass the video check:") + .fontWeight(.semibold) + step(number: 1, text: "Maximize your screen's brightness.") + .accessibilityElement(children: .combine) + + step(number: 2, text: "Avoid very bright lighting conditions, such as direct sunlight.") + .accessibilityElement(children: .combine) + + step(number: 3, text: "Remove sunglasses, mask, hat, or anything blocking your face.") + .accessibilityElement(children: .combine) + } + } +} + + +extension LivenessResultContentView { + static let mock = LivenessResultContentView( + fetchResults: { + .init( + livenessResult: .init( + auditImageBytes: nil, + confidenceScore: 99.8329, + isLive: true + ) + ) + } + ) +} + +struct LivenessResultContentView_Previews: PreviewProvider { + static var previews: some View { + LivenessResultContentView.mock + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultView.swift new file mode 100644 index 00000000..7f75844d --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultView.swift @@ -0,0 +1,149 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessResultView: View { + let title: String + let sessionID: String + let content: Content + let onTryAgain: () -> Void + @State var displayingCopiedNotification = false + + init( + title: String = "Liveness Check", + sessionID: String, + onTryAgain: @escaping () -> Void, + @ViewBuilder content: () -> Content + ) { + self.title = title + self.sessionID = sessionID + self.content = content() + self.onTryAgain = onTryAgain + } + + var body: some View { + VStack { + ScrollView { + VStack(alignment: .leading) { + Text(title) + .font(.system(size: 34, weight: .semibold)) + .padding(.bottom, 8) + + sessionIDBox + .padding(.bottom, 16) + + content + } + .padding() + } + + if displayingCopiedNotification { + Text("Copied Session ID") + .foregroundColor(.dynamicColors(light: .white, dark: .black)) + .padding(8) + .background(Color.dynamicColors(light: .darkGray, dark: .lightGray)) + .cornerRadius(6) + .onAppear { + Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + withAnimation { + displayingCopiedNotification = false + } + } + } + } + tryAgainButton + } + } + + private func copySessionID() { + withAnimation { + displayingCopiedNotification = true + } + UIPasteboard.general.string = sessionID + } + + private var sessionIDBox: some View { + HStack { + VStack(alignment: .leading) { + Text("Session ID:") + .fontWeight(.semibold) + Text(sessionID) + } + Spacer() + Button( + action: copySessionID, + label: { + Image(systemName: "square.on.square") + .foregroundColor(.primary) + .frame(width: 20, height: 20) + } + ) + .frame(width: 44, height: 44) + } + .padding() + .background( + Rectangle() + .foregroundColor( + .dynamicColors( + light: .hex("#ECECEC"), + dark: .darkGray + ) + ) + .cornerRadius(6) + ) + } + + private var tryAgainButton: some View { + Button( + action: onTryAgain, + label: { + Text("Try Again") + .foregroundColor( + .dynamicColors(light: .white, dark: .black) + ) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { + Color.dynamicColors(light: .hex("#047D95"), dark: .hex("#7dd6e8")) + } + .cornerRadius(14) + .padding(.leading) + .padding(.trailing) + .padding(.bottom, 16) + } +} + +extension LivenessResultView where Content == LivenessResultContentView { + static var sessionID: String { + String(UUID().uuidString.flatMap { $0.lowercased() }) + } + + static var mock: Self { + .init( + sessionID: sessionID, + onTryAgain: {}, + content: { LivenessResultContentView.mock } + ) + } +} + +struct LivenessCheckView_Previews: PreviewProvider { + static let sessionID = String(UUID().uuidString.flatMap { $0.lowercased() }) + static var previews: some View { + LivenessResultView( + sessionID: sessionID, + onTryAgain: {}, + content: { + LivenessCheckErrorContentView.mock + } + ) + } +} + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/RootView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/RootView.swift new file mode 100644 index 00000000..7600f1b4 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/RootView.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct RootView: View { + @EnvironmentObject var sceneDelegate: SceneDelegate + @State var sessionID = "" + @State var isPresentingContainerView = false + + var body: some View { + if isPresentingContainerView { + ExampleLivenessView( + sessionID: sessionID, + isPresented: $isPresentingContainerView + ) + } else { + StartSessionView( + sessionID: $sessionID, + isPresentingContainerView: $isPresentingContainerView + ) + .background(Color.dynamicColors(light: .white, dark: .secondarySystemBackground)) + .edgesIgnoringSafeArea(.all) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView+PresentationState.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView+PresentationState.swift new file mode 100644 index 00000000..035a2f77 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView+PresentationState.swift @@ -0,0 +1,55 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension StartSessionView { + struct PresentationState: Equatable { + let buttonText: String + let buttonBackgroundColor: Color + let buttonAction: () -> Void + let buttonEnabled: Bool + + static let loading = PresentationState( + buttonText: "...", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: {}, + buttonEnabled: false + ) + + static func signedIn(action: @escaping () -> Void) -> PresentationState { + PresentationState( + buttonText: "Sign Out", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: action, + buttonEnabled: true + ) + } + + static func signedOut(action: @escaping () -> Void) -> PresentationState { + PresentationState( + buttonText: "Sign In", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: action, + buttonEnabled: true + ) + } + + static func == (lhs: StartSessionView.PresentationState, rhs: StartSessionView.PresentationState) -> Bool { + lhs.buttonText == rhs.buttonText + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView.swift new file mode 100644 index 00000000..173cc9c5 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView.swift @@ -0,0 +1,72 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import Amplify + +struct StartSessionView: View { + @EnvironmentObject var sceneDelegate: SceneDelegate + @ObservedObject var viewModel = StartSessionViewModel() + @Binding var sessionID: String + @Binding var isPresentingContainerView: Bool + + var body: some View { + VStack { + Spacer() + button( + text: viewModel.presentationState.buttonText, + backgroundColor: viewModel.presentationState.buttonBackgroundColor, + action: viewModel.presentationState.buttonAction, + enabled: viewModel.presentationState.buttonEnabled + ) + + button( + text: "Create Liveness Session", + backgroundColor: .dynamicColors( + light: .hex("#047D95"), + dark: .hex("#7dd6e8") + ), + action: { + viewModel.createSession { + sessionID = $0 + isPresentingContainerView = true + } + }, + enabled: true + ) + + Spacer() + } + .onAppear { viewModel.setup() } + } + + func button( + text: String, + backgroundColor: Color, + action: @escaping () -> Void, + enabled: Bool + ) -> some View { + Button( + action: action, + label: { + Text(text) + .foregroundColor(.dynamicColors(light: .white, dark: .black)) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { + backgroundColor.opacity(enabled ? 1.0 : 0.6) + } + .cornerRadius(14) + .padding(.leading) + .padding(.trailing) + .padding(.bottom, 16) + .disabled(!enabled) + } +} + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionViewModel.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionViewModel.swift new file mode 100644 index 00000000..a8250fd1 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionViewModel.swift @@ -0,0 +1,69 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import Amplify + +class StartSessionViewModel: ObservableObject { + @Published var presentationState: StartSessionView.PresentationState = .loading + var window: UIWindow? + + var isSignedIn: Bool { + presentationState == .signedIn {} + } + + func setup() { + Task { @MainActor in + presentationState = .loading + let session = try await Amplify.Auth.fetchAuthSession() + presentationState = session.isSignedIn + ? .signedIn(action: signOut) + : .signedOut(action: signIn) + } + } + + func createSession(_ completion: @escaping (String) -> Void) { + Task { @MainActor in + presentationState = .loading + let request = RESTRequest( + apiName: "liveness", + path: "/liveness/create" + ) + + do { + let data = try await Amplify.API.post(request: request) + let response = try JSONDecoder().decode( + CreateSessionResponse.self, + from: data + ) + completion(response.sessionId) + } catch { + print("Error creating session", error) + } + } + } + + func signIn() { + Task { @MainActor in + presentationState = .loading + let signInResult = try await Amplify.Auth.signInWithWebUI( + presentationAnchor: window + ) + if signInResult.isSignedIn { + presentationState = .signedIn(action: signOut) + } + } + } + + func signOut() { + Task { @MainActor in + presentationState = .loading + _ = await Amplify.Auth.signOut() + presentationState = .signedOut(action: signIn) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestAppUITests/LivenessIntegrationUITests.swift b/Tests/IntegrationTestApp/IntegrationTestAppUITests/LivenessIntegrationUITests.swift new file mode 100644 index 00000000..95db95bc --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestAppUITests/LivenessIntegrationUITests.swift @@ -0,0 +1,52 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +class CreateLivenessSessionUITests: XCTestCase { + + var app: XCUIApplication? + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app?.launch() + } + + func testBeginCheckUI() throws { + XCTAssertEqual(app!.label, UIConstants.appName) + XCTAssert(app!.buttons[UIConstants.primaryButton].exists) + XCTAssert(app!.buttons[UIConstants.primaryButton].isEnabled) + app!.buttons[UIConstants.primaryButton].tap() + Thread.sleep(forTimeInterval: 2) + XCTAssert(app!.buttons[UIConstants.BeginCheck.primaryButton].exists) + XCTAssertFalse(app!.buttons[UIConstants.primaryButton].exists) + XCTAssert(app!.staticTexts[UIConstants.BeginCheck.warningTitle].exists) + XCTAssert(app!.staticTexts[UIConstants.BeginCheck.warningDescription].exists) + XCTAssert(app!.staticTexts[UIConstants.BeginCheck.instruction].exists) + } + + func testStartLivenessIntegration() throws { + XCTAssertEqual(app!.label, UIConstants.appName) + XCTAssert(app!.buttons[UIConstants.primaryButton].exists) + XCTAssert(app!.buttons[UIConstants.primaryButton].isEnabled) + app?.buttons[UIConstants.primaryButton].tap() + Thread.sleep(forTimeInterval: 2) + XCTAssert(app!.buttons[UIConstants.BeginCheck.primaryButton].exists) + app!.buttons[UIConstants.BeginCheck.primaryButton].tap() + Thread.sleep(forTimeInterval: 2) + XCTAssert(app!.buttons[UIConstants.LivenessCheck.closeButton].exists) + XCTAssert(app!.staticTexts[UIConstants.LivenessCheck.moveInstruction].exists) + Thread.sleep(forTimeInterval: 4) + XCTAssert(app!.staticTexts[UIConstants.LivenessCheck.holdInstruction].exists) + Thread.sleep(forTimeInterval: 8) + XCTAssert(app!.buttons[UIConstants.LivenessResult.primaryButton].exists) + XCTAssert(app!.staticTexts[UIConstants.LivenessResult.result].exists) + XCTAssert(app!.staticTexts[UIConstants.LivenessResult.confidence].exists) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestAppUITests/UIConstants.swift b/Tests/IntegrationTestApp/IntegrationTestAppUITests/UIConstants.swift new file mode 100644 index 00000000..04a5db64 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestAppUITests/UIConstants.swift @@ -0,0 +1,31 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +struct UIConstants { + static let appName = "IntegrationTestApp" + static let primaryButton = "Create Liveness Session" + + struct BeginCheck { + static let primaryButton = "Start video check" + static let warningTitle = "Photosensitivity Warning" + static let warningDescription = "This check flashes different colors. Use caution if you are photosensitive." + static let instruction = "Center your face" + } + + struct LivenessCheck { + static let moveInstruction = "Move closer" + static let holdInstruction = "Hold still" + static let closeButton = "Close" + } + + struct LivenessResult { + static let result = "Result:" + static let confidence = "Liveness confidence score:" + static let primaryButton = "Try Again" + } +} + diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..e0877009 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,15 @@ +ignore: + - "**/Views" + - "**/Tests" + - "**/Resources" + - "**/fastlane" + +codecov: + branch: main + +coverage: + status: + patch: off + project: + default: + threshold: 1% \ No newline at end of file diff --git a/fastlane/.gitignore b/fastlane/.gitignore new file mode 100644 index 00000000..007b73ee --- /dev/null +++ b/fastlane/.gitignore @@ -0,0 +1 @@ +test_output diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 00000000..6715b218 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,95 @@ +opt_out_usage +default_platform(:ios) + +platform :ios do + before_all do + # Perform a fetch before inferring the next version + # to reduce race conditions with simultaneous pipelines attempting to create the same tag + sh('git', 'fetch', '--tags', '-f') + sh('git', 'fetch') + end + + desc "Create a pre-release version by pushing a tag to GitHub" + lane :unstable do + next_version = calculate_next_canary_version + + UI.message("Releasing unstable version: #{next_version}") + + # Increment all specs and plists + increment_versions(version: next_version) + + # Create tag and push to origin + add_tag(version: next_version) + + end + + desc "Create a release version by building and committing a changelog, pushing a tag to GitHub" + lane :release do + next_version, commits = calculate_next_release_version + + UI.message("Releasing version: #{next_version}") + + # Increment all specs and plists + increment_versions(version: next_version) + + changelog = build_changelog(version: next_version, commits: commits) + + # Commit and push + release_commit(version: next_version) + + # Create tag and push to origin + add_tag(version: next_version) + + post_release(version: next_version, changelog: changelog) + end + + desc "Increment versions" + private_lane :increment_versions do |options| + version = options[:version].to_s + set_key_value(file: "Sources/FaceLiveness/Utilities/UserAgent.swift", key: "libVersion", value: version) + end + + desc "Commit and push" + private_lane :release_commit do |options| + next_version = options[:version] + + sh('git', 'config', '--global', 'user.email', ENV['GITHUB_EMAIL']) + sh('git', 'config', '--global', 'user.name', ENV['GITHUB_USER']) + + commit_message = "chore: release #{next_version} [skip ci]" + sh('git', 'commit', '-am', commit_message) + + # push to origin + sh('git', 'push', 'origin', 'release') + end + + desc "Tag in git and push to GitHub" + private_lane :add_tag do |options| + next_version = options[:version] + next_tag = "#{next_version}" + + add_git_tag(tag: next_tag) + push_git_tags(tag: next_tag) + end + + desc "Post-release" + private_lane :post_release do |options| + version = options[:version].to_s + changelog = options[:changelog] + tag = "#{version}" + + sh('bundle', 'exec', 'swift', 'package', 'update') + + write_changelog(changelog: changelog, path: 'CHANGELOG.md') + + commit_message = "chore: finalize release #{version} [skip ci]" + sh('git', 'commit', '-am', commit_message) + + add_git_tag(tag: tag, force: true) + push_git_tags(tag: tag, force: true) + + # push to origin + sh('git', 'push', 'origin', 'release') + sh('git', 'push', 'origin', 'release:main') + end +end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 00000000..9750a5bd --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-release_actions', git: 'https://github.com/aws-amplify/amplify-ci-support', branch: 'main', glob: 'src/fastlane/release_actions/*.gemspec' \ No newline at end of file diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 00000000..1571e7ed --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios unstable + +```sh +[bundle exec] fastlane ios unstable +``` + +Create a pre-release version by pushing a tag to GitHub + +### ios release + +```sh +[bundle exec] fastlane ios release +``` + +Create a release version by building and committing a changelog, pushing a tag to GitHub + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).