diff --git a/.gitignore b/.gitignore
index c5a07310..f9d33fdd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.DS_Store
/.build
+/.index-build
/Packages
/*.xcodeproj
xcuserdata/
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..86f49135
--- /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/AmplifyUILiveness.podspec b/AmplifyUILiveness.podspec
new file mode 100644
index 00000000..966ba593
--- /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/guamacard/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
index c2c1dcc3..225fc0a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 1.4.2 (2025-07-17)
+
+## 1.4.1 (2025-07-01)
+
+## 1.4.0 (2025-06-30)
+
+### Features
+
+- add no-light/facemovementonly challenge and back camera support (#131)
+
## 1.3.5 (2025-03-18)
## 1.3.4 (2025-01-13)
diff --git a/HostApp/HostApp.xcodeproj/project.pbxproj b/HostApp/HostApp.xcodeproj/project.pbxproj
deleted file mode 100644
index 7d1314c5..00000000
--- a/HostApp/HostApp.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,724 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 55;
- objects = {
-
-/* Begin PBXBuildFile section */
- 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 */
- 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 */,
- );
- 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 = (
- 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 = "";
- };
-/* 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;
- 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 */,
- 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 = W3DRXD72QU;
- 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 = W3DRXD72QU;
- 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 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
deleted file mode 100644
index 919434a6..00000000
--- a/HostApp/HostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d98100..00000000
--- a/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
deleted file mode 100644
index 9a475d8f..00000000
--- a/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "pins" : [
- {
- "identity" : "amplify-swift",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/aws-amplify/amplify-swift",
- "state" : {
- "revision" : "5b603ff7cfe1b03d753ae7ff9664316e6447f0ae",
- "version" : "2.46.1"
- }
- },
- {
- "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" : "dd17a98750b6182edacd6e8f0c30aa289c472b22",
- "version" : "0.40.0"
- }
- },
- {
- "identity" : "aws-sdk-swift",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/awslabs/aws-sdk-swift.git",
- "state" : {
- "revision" : "9ad12684f6cb9c9b60e840c051a2bba604024650",
- "version" : "1.0.69"
- }
- },
- {
- "identity" : "smithy-swift",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/smithy-lang/smithy-swift",
- "state" : {
- "revision" : "402f091374dcf72c1e7ed43af10e3ee7e634fad8",
- "version" : "0.106.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" : "3d8596ed08bd13520157f0355e35caed215ffbfa",
- "version" : "1.6.3"
- }
- }
- ],
- "version" : 2
-}
diff --git a/HostApp/HostApp.xcodeproj/xcshareddata/xcschemes/HostApp.xcscheme b/HostApp/HostApp.xcodeproj/xcshareddata/xcschemes/HostApp.xcscheme
deleted file mode 100644
index ee175798..00000000
--- a/HostApp/HostApp.xcodeproj/xcshareddata/xcschemes/HostApp.xcscheme
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/HostApp/HostApp/Assets.xcassets/AccentColor.colorset/Contents.json b/HostApp/HostApp/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index eb878970..00000000
--- a/HostApp/HostApp/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "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
deleted file mode 100644
index 13613e3e..00000000
--- a/HostApp/HostApp/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "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
deleted file mode 100644
index 73c00596..00000000
--- a/HostApp/HostApp/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/HostApp/HostApp/HostApp.entitlements b/HostApp/HostApp/HostApp.entitlements
deleted file mode 100644
index 16ebdce6..00000000
--- a/HostApp/HostApp/HostApp.entitlements
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- keychain-access-groups
-
- $(AppIdentifierPrefix)com.amazonaws.mobile.amplify.liveness.testing.hostapp
-
-
-
diff --git a/HostApp/HostApp/HostAppApp.swift b/HostApp/HostApp/HostAppApp.swift
deleted file mode 100644
index 61863c5c..00000000
--- a/HostApp/HostApp/HostAppApp.swift
+++ /dev/null
@@ -1,72 +0,0 @@
-//
-// 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
deleted file mode 100644
index b72ec3ba..00000000
--- a/HostApp/HostApp/Info.plist
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
- CFBundleURLTypes
-
-
- CFBundleURLSchemes
-
- myapp
-
-
-
-
-
diff --git a/HostApp/HostApp/Model/CreateSessionResponse.swift b/HostApp/HostApp/Model/CreateSessionResponse.swift
deleted file mode 100644
index f2f44e37..00000000
--- a/HostApp/HostApp/Model/CreateSessionResponse.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-//
-// 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
deleted file mode 100644
index 226bc30f..00000000
--- a/HostApp/HostApp/Model/LivenessResult.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-//
-// 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
deleted file mode 100644
index 73c00596..00000000
--- a/HostApp/HostApp/Preview Content/Preview Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/HostApp/HostApp/Utilities/Color+DynamicColors.swift b/HostApp/HostApp/Utilities/Color+DynamicColors.swift
deleted file mode 100644
index 71afbf63..00000000
--- a/HostApp/HostApp/Utilities/Color+DynamicColors.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// 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
deleted file mode 100644
index 0136a714..00000000
--- a/HostApp/HostApp/Utilities/Color+Hex.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// 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
deleted file mode 100644
index 678773d1..00000000
--- a/HostApp/HostApp/Utilities/UIColor+Hex.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-//
-// 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
deleted file mode 100644
index 4ffb074c..00000000
--- a/HostApp/HostApp/Utilities/View+Background.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// 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/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift
deleted file mode 100644
index 5f6868b6..00000000
--- a/HostApp/HostApp/Views/ExampleLivenessView.swift
+++ /dev/null
@@ -1,95 +0,0 @@
-//
-// 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
deleted file mode 100644
index a04571bc..00000000
--- a/HostApp/HostApp/Views/ExampleLivenessViewModel.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-//
-// 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/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift b/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift
deleted file mode 100644
index 866c2c1c..00000000
--- a/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift
+++ /dev/null
@@ -1,69 +0,0 @@
-//
-// 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
deleted file mode 100644
index 3f57982f..00000000
--- a/HostApp/HostApp/Views/LivenessResultContentView+Result.swift
+++ /dev/null
@@ -1,80 +0,0 @@
-//
-// 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
deleted file mode 100644
index de2ecff7..00000000
--- a/HostApp/HostApp/Views/LivenessResultContentView.swift
+++ /dev/null
@@ -1,113 +0,0 @@
-//
-// 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
deleted file mode 100644
index 23ba99fe..00000000
--- a/HostApp/HostApp/Views/LivenessResultView.swift
+++ /dev/null
@@ -1,149 +0,0 @@
-//
-// 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
deleted file mode 100644
index 7600f1b4..00000000
--- a/HostApp/HostApp/Views/RootView.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// 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
deleted file mode 100644
index 035a2f77..00000000
--- a/HostApp/HostApp/Views/StartSessionView+PresentationState.swift
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// 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
deleted file mode 100644
index 42f64401..00000000
--- a/HostApp/HostApp/Views/StartSessionView.swift
+++ /dev/null
@@ -1,96 +0,0 @@
-//
-// 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: viewModel.isSignedIn
- )
- .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
deleted file mode 100644
index 8b8484db..00000000
--- a/HostApp/HostApp/Views/StartSessionViewModel.swift
+++ /dev/null
@@ -1,82 +0,0 @@
-//
-// 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
- do {
- let session = try await Amplify.Auth.fetchAuthSession()
- presentationState = session.isSignedIn
- ? .signedIn(action: signOut)
- : .signedOut(action: signIn)
- } catch {
- presentationState = .signedOut(action: signIn)
- print("Error fetching auth session", error)
- }
-
- }
- }
-
- func createSession(_ completion: @escaping (String?, Error?) -> Void) {
- Task { @MainActor in
- let currentPresentationState = presentationState
- 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
- )
- presentationState = currentPresentationState
- completion(response.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
deleted file mode 100644
index 812cc948..00000000
--- a/HostApp/HostAppUITests/HostAppUITests.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// 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
deleted file mode 100644
index 8caed7c5..00000000
--- a/HostApp/HostAppUITests/HostAppUITestsLaunchTests.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// 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
deleted file mode 100644
index 1234c14b..00000000
--- a/HostApp/README.md
+++ /dev/null
@@ -1,117 +0,0 @@
-
-
----
-
-# 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/Package.resolved b/Package.resolved
index e4e6e13c..a81fe4d4 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/aws-amplify/amplify-swift",
"state" : {
- "revision" : "5b603ff7cfe1b03d753ae7ff9664316e6447f0ae",
- "version" : "2.46.1"
+ "revision" : "7b1d5ee05a23ea24c7458dffed1c563c8501042d",
+ "version" : "2.45.4"
}
},
{
@@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/awslabs/aws-crt-swift",
"state" : {
- "revision" : "dd17a98750b6182edacd6e8f0c30aa289c472b22",
- "version" : "0.40.0"
+ "revision" : "3f844bef042cc0a4c3381f7090414ce3f9a7e935",
+ "version" : "0.37.0"
}
},
{
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/awslabs/aws-sdk-swift",
"state" : {
- "revision" : "9ad12684f6cb9c9b60e840c051a2bba604024650",
- "version" : "1.0.69"
+ "revision" : "c6c1064da9bfccb119a7a8ab9ba636fb3bbfa6f5",
+ "version" : "1.0.47"
}
},
{
@@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/smithy-lang/smithy-swift",
"state" : {
- "revision" : "402f091374dcf72c1e7ed43af10e3ee7e634fad8",
- "version" : "0.106.0"
+ "revision" : "3cd9f181b3ba8ff71da43bf53c09f8de6790a4ad",
+ "version" : "0.96.0"
}
},
{
diff --git a/Package.swift b/Package.swift
index 4e2d2e80..5eb3fae2 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,7 +13,7 @@ let package = Package(
targets: ["FaceLiveness"]),
],
dependencies: [
- .package(url: "https://github.com/aws-amplify/amplify-swift", exact: "2.46.1")
+ .package(url: "https://github.com/aws-amplify/amplify-swift", exact: "2.49.0")
],
targets: [
.target(
diff --git a/Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift b/Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift
index d6879848..1d62b263 100644
--- a/Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift
+++ b/Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift
@@ -6,6 +6,7 @@
//
import Foundation
+@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
struct DetectedFace {
var boundingBox: CGRect
@@ -19,7 +20,8 @@ struct DetectedFace {
let confidence: Float
- func boundingBoxFromLandmarks(ovalRect: CGRect) -> CGRect {
+ func boundingBoxFromLandmarks(ovalRect: CGRect,
+ ovalMatchChallenge: FaceLivenessSession.OvalMatchChallenge) -> CGRect {
let alpha = 2.0
let gamma = 1.8
let ow = (alpha * pupilDistance + gamma * faceHeight) / 2
@@ -34,7 +36,7 @@ struct DetectedFace {
}
let faceWidth = ow
- let faceHeight = 1.618 * faceWidth
+ let faceHeight = ovalMatchChallenge.oval.heightWidthRatio * faceWidth
let faceBoxBottom = boundingBox.maxY
let faceBoxTop = faceBoxBottom - faceHeight
let faceBoxLeft = min(cx - ow / 2, rightEar.x)
diff --git a/Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift b/Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift
index d9430720..2e8a6900 100644
--- a/Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift
+++ b/Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift
@@ -12,6 +12,7 @@ import Accelerate
import CoreGraphics
import CoreImage
import VideoToolbox
+@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
enum FaceDetectorShortRange {}
@@ -33,11 +34,16 @@ extension FaceDetectorShortRange {
)
}
+ weak var faceDetectionSessionConfiguration: FaceDetectionSessionConfigurationWrapper?
weak var detectionResultHandler: FaceDetectionResultHandler?
func setResultHandler(detectionResultHandler: FaceDetectionResultHandler) {
self.detectionResultHandler = detectionResultHandler
}
+
+ func setFaceDetectionSessionConfigurationWrapper(configuration: FaceDetectionSessionConfigurationWrapper) {
+ self.faceDetectionSessionConfiguration = configuration
+ }
func detectFaces(from buffer: CVPixelBuffer) {
let faces = prediction(for: buffer)
@@ -105,10 +111,22 @@ extension FaceDetectorShortRange {
count: confidenceScoresCapacity
)
)
+
+ let blazeFaceDetectionThreshold: Float
+ if let sessionConfiguration = faceDetectionSessionConfiguration?.sessionConfiguration {
+ switch sessionConfiguration {
+ case .faceMovement(let ovalMatchChallenge):
+ blazeFaceDetectionThreshold = Float(ovalMatchChallenge.faceDetectionThreshold)
+ case .faceMovementAndLight(_, let ovalMatchChallenge):
+ blazeFaceDetectionThreshold = Float(ovalMatchChallenge.faceDetectionThreshold)
+ }
+ } else {
+ blazeFaceDetectionThreshold = confidenceScoreThreshold
+ }
var passingConfidenceScoresIndices = confidenceScores
.enumerated()
- .filter { $0.element >= confidenceScoreThreshold }
+ .filter { $0.element >= blazeFaceDetectionThreshold}
.sorted(by: {
$0.element > $1.element
})
diff --git a/Sources/FaceLiveness/FaceDetection/FaceDetector.swift b/Sources/FaceLiveness/FaceDetection/FaceDetector.swift
index 3801eeab..1afb90c1 100644
--- a/Sources/FaceLiveness/FaceDetection/FaceDetector.swift
+++ b/Sources/FaceLiveness/FaceDetection/FaceDetector.swift
@@ -6,6 +6,7 @@
//
import AVFoundation
+@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
protocol FaceDetector {
func detectFaces(from buffer: CVPixelBuffer)
@@ -16,6 +17,10 @@ protocol FaceDetectionResultHandler: AnyObject {
func process(newResult: FaceDetectionResult)
}
+protocol FaceDetectionSessionConfigurationWrapper: AnyObject {
+ var sessionConfiguration: FaceLivenessSession.SessionConfiguration? { get }
+}
+
enum FaceDetectionResult {
case noFace
case singleFace(DetectedFace)
diff --git a/Sources/FaceLiveness/Utilities/Color+Liveness.swift b/Sources/FaceLiveness/Utilities/Color+Liveness.swift
index 6d998b58..884deb4c 100644
--- a/Sources/FaceLiveness/Utilities/Color+Liveness.swift
+++ b/Sources/FaceLiveness/Utilities/Color+Liveness.swift
@@ -9,13 +9,13 @@ import SwiftUI
extension Color {
static let livenessPrimaryBackground = Color.dynamicColors(
- light: .hex("#047D95"),
- dark: .hex("#7DD6E8")
+ light: .hex("#FF8473"),
+ dark: .hex("#FF8473")
)
static let livenessPrimaryLabel = Color.dynamicColors(
light: .white,
- dark: .hex("#0D1926")
+ dark: .white
)
static let livenessBackground = Color.dynamicColors(
@@ -44,8 +44,8 @@ extension Color {
)
static let livenessWarningLabel = Color.dynamicColors(
- light: .hex("#002266"),
- dark: .hex("#EFBF8F")
+ light: .hex("#FFFFFF"),
+ dark: .hex("#FFFFFF")
)
static let livenessPreviewBorder = Color.dynamicColors(
diff --git a/Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift b/Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift
index ccdf971f..9e864ce2 100644
--- a/Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift
+++ b/Sources/FaceLiveness/Utilities/FinalClientEvent+Init.swift
@@ -17,8 +17,14 @@ extension FinalClientEvent {
faceMatchedEnd: UInt64,
videoEnd: UInt64
) {
- let normalizedBoundingBox = sessionConfiguration
- .ovalMatchChallenge
+ let ovalMatchChallenge: FaceLivenessSession.OvalMatchChallenge
+ switch sessionConfiguration {
+ case .faceMovement(let challenge):
+ ovalMatchChallenge = challenge
+ case .faceMovementAndLight(_, let challenge):
+ ovalMatchChallenge = challenge
+ }
+ let normalizedBoundingBox = ovalMatchChallenge
.oval.boundingBox
.normalize(within: videoSize)
diff --git a/Sources/FaceLiveness/Utilities/UserAgent.swift b/Sources/FaceLiveness/Utilities/UserAgent.swift
index 6184f428..b892f6ba 100644
--- a/Sources/FaceLiveness/Utilities/UserAgent.swift
+++ b/Sources/FaceLiveness/Utilities/UserAgent.swift
@@ -10,7 +10,7 @@ import InternalAmplifyCredentials
struct UserAgentValues {
- static let libVersion = "1.3.5"
+ static let libVersion = "1.4.2"
static let libName = "amplify-ui-swift-face-liveness"
let amplifyVersion: String
diff --git a/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift b/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift
index e5edbf3f..ee9f3966 100644
--- a/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift
+++ b/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift
@@ -17,7 +17,7 @@ struct CameraPermissionView: View {
}
var body: some View {
- VStack(alignment: .leading) {
+ VStack(alignment: .center) {
Spacer()
VStack {
Text(LocalizedStrings.camera_permission_change_setting_header)
diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift
index 2e8530f3..19e2f483 100644
--- a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift
+++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift
@@ -15,7 +15,7 @@ struct CameraPreviewView: View {
@StateObject var model: CameraPreviewViewModel
- init(model: CameraPreviewViewModel = CameraPreviewViewModel()) {
+ init(model: CameraPreviewViewModel = CameraPreviewViewModel(cameraPosition: .front)) {
self._model = StateObject(wrappedValue: model)
}
diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift
index b50173b0..a46dbaa8 100644
--- a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift
+++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift
@@ -16,15 +16,18 @@ class CameraPreviewViewModel: NSObject, ObservableObject {
@Published var buffer: CVPixelBuffer?
var previewCaptureSession: LivenessCaptureSession?
+ let cameraPosition: LivenessCamera
- override init() {
+ init(cameraPosition: LivenessCamera) {
+ self.cameraPosition = cameraPosition
+
super.init()
setupSubscriptions()
let avCaptureDevice = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
- position: .front
+ position: cameraPosition == .front ? .front : .back
).devices.first
let outputDelegate = CameraPreviewOutputSampleBufferDelegate { [weak self] buffer in
diff --git a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift
index 00ecb9b7..b583c314 100644
--- a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift
+++ b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift
@@ -6,30 +6,39 @@
//
import SwiftUI
+@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
struct GetReadyPageView: View {
let beginCheckButtonDisabled: Bool
let onBegin: () -> Void
-
+ let challenge: Challenge
+ let cameraPosition: LivenessCamera
+
init(
onBegin: @escaping () -> Void,
- beginCheckButtonDisabled: Bool = false
+ beginCheckButtonDisabled: Bool = false,
+ challenge: Challenge,
+ cameraPosition: LivenessCamera
) {
self.onBegin = onBegin
self.beginCheckButtonDisabled = beginCheckButtonDisabled
+ self.challenge = challenge
+ self.cameraPosition = cameraPosition
}
var body: some View {
VStack {
ZStack {
- CameraPreviewView()
+ CameraPreviewView(model: CameraPreviewViewModel(cameraPosition: cameraPosition))
VStack {
WarningBox(
+
titleText: LocalizedStrings.get_ready_photosensitivity_title,
bodyText: LocalizedStrings.get_ready_photosensitivity_description,
popoverContent: { photosensitivityWarningPopoverContent }
)
.accessibilityElement(children: .combine)
+ .opacity(challenge == Challenge.faceMovementAndLightChallenge("2.0.0") ? 1.0 : 0.0)
Text(LocalizedStrings.preview_center_your_face_text)
.font(.title)
.multilineTextAlignment(.center)
@@ -51,7 +60,7 @@ struct GetReadyPageView: View {
)
.disabled(beginCheckButtonDisabled)
.frame(height: 52)
- ._background { Color.livenessPrimaryBackground }
+ ._background {Color(UIColor.hex("#FF8473")) }
.cornerRadius(14)
.padding([.leading, .trailing])
.padding(.bottom, 16)
@@ -60,6 +69,7 @@ struct GetReadyPageView: View {
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()
@@ -72,6 +82,9 @@ struct GetReadyPageView: View {
struct GetReadyPageView_Previews: PreviewProvider {
static var previews: some View {
- GetReadyPageView(onBegin: {})
+ GetReadyPageView(
+ onBegin: {},
+ challenge: .faceMovementAndLightChallenge("2.0.0"),
+ cameraPosition: .front)
}
}
diff --git a/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift
index ff02a3d6..0a4e0367 100644
--- a/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift
+++ b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import Combine
+@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
struct InstructionContainerView: View {
@ObservedObject var viewModel: FaceLivenessDetectionViewModel
@@ -97,13 +98,29 @@ struct InstructionContainerView: View {
argument: LocalizedStrings.challenge_verifying
)
}
- case .faceMatched:
+ case .completedNoLightCheck:
InstructionView(
- text: LocalizedStrings.challenge_instruction_hold_still,
- backgroundColor: .livenessPrimaryBackground,
- textColor: .livenessPrimaryLabel,
- font: .title
+ text: LocalizedStrings.challenge_verifying,
+ backgroundColor: .livenessBackground
)
+ .onAppear {
+ UIAccessibility.post(
+ notification: .announcement,
+ argument: LocalizedStrings.challenge_verifying
+ )
+ }
+ case .faceMatched:
+ if let challenge = viewModel.challengeReceived,
+ case .faceMovementAndLightChallenge = challenge {
+ InstructionView(
+ text: LocalizedStrings.challenge_instruction_hold_still,
+ backgroundColor: .livenessPrimaryBackground,
+ textColor: .livenessPrimaryLabel,
+ font: .title
+ )
+ } else {
+ EmptyView()
+ }
default:
EmptyView()
}
diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift
index e90a6f06..19ced079 100644
--- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift
+++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift
@@ -125,7 +125,7 @@ public struct FaceLivenessDetectionError: Error, Equatable {
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.",
diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift
index 81eacfe9..bad7e609 100644
--- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift
+++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift
@@ -16,10 +16,11 @@ import Amplify
public struct FaceLivenessDetectorView: View {
@StateObject var viewModel: FaceLivenessDetectionViewModel
@Binding var isPresented: Bool
- @State var displayState: DisplayState = .awaitingCameraPermission
+ @State var displayState: DisplayState = .awaitingChallengeType
@State var displayingCameraPermissionsNeededAlert = false
let disableStartView: Bool
+ let challengeOptions: ChallengeOptions
let onCompletion: (Result) -> Void
let sessionTask: Task
@@ -29,19 +30,20 @@ public struct FaceLivenessDetectorView: View {
credentialsProvider: AWSCredentialsProvider? = nil,
region: String,
disableStartView: Bool = false,
+ challengeOptions: ChallengeOptions = .init(),
isPresented: Binding,
onCompletion: @escaping (Result) -> Void
- ) {
+ ) {
self.disableStartView = disableStartView
self._isPresented = isPresented
self.onCompletion = onCompletion
+ self.challengeOptions = challengeOptions
self.sessionTask = Task {
let session = try await AWSPredictionsPlugin.startFaceLivenessSession(
withID: sessionID,
credentialsProvider: credentialsProvider,
region: region,
- options: .init(),
completion: map(detectionCompletion: onCompletion)
)
return session
@@ -58,28 +60,15 @@ public struct FaceLivenessDetectorView: View {
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
+ sessionID: sessionID,
+ isPreviewScreenEnabled: !disableStartView,
+ challengeOptions: challengeOptions
)
)
}
@@ -89,6 +78,7 @@ public struct FaceLivenessDetectorView: View {
credentialsProvider: AWSCredentialsProvider? = nil,
region: String,
disableStartView: Bool = false,
+ challengeOptions: ChallengeOptions = .init(),
isPresented: Binding,
onCompletion: @escaping (Result) -> Void,
captureSession: LivenessCaptureSession
@@ -96,13 +86,13 @@ public struct FaceLivenessDetectorView: View {
self.disableStartView = disableStartView
self._isPresented = isPresented
self.onCompletion = onCompletion
+ self.challengeOptions = challengeOptions
self.sessionTask = Task {
let session = try await AWSPredictionsPlugin.startFaceLivenessSession(
withID: sessionID,
credentialsProvider: credentialsProvider,
region: region,
- options: .init(),
completion: map(detectionCompletion: onCompletion)
)
return session
@@ -116,42 +106,104 @@ public struct FaceLivenessDetectorView: View {
wrappedValue: .init(
faceDetector: captureSession.outputSampleBufferCapturer!.faceDetector,
faceInOvalMatching: faceInOvalStateMatching,
- captureSession: captureSession,
videoChunker: captureSession.outputSampleBufferCapturer!.videoChunker,
closeButtonAction: { onCompletion(.failure(.userCancelled)) },
- sessionID: sessionID
+ sessionID: sessionID,
+ isPreviewScreenEnabled: !disableStartView,
+ challengeOptions: challengeOptions
)
)
}
public var body: some View {
switch displayState {
- case .awaitingLivenessSession:
+ case .awaitingChallengeType:
+ LoadingPageView()
+ .onAppear {
+ Task {
+ do {
+ let session = try await sessionTask.value
+ viewModel.livenessService = session
+ viewModel.registerServiceEvents(onChallengeTypeReceived: { challenge in
+ self.displayState = DisplayState.awaitingCameraPermission(challenge)
+ })
+ viewModel.initializeLivenessStream()
+ } catch let error as FaceLivenessDetectionError {
+ switch error {
+ case .unknown:
+ viewModel.livenessState.unrecoverableStateEncountered(.unknown)
+ case .sessionTimedOut,
+ .faceInOvalMatchExceededTimeLimitError,
+ .countdownFaceTooClose,
+ .countdownMultipleFaces,
+ .countdownNoFace:
+ viewModel.livenessState.unrecoverableStateEncountered(.timedOut)
+ case .cameraPermissionDenied:
+ viewModel.livenessState.unrecoverableStateEncountered(.missingVideoPermission)
+ case .userCancelled:
+ viewModel.livenessState.unrecoverableStateEncountered(.userCancelled)
+ case .socketClosed:
+ viewModel.livenessState.unrecoverableStateEncountered(.socketClosed)
+ case .cameraNotAvailable:
+ viewModel.livenessState.unrecoverableStateEncountered(.cameraNotAvailable)
+ default:
+ viewModel.livenessState.unrecoverableStateEncountered(.couldNotOpenStream)
+ }
+ } catch {
+ viewModel.livenessState.unrecoverableStateEncountered(.couldNotOpenStream)
+ }
+
+ DispatchQueue.main.async {
+ if let faceDetector = viewModel.faceDetector as? FaceDetectorShortRange.Model {
+ faceDetector.setFaceDetectionSessionConfigurationWrapper(configuration: viewModel)
+ }
+ }
+ }
+ }
+ .onReceive(viewModel.$livenessState) { output in
+ switch output.state {
+ case .encounteredUnrecoverableError(let error):
+ let closeCode = error.webSocketCloseCode ?? .normalClosure
+ viewModel.livenessService?.closeSocket(with: closeCode)
+ isPresented = false
+ onCompletion(.failure(mapError(error)))
+ default:
+ break
+ }
+ }
+ case .awaitingCameraPermission(let challenge):
+ CameraPermissionView(displayingCameraPermissionsNeededAlert: $displayingCameraPermissionsNeededAlert)
+ .onAppear {
+ checkCameraPermission(for: challenge)
+ }
+ case .awaitingLivenessSession(let challenge):
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
+ let cameraPosition: LivenessCamera
+ switch challenge {
+ case .faceMovementAndLightChallenge:
+ cameraPosition = challengeOptions.faceMovementAndLightChallengeOption.camera
+ case .faceMovementChallenge:
+ cameraPosition = challengeOptions.faceMovementChallengeOption.camera
}
+
+ let newState = disableStartView
+ ? DisplayState.displayingLiveness
+ : DisplayState.displayingGetReadyView(challenge, cameraPosition)
+ guard self.displayState != newState else { return }
+ self.displayState = newState
}
}
-
- case .displayingGetReadyView:
+ case .displayingGetReadyView(let challenge, let cameraPosition):
GetReadyPageView(
onBegin: {
guard displayState != .displayingLiveness else { return }
displayState = .displayingLiveness
},
- beginCheckButtonDisabled: false
+ beginCheckButtonDisabled: false,
+ challenge: challenge,
+ cameraPosition: cameraPosition
)
.onAppear {
DispatchQueue.main.async {
@@ -189,11 +241,6 @@ public struct FaceLivenessDetectorView: View {
break
}
}
- case .awaitingCameraPermission:
- CameraPermissionView(displayingCameraPermissionsNeededAlert: $displayingCameraPermissionsNeededAlert)
- .onAppear {
- checkCameraPermission()
- }
}
}
@@ -203,7 +250,7 @@ public struct FaceLivenessDetectorView: View {
return .userCancelled
case .timedOut:
return .faceInOvalMatchExceededTimeLimitError
- case .socketClosed:
+ case .couldNotOpenStream, .socketClosed:
return .socketClosed
case .cameraNotAvailable:
return .cameraNotAvailable
@@ -212,41 +259,58 @@ public struct FaceLivenessDetectorView: View {
}
}
- private func requestCameraPermission() {
+ private func requestCameraPermission(for challenge: Challenge) {
AVCaptureDevice.requestAccess(
for: .video,
completionHandler: { accessGranted in
guard accessGranted == true else { return }
- displayState = .awaitingLivenessSession
+ displayState = .awaitingLivenessSession(challenge)
}
)
-
}
private func alertCameraAccessNeeded() {
displayingCameraPermissionsNeededAlert = true
}
- private func checkCameraPermission() {
+ private func checkCameraPermission(for challenge: Challenge) {
let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch cameraAuthorizationStatus {
case .notDetermined:
- requestCameraPermission()
+ requestCameraPermission(for: challenge)
case .restricted, .denied:
alertCameraAccessNeeded()
case .authorized:
- displayState = .awaitingLivenessSession
+ displayState = .awaitingLivenessSession(challenge)
@unknown default:
break
}
}
}
-enum DisplayState {
- case awaitingLivenessSession
- case displayingGetReadyView
+enum DisplayState: Equatable {
+ case awaitingChallengeType
+ case awaitingCameraPermission(Challenge)
+ case awaitingLivenessSession(Challenge)
+ case displayingGetReadyView(Challenge, LivenessCamera)
case displayingLiveness
- case awaitingCameraPermission
+
+ static func == (lhs: DisplayState, rhs: DisplayState) -> Bool {
+ switch (lhs, rhs) {
+ case (.awaitingChallengeType, .awaitingChallengeType):
+ return true
+ case (let .awaitingLivenessSession(c1), let .awaitingLivenessSession(c2)):
+ return c1 == c2
+ case (let .displayingGetReadyView(c1, position1), let .displayingGetReadyView(c2, position2)):
+ return c1 == c2 && position1 == position2
+ case (.displayingLiveness, .displayingLiveness):
+ return true
+ case (.awaitingCameraPermission, .awaitingCameraPermission):
+ return true
+ default:
+ return false
+ }
+ }
}
enum InstructionState {
@@ -282,3 +346,39 @@ private func map(detectionCompletion: @escaping (Result Void
let videoChunker: VideoChunker
let sessionID: String
@@ -28,44 +29,60 @@ class FaceLivenessDetectionViewModel: ObservableObject {
let faceDetector: FaceDetector
let faceInOvalMatching: FaceInOvalMatching
let challengeID: String = UUID().uuidString
+ let isPreviewScreenEnabled : Bool
var colorSequences: [ColorSequence] = []
var hasSentFinalVideoEvent = false
var hasSentFirstVideo = false
var layerRectConverted: (CGRect) -> CGRect = { $0 }
var sessionConfiguration: FaceLivenessSession.SessionConfiguration?
+ var challengeReceived: Challenge?
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?
+ let challengeOptions: ChallengeOptions
+
+ static var attemptCount: Int = 0
+ static var attemptIdTimeStamp: Date = Date()
var noFitTimeoutInterval: TimeInterval {
- if let sessionTimeoutMilliSec = sessionConfiguration?.ovalMatchChallenge.oval.ovalFitTimeout {
- return TimeInterval(sessionTimeoutMilliSec/1_000)
- } else {
+ guard let sessionConfiguration = sessionConfiguration else {
return defaultNoFitTimeoutInterval
}
+
+ let ovalMatchChallenge: FaceLivenessSession.OvalMatchChallenge
+ switch sessionConfiguration{
+ case .faceMovement(let challenge):
+ ovalMatchChallenge = challenge
+ case .faceMovementAndLight(_, let challenge):
+ ovalMatchChallenge = challenge
+ }
+
+ let sessionTimeoutMilliSec = ovalMatchChallenge.oval.ovalFitTimeout
+ return TimeInterval(sessionTimeoutMilliSec/1_000)
}
init(
faceDetector: FaceDetector,
faceInOvalMatching: FaceInOvalMatching,
- captureSession: LivenessCaptureSession,
videoChunker: VideoChunker,
stateMachine: LivenessStateMachine = .init(state: .initial),
closeButtonAction: @escaping () -> Void,
- sessionID: String
+ sessionID: String,
+ isPreviewScreenEnabled: Bool,
+ challengeOptions: ChallengeOptions
) {
self.closeButtonAction = closeButtonAction
self.videoChunker = videoChunker
self.livenessState = stateMachine
self.sessionID = sessionID
- self.captureSession = captureSession
self.faceDetector = faceDetector
self.faceInOvalMatching = faceInOvalMatching
+ self.isPreviewScreenEnabled = isPreviewScreenEnabled
+ self.challengeOptions = challengeOptions
self.closeButtonAction = { [weak self] in
guard let self else { return }
@@ -89,7 +106,7 @@ class FaceLivenessDetectionViewModel: ObservableObject {
NotificationCenter.default.removeObserver(self)
}
- func registerServiceEvents() {
+ func registerServiceEvents(onChallengeTypeReceived: @escaping (Challenge) -> Void) {
livenessService?.register(onComplete: { [weak self] reason in
self?.stopRecording()
@@ -112,6 +129,14 @@ class FaceLivenessDetectionViewModel: ObservableObject {
},
on: .challenge
)
+
+ livenessService?.register(
+ listener: { [weak self] _challenge in
+ self?.challengeReceived = _challenge
+ self?.configureCaptureSession(challenge: _challenge)
+ onChallengeTypeReceived(_challenge)
+ },
+ on: .challenge)
}
@objc func willResignActive(_ notification: Notification) {
@@ -123,16 +148,16 @@ class FaceLivenessDetectionViewModel: ObservableObject {
}
func startSession() {
- captureSession.startSession()
+ captureSession?.startSession()
}
func stopRecording() {
- captureSession.stopRunning()
+ captureSession?.stopRunning()
}
func configureCamera(withinFrame frame: CGRect) -> CALayer? {
do {
- let avLayer = try captureSession.configureCamera(frame: frame)
+ let avLayer = try captureSession?.configureCamera(frame: frame)
DispatchQueue.main.async {
self.livenessState.checkIsFacePrepared()
}
@@ -149,9 +174,17 @@ class FaceLivenessDetectionViewModel: ObservableObject {
func drawOval(onComplete: @escaping () -> Void) {
guard livenessState.state == .recording(ovalDisplayed: false),
- let ovalParameters = sessionConfiguration?.ovalMatchChallenge.oval
- else { return }
-
+ let sessionConfiguration = sessionConfiguration else { return }
+
+ let ovalMatchChallenge: FaceLivenessSession.OvalMatchChallenge
+ switch sessionConfiguration {
+ case .faceMovement(let challenge):
+ ovalMatchChallenge = challenge
+ case .faceMovementAndLight(_, let challenge):
+ ovalMatchChallenge = challenge
+ }
+
+ let ovalParameters = ovalMatchChallenge.oval
let scaleRatio = cameraViewRect.width / videoSize.width
let rect = CGRect(
x: ovalParameters.boundingBox.x,
@@ -178,9 +211,21 @@ class FaceLivenessDetectionViewModel: ObservableObject {
func initializeLivenessStream() {
do {
+ if (abs(Self.attemptIdTimeStamp.timeIntervalSinceNow) > defaultAttemptCountResetInterval) {
+ Self.attemptCount = 1
+ } else {
+ Self.attemptCount += 1
+ }
+ Self.attemptIdTimeStamp = Date()
+
try livenessService?.initializeLivenessStream(
withSessionID: sessionID,
- userAgent: UserAgentValues.standard().userAgentString
+ userAgent: UserAgentValues.standard().userAgentString,
+ challenges: [challengeOptions.faceMovementChallengeOption.challenge,
+ challengeOptions.faceMovementAndLightChallengeOption.challenge],
+ options: .init(
+ attemptCount: Self.attemptCount,
+ preCheckViewEnabled: isPreviewScreenEnabled)
)
} catch {
DispatchQueue.main.async {
@@ -226,6 +271,8 @@ class FaceLivenessDetectionViewModel: ObservableObject {
videoStartTime: UInt64
) {
guard initialClientEvent == nil else { return }
+ guard let challengeReceived else { return }
+
videoChunker.start()
let initialFace = FaceDetection(
@@ -243,7 +290,8 @@ class FaceLivenessDetectionViewModel: ObservableObject {
do {
try livenessService?.send(
- .initialFaceDetected(event: _initialClientEvent),
+ .initialFaceDetected(event: _initialClientEvent,
+ challenge: challengeReceived),
eventDate: { .init() }
)
} catch {
@@ -254,14 +302,14 @@ class FaceLivenessDetectionViewModel: ObservableObject {
}
func sendFinalEvent(
- targetFaceRect: CGRect,
viewSize: CGSize,
faceMatchedEnd: UInt64
) {
guard
let sessionConfiguration,
let initialClientEvent,
- let faceMatchedTimestamp
+ let faceMatchedTimestamp,
+ let challengeReceived
else { return }
let finalClientEvent = FinalClientEvent(
@@ -275,7 +323,8 @@ class FaceLivenessDetectionViewModel: ObservableObject {
do {
try livenessService?.send(
- .final(event: finalClientEvent),
+ .final(event: finalClientEvent,
+ challenge: challengeReceived),
eventDate: { .init() }
)
@@ -294,7 +343,6 @@ class FaceLivenessDetectionViewModel: ObservableObject {
func sendFinalVideoEvent() {
sendFinalEvent(
- targetFaceRect: faceGuideRect,
viewSize: videoSize,
faceMatchedEnd: Date().timestampMilliseconds
)
@@ -304,10 +352,15 @@ class FaceLivenessDetectionViewModel: ObservableObject {
}
}
- func handleFreshnessComplete(faceGuide: CGRect) {
+ func handleFreshnessComplete() {
DispatchQueue.main.async {
self.livenessState.completedDisplayingFreshness()
- self.faceGuideRect = faceGuide
+ }
+ }
+
+ func completeNoLightCheck() {
+ DispatchQueue.main.async {
+ self.livenessState.completedNoLightCheck()
}
}
@@ -361,4 +414,29 @@ class FaceLivenessDetectionViewModel: ObservableObject {
}
return data
}
+
+ func configureCaptureSession(challenge: Challenge) {
+ let cameraPosition: LivenessCamera
+ switch challenge {
+ case .faceMovementChallenge:
+ cameraPosition = challengeOptions.faceMovementChallengeOption.camera
+ case .faceMovementAndLightChallenge:
+ cameraPosition = challengeOptions.faceMovementAndLightChallengeOption.camera
+ }
+
+ let avCaptureDevice = AVCaptureDevice.default(
+ .builtInWideAngleCamera,
+ for: .video,
+ position: cameraPosition == .front ? .front : .back)
+
+ self.captureSession = LivenessCaptureSession(
+ captureDevice: .init(avCaptureDevice: avCaptureDevice),
+ outputDelegate: OutputSampleBufferCapturer(
+ faceDetector: self.faceDetector,
+ videoChunker: self.videoChunker
+ )
+ )
+ }
}
+
+extension FaceLivenessDetectionViewModel: FaceDetectionSessionConfigurationWrapper { }
diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift
index 5786620b..8fff8b9f 100644
--- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift
+++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessViewControllerPresenter.swift
@@ -12,4 +12,5 @@ protocol FaceLivenessViewControllerPresenter: AnyObject {
func drawOvalInCanvas(_ ovalRect: CGRect)
func displayFreshness(colorSequences: [FaceLivenessSession.DisplayColor])
func displaySingleFrame(uiImage: UIImage)
+ func completeNoLightCheck()
}
diff --git a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift
index c59629c9..62e563ef 100644
--- a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift
+++ b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift
@@ -76,6 +76,10 @@ struct LivenessStateMachine {
mutating func completedDisplayingFreshness() {
state = .completedDisplayingFreshness
}
+
+ mutating func completedNoLightCheck() {
+ state = .completedNoLightCheck
+ }
mutating func displayingFreshness() {
state = .displayingFreshness
@@ -95,6 +99,7 @@ struct LivenessStateMachine {
enum State: Equatable {
case initial
+ case awaitingChallengeType
case pendingFacePreparedConfirmation(FaceNotPreparedReason)
case recording(ovalDisplayed: Bool)
case awaitingFaceInOvalMatch(FaceNotPreparedReason, Double)
@@ -102,6 +107,7 @@ struct LivenessStateMachine {
case initialClientInfoEventSent
case displayingFreshness
case completedDisplayingFreshness
+ case completedNoLightCheck
case completed
case awaitingDisconnectEvent
case disconnectEventReceived
@@ -159,7 +165,7 @@ struct LivenessStateMachine {
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 let cameraNotAvailable = LivenessError(code: 9, webSocketCloseCode: .unexpectedRuntimeError)
static func == (lhs: LivenessError, rhs: LivenessError) -> Bool {
lhs.code == rhs.code
diff --git a/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift b/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift
index c274bde0..35952c12 100644
--- a/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift
+++ b/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift
@@ -105,7 +105,6 @@ final class _LivenessViewController: UIViewController {
var initialFace: FaceDetection?
var videoStartTimeStamp: UInt64?
var faceMatchStartTime: UInt64?
- var faceGuideRect: CGRect?
var freshnessEventsComplete = false
var videoSentCount = 0
var hasSentFinalEvent = false
@@ -146,9 +145,7 @@ extension _LivenessViewController: FaceLivenessViewControllerPresenter {
guard let self else { return }
self.freshnessView.removeFromSuperview()
- self.viewModel.handleFreshnessComplete(
- faceGuide: self.faceGuideRect!
- )
+ self.viewModel.handleFreshnessComplete()
}
)
}
@@ -156,7 +153,6 @@ extension _LivenessViewController: FaceLivenessViewControllerPresenter {
func drawOvalInCanvas(_ ovalRect: CGRect) {
DispatchQueue.main.async {
guard let previewLayer = self.previewLayer else { return }
- self.faceGuideRect = ovalRect
let ovalView = OvalView(
frame: previewLayer.frame,
@@ -173,4 +169,8 @@ extension _LivenessViewController: FaceLivenessViewControllerPresenter {
self.ovalExists = true
}
}
+
+ func completeNoLightCheck() {
+ self.viewModel.completeNoLightCheck()
+ }
}
diff --git a/Sources/FaceLiveness/Views/LoadingPage/LoadingPageView.swift b/Sources/FaceLiveness/Views/LoadingPage/LoadingPageView.swift
new file mode 100644
index 00000000..e02b4e79
--- /dev/null
+++ b/Sources/FaceLiveness/Views/LoadingPage/LoadingPageView.swift
@@ -0,0 +1,27 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import SwiftUI
+
+struct LoadingPageView: View {
+
+ var body: some View {
+ VStack {
+ HStack(spacing: 5) {
+ ProgressView()
+ Text(LocalizedStrings.challenge_connecting)
+ }
+
+ }
+ }
+}
+
+struct LoadingPageView_Previews: PreviewProvider {
+ static var previews: some View {
+ LoadingPageView()
+ }
+}
diff --git a/Sources/FaceLiveness/Views/WarningBox.swift b/Sources/FaceLiveness/Views/WarningBox.swift
index 050c2aa4..8cf89319 100644
--- a/Sources/FaceLiveness/Views/WarningBox.swift
+++ b/Sources/FaceLiveness/Views/WarningBox.swift
@@ -53,7 +53,7 @@ struct WarningBox: View {
.padding()
.background(
Rectangle()
- .foregroundColor(.livenessWarningBackground)
+ .foregroundColor(Color(UIColor.hex("#FF8473")))
.cornerRadius(6)
)
}
diff --git a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift
index 7d69251b..988fcc4f 100644
--- a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift
+++ b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift
@@ -27,21 +27,16 @@ final class CredentialsProviderTestCase: XCTestCase {
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
+ sessionID: UUID().uuidString,
+ isPreviewScreenEnabled: false,
+ challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front),
+ faceMovementAndLightChallengeOption: .init())
)
self.videoChunker = videoChunker
@@ -65,6 +60,8 @@ final class CredentialsProviderTestCase: XCTestCase {
sessionID: UUID().uuidString,
credentialsProvider: credentialsProvider,
region: "us-east-1",
+ challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front),
+ faceMovementAndLightChallengeOption: .init()),
isPresented: .constant(true),
onCompletion: { _ in }
)
@@ -101,6 +98,8 @@ final class CredentialsProviderTestCase: XCTestCase {
sessionID: UUID().uuidString,
credentialsProvider: credentialsProvider,
region: "us-east-1",
+ challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front),
+ faceMovementAndLightChallengeOption: .init()),
isPresented: .constant(true),
onCompletion: { _ in }
)
diff --git a/Tests/FaceLivenessTests/DetectedFaceTests.swift b/Tests/FaceLivenessTests/DetectedFaceTests.swift
index 4bee8292..6d538e33 100644
--- a/Tests/FaceLivenessTests/DetectedFaceTests.swift
+++ b/Tests/FaceLivenessTests/DetectedFaceTests.swift
@@ -7,7 +7,7 @@
import XCTest
@testable import FaceLiveness
-
+@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
final class DetectedFaceTests: XCTestCase {
var detectedFace: DetectedFace!
@@ -104,7 +104,29 @@ final class DetectedFaceTests: XCTestCase {
width: 0.6240418540649166,
height: 0.8144985824018897
)
- let boundingBox = detectedFace.boundingBoxFromLandmarks(ovalRect: ovalRect)
+
+ let face = FaceLivenessSession.OvalMatchChallenge.Face(
+ distanceThreshold: 0.1,
+ distanceThresholdMax: 0.1,
+ distanceThresholdMin: 0.1,
+ iouWidthThreshold: 0.1,
+ iouHeightThreshold: 0.1
+ )
+
+ let oval = FaceLivenessSession.OvalMatchChallenge.Oval(boundingBox: .init(x: 0.1,
+ y: 0.1,
+ width: 0.1,
+ height: 0.1),
+ heightWidthRatio: 1.618,
+ iouThreshold: 0.1,
+ iouWidthThreshold: 0.1,
+ iouHeightThreshold: 0.1,
+ ovalFitTimeout: 1)
+
+ let boundingBox = detectedFace.boundingBoxFromLandmarks(ovalRect: ovalRect,
+ ovalMatchChallenge: .init(faceDetectionThreshold: 0.7,
+ face: face,
+ oval: oval))
XCTAssertEqual(boundingBox.origin.x, expectedBoundingBox.origin.x)
XCTAssertEqual(boundingBox.origin.y, expectedBoundingBox.origin.y)
XCTAssertEqual(boundingBox.width, expectedBoundingBox.width)
diff --git a/Tests/FaceLivenessTests/LivenessTests.swift b/Tests/FaceLivenessTests/LivenessTests.swift
index da063930..9180841f 100644
--- a/Tests/FaceLivenessTests/LivenessTests.swift
+++ b/Tests/FaceLivenessTests/LivenessTests.swift
@@ -18,21 +18,16 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
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
+ sessionID: UUID().uuidString,
+ isPreviewScreenEnabled: false,
+ challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front),
+ faceMovementAndLightChallengeOption: .init())
)
self.videoChunker = videoChunker
@@ -69,6 +64,7 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
/// Then: The end state of this flow is `.faceMatched`
func testHappyPathToMatchedFace() async throws {
viewModel.livenessService = self.livenessService
+ viewModel.challengeReceived = .faceMovementAndLightChallenge("2.0.0")
viewModel.livenessState.checkIsFacePrepared()
XCTAssertEqual(viewModel.livenessState.state, .pendingFacePreparedConfirmation(.pendingCheck))
@@ -104,15 +100,38 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
"setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)"
])
XCTAssertEqual(livenessService.interactions, [
- "initializeLivenessStream(withSessionID:userAgent:)"
+ "initializeLivenessStream(withSessionID:userAgent:challenges:options:)"
])
}
/// 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
+ /// Then: The end state of this flow is `.recording(ovalDisplayed: false)`
func testTransitionToRecordingState() async throws {
viewModel.livenessService = self.livenessService
+ viewModel.challengeReceived = .faceMovementChallenge("1.0.0")
+
+ let face = FaceLivenessSession.OvalMatchChallenge.Face(
+ distanceThreshold: 0.32,
+ distanceThresholdMax: 0.1,
+ distanceThresholdMin: 0.1,
+ iouWidthThreshold: 0.1,
+ iouHeightThreshold: 0.1
+ )
+
+ let oval = FaceLivenessSession.OvalMatchChallenge.Oval(boundingBox: .init(x: 0.1,
+ y: 0.1,
+ width: 0.1,
+ height: 0.1),
+ heightWidthRatio: 1.618,
+ iouThreshold: 0.1,
+ iouWidthThreshold: 0.1,
+ iouHeightThreshold: 0.1,
+ ovalFitTimeout: 1)
+
+ viewModel.sessionConfiguration = .faceMovement(.init(faceDetectionThreshold: 0.7,
+ face: face,
+ oval: oval))
viewModel.livenessState.checkIsFacePrepared()
XCTAssertEqual(viewModel.livenessState.state, .pendingFacePreparedConfirmation(.pendingCheck))
@@ -136,9 +155,6 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
XCTAssertEqual(faceDetector.interactions, [
"setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)"
])
- XCTAssertEqual(livenessService.interactions, [
- "initializeLivenessStream(withSessionID:userAgent:)"
- ])
}
/// Given: A `FaceLivenessDetectionViewModel`
@@ -174,4 +190,55 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
try await Task.sleep(seconds: 1)
XCTAssertEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
}
+
+ /// Given: A `FaceLivenessDetectionViewModel`
+ /// When: The initializeLivenessStream() is called for the first time and then called again after 3 seconds
+ /// Then: The attempt count is incremented
+ func testAttemptCountIncrementFirstTime() async throws {
+ viewModel.livenessService = self.livenessService
+ self.viewModel.initializeLivenessStream()
+ XCTAssertEqual(livenessService.interactions, [
+ "initializeLivenessStream(withSessionID:userAgent:challenges:options:)"
+ ])
+
+ XCTAssertEqual(FaceLivenessDetectionViewModel.attemptCount, 1)
+ try await Task.sleep(seconds: 3)
+
+ self.viewModel.initializeLivenessStream()
+ XCTAssertEqual(livenessService.interactions, [
+ "initializeLivenessStream(withSessionID:userAgent:challenges:options:)",
+ "initializeLivenessStream(withSessionID:userAgent:challenges:options:)"
+ ])
+ XCTAssertEqual(FaceLivenessDetectionViewModel.attemptCount, 2)
+ }
+
+ /// Given: A `FaceLivenessDetectionViewModel`
+ /// When: The attempt count is 4, last attempt time was < 5 minutes and initializeLivenessStream() is called
+ /// Then: The attempt count is incremented
+ func testAttemptCountIncrement() async throws {
+ viewModel.livenessService = self.livenessService
+ FaceLivenessDetectionViewModel.attemptCount = 4
+ FaceLivenessDetectionViewModel.attemptIdTimeStamp = Date().addingTimeInterval(-180)
+ self.viewModel.initializeLivenessStream()
+ XCTAssertEqual(livenessService.interactions, [
+ "initializeLivenessStream(withSessionID:userAgent:challenges:options:)"
+ ])
+
+ XCTAssertEqual(FaceLivenessDetectionViewModel.attemptCount, 5)
+ }
+
+ /// Given: A `FaceLivenessDetectionViewModel`
+ /// When: The attempt count is 4, last attempt time was > 5 minutes and initializeLivenessStream() is called
+ /// Then: The attempt count is not incremented and reset to 1
+ func testAttemptCountReset() async throws {
+ viewModel.livenessService = self.livenessService
+ FaceLivenessDetectionViewModel.attemptCount = 4
+ FaceLivenessDetectionViewModel.attemptIdTimeStamp = Date().addingTimeInterval(-305)
+ self.viewModel.initializeLivenessStream()
+ XCTAssertEqual(livenessService.interactions, [
+ "initializeLivenessStream(withSessionID:userAgent:challenges:options:)"
+ ])
+
+ XCTAssertEqual(FaceLivenessDetectionViewModel.attemptCount, 1)
+ }
}
diff --git a/Tests/FaceLivenessTests/MockLivenessService.swift b/Tests/FaceLivenessTests/MockLivenessService.swift
index 2b4633d1..d3e43a8d 100644
--- a/Tests/FaceLivenessTests/MockLivenessService.swift
+++ b/Tests/FaceLivenessTests/MockLivenessService.swift
@@ -18,7 +18,7 @@ class MockLivenessService {
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 onInitializeLivenessStream: (String, String,[Challenge]?,FaceLivenessSession.Options) -> Void = { _, _, _, _ in }
var onServiceException: (FaceLivenessSessionError) -> Void = { _ in }
var onCloseSocket: (URLSessionWebSocketTask.CloseCode) -> Void = { _ in }
}
@@ -44,10 +44,13 @@ extension MockLivenessService: LivenessService {
}
func initializeLivenessStream(
- withSessionID sessionID: String, userAgent: String
+ withSessionID sessionID: String,
+ userAgent: String,
+ challenges: [Challenge],
+ options: FaceLivenessSession.Options
) throws {
interactions.append(#function)
- onInitializeLivenessStream(sessionID, userAgent)
+ onInitializeLivenessStream(sessionID, userAgent, challenges, options)
}
func register(
@@ -62,6 +65,10 @@ extension MockLivenessService: LivenessService {
) {
interactions.append(#function)
}
+
+ func register(listener: @escaping (Challenge) -> Void, on event: LivenessEventKind.Server) {
+ interactions.append(#function)
+ }
func closeSocket(with code: URLSessionWebSocketTask.CloseCode) {
interactions.append(#function)