diff --git a/.gitignore b/.gitignore index df4f2021..c5a07310 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ HostApp/dist/ HostApp/node_modules/ HostApp/generated-src/ HostApp/aws-exports.js -HostApp/.gitignore HostApp/awsconfiguration.json HostApp/amplifyconfiguration.json HostApp/amplifyconfiguration.dart @@ -21,4 +20,19 @@ HostApp/amplify-build-config.json HostApp/amplify-gradle-config.json HostApp/amplifytools.xcconfig HostApp/.secret-* -HostApp/**.sample \ No newline at end of file +HostApp/**.sample +Tests/IntegrationTestApp/amplify/ +Tests/IntegrationTestApp/build/ +Tests/IntegrationTestApp/dist/ +Tests/IntegrationTestApp/node_modules/ +Tests/IntegrationTestApp/generated-src/ +Tests/IntegrationTestApp/aws-exports.js +Tests/IntegrationTestApp/awsconfiguration.json +Tests/IntegrationTestApp/amplifyconfiguration.json +Tests/IntegrationTestApp/amplifyconfiguration.dart +Tests/IntegrationTestApp/amplify-build-config.json +Tests/IntegrationTestApp/amplify-gradle-config.json +Tests/IntegrationTestApp/amplifytools.xcconfig +Tests/IntegrationTestApp/.secret-* +Tests/IntegrationTestApp/**.sample +Tests/IntegrationTestApp/*.xcodeproj \ No newline at end of file diff --git a/Sources/FaceLiveness/AV/LivenessCaptureSession.swift b/Sources/FaceLiveness/AV/LivenessCaptureSession.swift index 99af46b0..995a09ae 100644 --- a/Sources/FaceLiveness/AV/LivenessCaptureSession.swift +++ b/Sources/FaceLiveness/AV/LivenessCaptureSession.swift @@ -8,18 +8,18 @@ import UIKit import AVFoundation -final class LivenessCaptureSession { +class LivenessCaptureSession { let captureDevice: LivenessCaptureDevice private let captureQueue = DispatchQueue(label: "com.amazonaws.faceliveness.cameracapturequeue") - private let outputDelegate: OutputSampleBufferCapturer - private var captureSession: AVCaptureSession? + let outputDelegate: OutputSampleBufferCapturer + var captureSession: AVCaptureSession? init(captureDevice: LivenessCaptureDevice, outputDelegate: OutputSampleBufferCapturer) { self.captureDevice = captureDevice self.outputDelegate = outputDelegate } - func startSession(frame: CGRect) throws -> AVCaptureVideoPreviewLayer { + func startSession(frame: CGRect) throws -> CALayer { guard let camera = captureDevice.avCaptureDevice else { throw LivenessCaptureSessionError.cameraUnavailable } diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index de25be61..415a59e5 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -83,6 +83,46 @@ public struct FaceLivenessDetectorView: View { ) ) } + + init( + sessionID: String, + credentialsProvider: AWSCredentialsProvider? = nil, + region: String, + disableStartView: Bool = false, + isPresented: Binding, + onCompletion: @escaping (Result) -> Void, + captureSession: LivenessCaptureSession + ) { + self.disableStartView = disableStartView + self._isPresented = isPresented + self.onCompletion = onCompletion + + self.sessionTask = Task { + let session = try await AWSPredictionsPlugin.startFaceLivenessSession( + withID: sessionID, + credentialsProvider: credentialsProvider, + region: region, + options: .init(), + completion: map(detectionCompletion: onCompletion) + ) + return session + } + + let faceInOvalStateMatching = FaceInOvalMatching( + instructor: Instructor() + ) + + self._viewModel = StateObject( + wrappedValue: .init( + faceDetector: captureSession.outputDelegate.faceDetector, + faceInOvalMatching: faceInOvalStateMatching, + captureSession: captureSession, + videoChunker: captureSession.outputDelegate.videoChunker, + closeButtonAction: { onCompletion(.failure(.userCancelled)) }, + sessionID: sessionID + ) + ) + } public var body: some View { switch displayState { @@ -204,7 +244,7 @@ enum InstructionState { case display(text: String) } -fileprivate func map(detectionCompletion: @escaping (Result) -> Void) -> ((Result) -> Void) { +private func map(detectionCompletion: @escaping (Result) -> Void) -> ((Result) -> Void) { { result in switch result { case .success: diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift index 992f2a92..e21d261c 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift @@ -117,11 +117,9 @@ class FaceLivenessDetectionViewModel: ObservableObject { captureSession.stopRunning() } - func startCamera(withinFrame frame: CGRect) -> AVCaptureVideoPreviewLayer? { + func startCamera(withinFrame frame: CGRect) -> CALayer? { do { let avLayer = try captureSession.startSession(frame: frame) - avLayer.frame = frame - layerRectConverted = avLayer.layerRectConverted(fromMetadataOutputRect:) DispatchQueue.main.async { self.livenessState.checkIsFacePrepared() } diff --git a/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift b/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift index bcb498cd..0189c3c8 100644 --- a/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift +++ b/Sources/FaceLiveness/Views/Liveness/LivenessViewController.swift @@ -13,7 +13,7 @@ import Amplify final class _LivenessViewController: UIViewController { let viewModel: FaceLivenessDetectionViewModel - var previewLayer: AVCaptureVideoPreviewLayer! + var previewLayer: CALayer! let faceShapeLayer = CAShapeLayer() var ovalExists = false @@ -86,13 +86,6 @@ final class _LivenessViewController: UIViewController { } } - - - func convert(rect: CGRect) -> CGRect { - let box = previewLayer.layerRectConverted(fromMetadataOutputRect: rect) - return box - } - var runningFreshness = false var hasSentClientInformationEvent = false var challengeID = UUID().uuidString diff --git a/Tests/IntegrationTestApp/IntegrationTestApp.xcodeproj/project.pbxproj b/Tests/IntegrationTestApp/IntegrationTestApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..cae7d428 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp.xcodeproj/project.pbxproj @@ -0,0 +1,687 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 732CF8492A3183F3004D0BE3 /* FaceLiveness in Frameworks */ = {isa = PBXBuildFile; productRef = 732CF8482A3183F3004D0BE3 /* FaceLiveness */; }; + 732CF84E2A31871D004D0BE3 /* MockLivenessCaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732CF84D2A31871D004D0BE3 /* MockLivenessCaptureSession.swift */; }; + 732CF8502A3187E0004D0BE3 /* FaceLivenessDetectorView+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732CF84F2A3187E0004D0BE3 /* FaceLivenessDetectorView+Mock.swift */; }; + 732CF8522A318F7D004D0BE3 /* mock.mov in Resources */ = {isa = PBXBuildFile; fileRef = 732CF8512A318F7D004D0BE3 /* mock.mov */; }; + 7336965E2A312F17009448F0 /* ExampleLivenessViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696542A312F17009448F0 /* ExampleLivenessViewModel.swift */; }; + 7336965F2A312F17009448F0 /* StartSessionView+PresentationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696552A312F17009448F0 /* StartSessionView+PresentationState.swift */; }; + 733696602A312F17009448F0 /* LivenessResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696562A312F17009448F0 /* LivenessResultView.swift */; }; + 733696612A312F17009448F0 /* ExampleLivenessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696572A312F17009448F0 /* ExampleLivenessView.swift */; }; + 733696622A312F17009448F0 /* StartSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696582A312F17009448F0 /* StartSessionView.swift */; }; + 733696632A312F17009448F0 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696592A312F17009448F0 /* RootView.swift */; }; + 733696642A312F17009448F0 /* LivenessCheckErrorContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336965A2A312F17009448F0 /* LivenessCheckErrorContentView.swift */; }; + 733696652A312F17009448F0 /* LivenessResultContentView+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336965B2A312F17009448F0 /* LivenessResultContentView+Result.swift */; }; + 733696662A312F17009448F0 /* StartSessionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336965C2A312F17009448F0 /* StartSessionViewModel.swift */; }; + 733696672A312F17009448F0 /* LivenessResultContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336965D2A312F17009448F0 /* LivenessResultContentView.swift */; }; + 733696702A312F2D009448F0 /* CreateSessionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696692A312F2D009448F0 /* CreateSessionResponse.swift */; }; + 733696712A312F2D009448F0 /* LivenessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336966A2A312F2D009448F0 /* LivenessResult.swift */; }; + 733696722A312F2D009448F0 /* Color+DynamicColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336966C2A312F2D009448F0 /* Color+DynamicColors.swift */; }; + 733696732A312F2D009448F0 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336966D2A312F2D009448F0 /* UIColor+Hex.swift */; }; + 733696742A312F2D009448F0 /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336966E2A312F2D009448F0 /* Color+Hex.swift */; }; + 733696752A312F2D009448F0 /* View+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7336966F2A312F2D009448F0 /* View+Background.swift */; }; + 733696782A312FC3009448F0 /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 733696772A312FC3009448F0 /* amplifyconfiguration.json */; }; + 733696892A31329A009448F0 /* LivenessIntegrationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733696882A31329A009448F0 /* LivenessIntegrationUITests.swift */; }; + 735A62472A313B8F00837642 /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 735A62462A313B8E00837642 /* UIConstants.swift */; }; + 73B8F4202A2D7A27004215B5 /* IntegrationTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B8F41F2A2D7A27004215B5 /* IntegrationTestApp.swift */; }; + 73B8F4242A2D7A28004215B5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 73B8F4232A2D7A28004215B5 /* Assets.xcassets */; }; + 73B8F4282A2D7A28004215B5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 73B8F4272A2D7A28004215B5 /* Preview Assets.xcassets */; }; + 73F5DACB2A312594004CD4FC /* AWSAPIPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 73F5DACA2A312594004CD4FC /* AWSAPIPlugin */; }; + 73F5DACD2A312594004CD4FC /* AWSCognitoAuthPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 73F5DACC2A312594004CD4FC /* AWSCognitoAuthPlugin */; }; + 73F5DACF2A312594004CD4FC /* AWSPluginsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 73F5DACE2A312594004CD4FC /* AWSPluginsCore */; }; + 73F5DAD12A312594004CD4FC /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = 73F5DAD02A312594004CD4FC /* Amplify */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 733696832A313278009448F0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 73B8F4142A2D7A27004215B5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 73B8F41B2A2D7A27004215B5; + remoteInfo = IntegrationTestApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 732CF84D2A31871D004D0BE3 /* MockLivenessCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLivenessCaptureSession.swift; sourceTree = ""; }; + 732CF84F2A3187E0004D0BE3 /* FaceLivenessDetectorView+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FaceLivenessDetectorView+Mock.swift"; sourceTree = ""; }; + 732CF8512A318F7D004D0BE3 /* mock.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = mock.mov; sourceTree = ""; }; + 733696542A312F17009448F0 /* ExampleLivenessViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleLivenessViewModel.swift; sourceTree = ""; }; + 733696552A312F17009448F0 /* StartSessionView+PresentationState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StartSessionView+PresentationState.swift"; sourceTree = ""; }; + 733696562A312F17009448F0 /* LivenessResultView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessResultView.swift; sourceTree = ""; }; + 733696572A312F17009448F0 /* ExampleLivenessView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleLivenessView.swift; sourceTree = ""; }; + 733696582A312F17009448F0 /* StartSessionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartSessionView.swift; sourceTree = ""; }; + 733696592A312F17009448F0 /* RootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; + 7336965A2A312F17009448F0 /* LivenessCheckErrorContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessCheckErrorContentView.swift; sourceTree = ""; }; + 7336965B2A312F17009448F0 /* LivenessResultContentView+Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LivenessResultContentView+Result.swift"; sourceTree = ""; }; + 7336965C2A312F17009448F0 /* StartSessionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartSessionViewModel.swift; sourceTree = ""; }; + 7336965D2A312F17009448F0 /* LivenessResultContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessResultContentView.swift; sourceTree = ""; }; + 733696692A312F2D009448F0 /* CreateSessionResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSessionResponse.swift; sourceTree = ""; }; + 7336966A2A312F2D009448F0 /* LivenessResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivenessResult.swift; sourceTree = ""; }; + 7336966C2A312F2D009448F0 /* Color+DynamicColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Color+DynamicColors.swift"; sourceTree = ""; }; + 7336966D2A312F2D009448F0 /* UIColor+Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; + 7336966E2A312F2D009448F0 /* Color+Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = ""; }; + 7336966F2A312F2D009448F0 /* View+Background.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Background.swift"; sourceTree = ""; }; + 733696772A312FC3009448F0 /* amplifyconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = amplifyconfiguration.json; sourceTree = SOURCE_ROOT; }; + 7336967D2A313278009448F0 /* IntegrationTestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 733696882A31329A009448F0 /* LivenessIntegrationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivenessIntegrationUITests.swift; sourceTree = ""; }; + 735A62462A313B8E00837642 /* UIConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; + 735A62492A317F6000837642 /* amplify-ui-swift-liveness */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "amplify-ui-swift-liveness"; path = ../..; sourceTree = ""; }; + 73B8F41C2A2D7A27004215B5 /* IntegrationTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 73B8F41F2A2D7A27004215B5 /* IntegrationTestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTestApp.swift; sourceTree = ""; }; + 73B8F4232A2D7A28004215B5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 73B8F4252A2D7A28004215B5 /* IntegrationTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IntegrationTestApp.entitlements; sourceTree = ""; }; + 73B8F4272A2D7A28004215B5 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7336967A2A313278009448F0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 73B8F4192A2D7A27004215B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 73F5DACF2A312594004CD4FC /* AWSPluginsCore in Frameworks */, + 73F5DAD12A312594004CD4FC /* Amplify in Frameworks */, + 732CF8492A3183F3004D0BE3 /* FaceLiveness in Frameworks */, + 73F5DACB2A312594004CD4FC /* AWSAPIPlugin in Frameworks */, + 73F5DACD2A312594004CD4FC /* AWSCognitoAuthPlugin in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 732CF84A2A31866D004D0BE3 /* Extension */ = { + isa = PBXGroup; + children = ( + 732CF84D2A31871D004D0BE3 /* MockLivenessCaptureSession.swift */, + 732CF84F2A3187E0004D0BE3 /* FaceLivenessDetectorView+Mock.swift */, + ); + path = Extension; + sourceTree = ""; + }; + 733696532A312F17009448F0 /* Views */ = { + isa = PBXGroup; + children = ( + 733696542A312F17009448F0 /* ExampleLivenessViewModel.swift */, + 733696552A312F17009448F0 /* StartSessionView+PresentationState.swift */, + 733696562A312F17009448F0 /* LivenessResultView.swift */, + 733696572A312F17009448F0 /* ExampleLivenessView.swift */, + 733696582A312F17009448F0 /* StartSessionView.swift */, + 733696592A312F17009448F0 /* RootView.swift */, + 7336965A2A312F17009448F0 /* LivenessCheckErrorContentView.swift */, + 7336965B2A312F17009448F0 /* LivenessResultContentView+Result.swift */, + 7336965C2A312F17009448F0 /* StartSessionViewModel.swift */, + 7336965D2A312F17009448F0 /* LivenessResultContentView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 733696682A312F2D009448F0 /* Model */ = { + isa = PBXGroup; + children = ( + 733696692A312F2D009448F0 /* CreateSessionResponse.swift */, + 7336966A2A312F2D009448F0 /* LivenessResult.swift */, + ); + path = Model; + sourceTree = ""; + }; + 7336966B2A312F2D009448F0 /* Utilities */ = { + isa = PBXGroup; + children = ( + 7336966C2A312F2D009448F0 /* Color+DynamicColors.swift */, + 7336966D2A312F2D009448F0 /* UIColor+Hex.swift */, + 7336966E2A312F2D009448F0 /* Color+Hex.swift */, + 7336966F2A312F2D009448F0 /* View+Background.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 733696762A312F7C009448F0 /* AmplifyConfig */ = { + isa = PBXGroup; + children = ( + 733696772A312FC3009448F0 /* amplifyconfiguration.json */, + ); + path = AmplifyConfig; + sourceTree = ""; + }; + 7336967E2A313278009448F0 /* IntegrationTestAppUITests */ = { + isa = PBXGroup; + children = ( + 735A62462A313B8E00837642 /* UIConstants.swift */, + 733696882A31329A009448F0 /* LivenessIntegrationUITests.swift */, + ); + path = IntegrationTestAppUITests; + sourceTree = ""; + }; + 735A62482A317F6000837642 /* Packages */ = { + isa = PBXGroup; + children = ( + 735A62492A317F6000837642 /* amplify-ui-swift-liveness */, + ); + name = Packages; + sourceTree = ""; + }; + 73B8F4132A2D7A27004215B5 = { + isa = PBXGroup; + children = ( + 735A62482A317F6000837642 /* Packages */, + 73B8F41E2A2D7A27004215B5 /* IntegrationTestApp */, + 7336967E2A313278009448F0 /* IntegrationTestAppUITests */, + 73B8F41D2A2D7A27004215B5 /* Products */, + 73F5DAC82A31252E004CD4FC /* Frameworks */, + ); + sourceTree = ""; + }; + 73B8F41D2A2D7A27004215B5 /* Products */ = { + isa = PBXGroup; + children = ( + 73B8F41C2A2D7A27004215B5 /* IntegrationTestApp.app */, + 7336967D2A313278009448F0 /* IntegrationTestAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 73B8F41E2A2D7A27004215B5 /* IntegrationTestApp */ = { + isa = PBXGroup; + children = ( + 732CF84A2A31866D004D0BE3 /* Extension */, + 733696762A312F7C009448F0 /* AmplifyConfig */, + 733696682A312F2D009448F0 /* Model */, + 7336966B2A312F2D009448F0 /* Utilities */, + 733696532A312F17009448F0 /* Views */, + 73B8F41F2A2D7A27004215B5 /* IntegrationTestApp.swift */, + 73B8F4232A2D7A28004215B5 /* Assets.xcassets */, + 732CF8512A318F7D004D0BE3 /* mock.mov */, + 73B8F4252A2D7A28004215B5 /* IntegrationTestApp.entitlements */, + 73B8F4262A2D7A28004215B5 /* Preview Content */, + ); + path = IntegrationTestApp; + sourceTree = ""; + }; + 73B8F4262A2D7A28004215B5 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 73B8F4272A2D7A28004215B5 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 73F5DAC82A31252E004CD4FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7336967C2A313278009448F0 /* IntegrationTestAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 733696852A313278009448F0 /* Build configuration list for PBXNativeTarget "IntegrationTestAppUITests" */; + buildPhases = ( + 733696792A313278009448F0 /* Sources */, + 7336967A2A313278009448F0 /* Frameworks */, + 7336967B2A313278009448F0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 733696842A313278009448F0 /* PBXTargetDependency */, + ); + name = IntegrationTestAppUITests; + productName = IntegrationTestAppUITests; + productReference = 7336967D2A313278009448F0 /* IntegrationTestAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 73B8F41B2A2D7A27004215B5 /* IntegrationTestApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 73B8F42B2A2D7A28004215B5 /* Build configuration list for PBXNativeTarget "IntegrationTestApp" */; + buildPhases = ( + 73B8F4182A2D7A27004215B5 /* Sources */, + 73B8F4192A2D7A27004215B5 /* Frameworks */, + 73B8F41A2A2D7A27004215B5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IntegrationTestApp; + packageProductDependencies = ( + 73F5DACA2A312594004CD4FC /* AWSAPIPlugin */, + 73F5DACC2A312594004CD4FC /* AWSCognitoAuthPlugin */, + 73F5DACE2A312594004CD4FC /* AWSPluginsCore */, + 73F5DAD02A312594004CD4FC /* Amplify */, + 732CF8482A3183F3004D0BE3 /* FaceLiveness */, + ); + productName = IntegrationTestApp; + productReference = 73B8F41C2A2D7A27004215B5 /* IntegrationTestApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 73B8F4142A2D7A27004215B5 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 7336967C2A313278009448F0 = { + CreatedOnToolsVersion = 14.3; + TestTargetID = 73B8F41B2A2D7A27004215B5; + }; + 73B8F41B2A2D7A27004215B5 = { + CreatedOnToolsVersion = 14.3; + }; + }; + }; + buildConfigurationList = 73B8F4172A2D7A27004215B5 /* Build configuration list for PBXProject "IntegrationTestApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 73B8F4132A2D7A27004215B5; + packageReferences = ( + 73F5DAC92A312594004CD4FC /* XCRemoteSwiftPackageReference "amplify-swift" */, + ); + productRefGroup = 73B8F41D2A2D7A27004215B5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 73B8F41B2A2D7A27004215B5 /* IntegrationTestApp */, + 7336967C2A313278009448F0 /* IntegrationTestAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7336967B2A313278009448F0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 73B8F41A2A2D7A27004215B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 732CF8522A318F7D004D0BE3 /* mock.mov in Resources */, + 73B8F4282A2D7A28004215B5 /* Preview Assets.xcassets in Resources */, + 733696782A312FC3009448F0 /* amplifyconfiguration.json in Resources */, + 73B8F4242A2D7A28004215B5 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 733696792A313278009448F0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 733696892A31329A009448F0 /* LivenessIntegrationUITests.swift in Sources */, + 735A62472A313B8F00837642 /* UIConstants.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 73B8F4182A2D7A27004215B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 733696742A312F2D009448F0 /* Color+Hex.swift in Sources */, + 733696642A312F17009448F0 /* LivenessCheckErrorContentView.swift in Sources */, + 733696632A312F17009448F0 /* RootView.swift in Sources */, + 7336965F2A312F17009448F0 /* StartSessionView+PresentationState.swift in Sources */, + 733696622A312F17009448F0 /* StartSessionView.swift in Sources */, + 733696662A312F17009448F0 /* StartSessionViewModel.swift in Sources */, + 732CF84E2A31871D004D0BE3 /* MockLivenessCaptureSession.swift in Sources */, + 733696722A312F2D009448F0 /* Color+DynamicColors.swift in Sources */, + 733696652A312F17009448F0 /* LivenessResultContentView+Result.swift in Sources */, + 733696712A312F2D009448F0 /* LivenessResult.swift in Sources */, + 733696732A312F2D009448F0 /* UIColor+Hex.swift in Sources */, + 73B8F4202A2D7A27004215B5 /* IntegrationTestApp.swift in Sources */, + 733696752A312F2D009448F0 /* View+Background.swift in Sources */, + 733696672A312F17009448F0 /* LivenessResultContentView.swift in Sources */, + 733696702A312F2D009448F0 /* CreateSessionResponse.swift in Sources */, + 733696602A312F17009448F0 /* LivenessResultView.swift in Sources */, + 733696612A312F17009448F0 /* ExampleLivenessView.swift in Sources */, + 732CF8502A3187E0004D0BE3 /* FaceLivenessDetectorView+Mock.swift in Sources */, + 7336965E2A312F17009448F0 /* ExampleLivenessViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 733696842A313278009448F0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 73B8F41B2A2D7A27004215B5 /* IntegrationTestApp */; + targetProxy = 733696832A313278009448F0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 733696862A313278009448F0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D8BB58X7QJ; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.liveness.IntegrationTestAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = IntegrationTestApp; + }; + name = Debug; + }; + 733696872A313278009448F0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D8BB58X7QJ; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.liveness.IntegrationTestAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = IntegrationTestApp; + }; + name = Release; + }; + 73B8F4292A2D7A28004215B5 /* 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++20"; + 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 = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 73B8F42A2A2D7A28004215B5 /* 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++20"; + 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 = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 73B8F42C2A2D7A28004215B5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = IntegrationTestApp/IntegrationTestApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"IntegrationTestApp/Preview Content\""; + DEVELOPMENT_TEAM = D8BB58X7QJ; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Liveness requires access to device camera."; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = ""; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.amplify.liveness.IntegrationTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 73B8F42D2A2D7A28004215B5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = IntegrationTestApp/IntegrationTestApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"IntegrationTestApp/Preview Content\""; + DEVELOPMENT_TEAM = D8BB58X7QJ; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_TESTABILITY = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Liveness requires access to device camera."; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = ""; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.amplify.liveness.IntegrationTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 733696852A313278009448F0 /* Build configuration list for PBXNativeTarget "IntegrationTestAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 733696862A313278009448F0 /* Debug */, + 733696872A313278009448F0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 73B8F4172A2D7A27004215B5 /* Build configuration list for PBXProject "IntegrationTestApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 73B8F4292A2D7A28004215B5 /* Debug */, + 73B8F42A2A2D7A28004215B5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 73B8F42B2A2D7A28004215B5 /* Build configuration list for PBXNativeTarget "IntegrationTestApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 73B8F42C2A2D7A28004215B5 /* Debug */, + 73B8F42D2A2D7A28004215B5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 73F5DAC92A312594004CD4FC /* XCRemoteSwiftPackageReference "amplify-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/aws-amplify/amplify-swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 732CF8482A3183F3004D0BE3 /* FaceLiveness */ = { + isa = XCSwiftPackageProductDependency; + productName = FaceLiveness; + }; + 73F5DACA2A312594004CD4FC /* AWSAPIPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 73F5DAC92A312594004CD4FC /* XCRemoteSwiftPackageReference "amplify-swift" */; + productName = AWSAPIPlugin; + }; + 73F5DACC2A312594004CD4FC /* AWSCognitoAuthPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 73F5DAC92A312594004CD4FC /* XCRemoteSwiftPackageReference "amplify-swift" */; + productName = AWSCognitoAuthPlugin; + }; + 73F5DACE2A312594004CD4FC /* AWSPluginsCore */ = { + isa = XCSwiftPackageProductDependency; + package = 73F5DAC92A312594004CD4FC /* XCRemoteSwiftPackageReference "amplify-swift" */; + productName = AWSPluginsCore; + }; + 73F5DAD02A312594004CD4FC /* Amplify */ = { + isa = XCSwiftPackageProductDependency; + package = 73F5DAC92A312594004CD4FC /* XCRemoteSwiftPackageReference "amplify-swift" */; + productName = Amplify; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 73B8F4142A2D7A27004215B5 /* Project object */; +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/IntegrationTestApp/IntegrationTestApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..a12ae29b --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "amplify-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift", + "state" : { + "revision" : "a01dfe5c0e5b38be13339a2005ce22874f5d4749", + "version" : "2.11.6" + } + }, + { + "identity" : "amplify-swift-utils-notifications", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", + "state" : { + "revision" : "d4fd3c17e8d40efc821f448d3d6cff75b8f3b0dd", + "version" : "1.0.0" + } + }, + { + "identity" : "aws-appsync-realtime-client-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/aws-appsync-realtime-client-ios.git", + "state" : { + "revision" : "b036e83716789c13a3480eeb292b70caa54114f2", + "version" : "3.1.0" + } + }, + { + "identity" : "aws-crt-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-crt-swift", + "state" : { + "revision" : "6feec6c3787877807aa9a00fad09591b96752376", + "version" : "0.6.1" + } + }, + { + "identity" : "aws-sdk-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-sdk-swift.git", + "state" : { + "revision" : "24bae88a2391fe75da8a940a544d1ef6441f5321", + "version" : "0.13.0" + } + }, + { + "identity" : "smithy-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/smithy-swift", + "state" : { + "revision" : "7b28da158d92cd06a3549140d43b8fbcf64a94a6", + "version" : "0.15.0" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "5f5ad81ac0d0a0f3e56e39e646e8423c617df523", + "version" : "0.13.2" + } + }, + { + "identity" : "starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/daltoniam/Starscream", + "state" : { + "revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21", + "version" : "4.0.4" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", + "version" : "1.5.2" + } + }, + { + "identity" : "xmlcoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MaxDesiatov/XMLCoder.git", + "state" : { + "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", + "version" : "0.17.1" + } + } + ], + "version" : 2 +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Extension/FaceLivenessDetectorView+Mock.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/FaceLivenessDetectorView+Mock.swift new file mode 100644 index 00000000..cc8bcbe7 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/FaceLivenessDetectorView+Mock.swift @@ -0,0 +1,45 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import protocol AWSPluginsCore.AWSCredentialsProvider +import AVFoundation +@testable import FaceLiveness + +extension FaceLivenessDetectorView { + static func getMockFaceLivenessDetectorView ( + sessionID: String, + credentialsProvider: AWSCredentialsProvider? = nil, + region: String, + isPresented: Binding, + onCompletion: @escaping (Result) -> Void + ) -> FaceLivenessDetectorView { + + let avCaptureDevice = AVCaptureDevice.DiscoverySession( + deviceTypes: [.builtInWideAngleCamera], + mediaType: .video, + position: .front + ).devices.first + let captureDevice = LivenessCaptureDevice(avCaptureDevice: avCaptureDevice) + + let faceDetector = try! FaceDetectorShortRange.Model() + + let videoChunker = VideoChunker( + assetWriter: LivenessAVAssetWriter(), + assetWriterDelegate: VideoChunker.AssetWriterDelegate(), + assetWriterInput: LivenessAVAssetWriterInput() + ) + + let outputDelegate = OutputSampleBufferCapturer(faceDetector: faceDetector, videoChunker: videoChunker + ) + let inputUrl = Bundle.main.url(forResource: "mock", withExtension: "mov")! + let captureSession = MockLivenessCaptureSession(captureDevice: captureDevice, outputDelegate: outputDelegate, inputFile: inputUrl) + let detectorView = FaceLivenessDetectorView(sessionID: sessionID, region: region, isPresented: isPresented, onCompletion: onCompletion, captureSession: captureSession) + + return detectorView + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift new file mode 100644 index 00000000..b7e0d0c2 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift @@ -0,0 +1,134 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import AVFoundation +@testable import FaceLiveness + +final class MockLivenessCaptureSession: LivenessCaptureSession { + private var videoRenderView: VideoRenderView? + private var displayLink: CADisplayLink? + private var playerItemOutput: AVPlayerItemVideoOutput? + private let videoFileReadingQueue = DispatchQueue(label: "com.amazonaws.faceliveness.cameracapturequeue") + private var videoFileFrameDuration = CMTime.invalid + private let inputFile: URL + + init(captureDevice: LivenessCaptureDevice, + outputDelegate: OutputSampleBufferCapturer, + inputFile: URL + ) { + self.inputFile = inputFile + super.init(captureDevice: captureDevice, outputDelegate: outputDelegate) + } + + override func stopRunning() { + videoRenderView?.player?.pause() + displayLink?.invalidate() + } + + override func startSession(frame: CGRect) throws -> CALayer { + videoRenderView = VideoRenderView(frame: frame) + let asset = AVAsset(url: inputFile) + // Setup display link + let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:))) + displayLink.preferredFramesPerSecond = 0 + displayLink.isPaused = true + displayLink.add(to: RunLoop.current, forMode: .default) + captureSession = AVCaptureSession() + guard let track = asset.tracks(withMediaType: .video).first else { + throw LivenessCaptureSessionError.captureSessionInputUnavailable + } + + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + let settings = [ + String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ] + let output = AVPlayerItemVideoOutput(pixelBufferAttributes: settings) + playerItem.add(output) + player.actionAtItemEnd = .pause + player.play() + + self.displayLink = displayLink + self.playerItemOutput = output + self.videoRenderView?.player = player + + videoFileFrameDuration = track.minFrameDuration + displayLink.isPaused = false + guard let previewLayer = videoRenderView?.layer else { + throw LivenessCaptureSessionError.captureSessionOutputUnavailable + } + return previewLayer + } + + @objc + private func handleDisplayLink(_ displayLink: CADisplayLink) { + guard let output = playerItemOutput else { + return + } + + videoFileReadingQueue.async { + let nextTimeStamp = displayLink.timestamp + displayLink.duration + let itemTime = output.itemTime(forHostTime: nextTimeStamp) + guard output.hasNewPixelBuffer(forItemTime: itemTime) else { + return + } + guard let pixelBuffer = output.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil) else { + return + } + + var sampleBuffer: CMSampleBuffer? + var formatDescription: CMVideoFormatDescription? + CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &formatDescription) + let duration = self.videoFileFrameDuration + var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: itemTime, decodeTimeStamp: itemTime) + CMSampleBufferCreateForImageBuffer(allocator: nil, + imageBuffer: pixelBuffer, + dataReady: true, + makeDataReadyCallback: nil, + refcon: nil, + formatDescription: formatDescription!, + sampleTiming: &timingInfo, + sampleBufferOut: &sampleBuffer) + if let sampleBuffer = sampleBuffer { + self.outputDelegate.videoChunker.consume(sampleBuffer) + guard let imageBuffer = sampleBuffer.rotateRightUpMirrored() + else { return } + self.outputDelegate.faceDetector.detectFaces(from: imageBuffer) + } + } + } +} + +class VideoRenderView: UIView { + private var renderLayer: AVPlayerLayer! + + var player: AVPlayer? { + get { + return renderLayer.player + } + set { + renderLayer.player = newValue + } + } + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } + + override init(frame: CGRect) { + super.init(frame: frame) + renderLayer = layer as? AVPlayerLayer + renderLayer.videoGravity = .resizeAspect + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Info.plist b/Tests/IntegrationTestApp/IntegrationTestApp/Info.plist new file mode 100644 index 00000000..b72ec3ba --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Info.plist @@ -0,0 +1,15 @@ + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + myapp + + + + + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.entitlements b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.entitlements new file mode 100644 index 00000000..f6ea6035 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.camera + + com.apple.security.files.user-selected.read-only + + + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.swift b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.swift new file mode 100644 index 00000000..4e506b4e --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/IntegrationTestApp.swift @@ -0,0 +1,71 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness +import Amplify +import AWSCognitoAuthPlugin +import AWSAPIPlugin + +@main +struct IntegrationTestApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + func increaseBrightness() { + UIScreen.main.brightness = 1.0 + } + + var body: some Scene { + WindowGroup { + RootView() + } + } + + init() { + do { + let auth = AWSCognitoAuthPlugin() + let api = AWSAPIPlugin() + try Amplify.add(plugin: auth) + try Amplify.add(plugin: api) + try Amplify.configure() + } catch { + print("Error configuring Amplify", error) + } + } +} + +class AppDelegate: NSObject, UIApplicationDelegate { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + if connectingSceneSession.role == .windowApplication { + configuration.delegateClass = SceneDelegate.self + } + return configuration + } +} + +class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + if #available(iOS 15.0, *) { + self.window = (scene as? UIWindowScene)?.keyWindow + } else { + self.window = (scene as? UIWindowScene)?.windows + .first(where: \.isKeyWindow) + } + } +} + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Model/CreateSessionResponse.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Model/CreateSessionResponse.swift new file mode 100644 index 00000000..f2f44e37 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Model/CreateSessionResponse.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct CreateSessionResponse: Codable { + let sessionId: String +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Model/LivenessResult.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Model/LivenessResult.swift new file mode 100644 index 00000000..226bc30f --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Model/LivenessResult.swift @@ -0,0 +1,25 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct LivenessResult: Codable { + let auditImageBytes: String? + let confidenceScore: Double + let isLive: Bool +} + +extension LivenessResult: CustomDebugStringConvertible { + var debugDescription: String { + """ + LivenessResult + - confidenceScore: \(confidenceScore) + - isLive: \(isLive) + - auditImageBytes: \(auditImageBytes == nil ? "nil" : "") + """ + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json b/Tests/IntegrationTestApp/IntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+DynamicColors.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+DynamicColors.swift new file mode 100644 index 00000000..71afbf63 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+DynamicColors.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension Color { + static func dynamicColors(light: UIColor, dark: UIColor) -> Color { + Color( + UIColor( + dynamicProvider: { traitCollection in + switch traitCollection.userInterfaceStyle { + case .dark: return dark + default: return light + } + } + ) + ) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+Hex.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+Hex.swift new file mode 100644 index 00000000..0136a714 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/Color+Hex.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import UIKit + +extension Color { + static func hex(_ hex: String) -> Color { + Color(UIColor.hex(hex)) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/UIColor+Hex.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/UIColor+Hex.swift new file mode 100644 index 00000000..678773d1 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/UIColor+Hex.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit + +extension UIColor { + static func hex(_ hex: String) -> UIColor { + assert(hex.hasPrefix("#")) + + let hex = String(hex.dropFirst()) + assert(hex.count == 6) + + let scanner = Scanner(string: hex) + var hexNumber: UInt64 = 0 + + precondition(scanner.scanHexInt64(&hexNumber)) + let r, g, b, a: CGFloat + r = CGFloat((hexNumber & 0xFF0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x00FF00) >> 8) / 255 + b = CGFloat((hexNumber & 0x0000FF)) / 255 + a = 1.0 + + return UIColor(red: r, green: g, blue: b, alpha: a) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/View+Background.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/View+Background.swift new file mode 100644 index 00000000..4ffb074c --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Utilities/View+Background.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension View { + @ViewBuilder func _background( + alignment: Alignment = .center, + @ViewBuilder _ content: () -> Content + ) -> some View { + if #available(iOS 15.0, *) { + background(alignment: alignment, content: content) + } else { + background(content(), alignment: alignment) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessView.swift new file mode 100644 index 00000000..7e57e8cf --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessView.swift @@ -0,0 +1,81 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness + +struct ExampleLivenessView: View { + @Binding var isPresented: Bool + @ObservedObject var viewModel: ExampleLivenessViewModel + + init(sessionID: String, isPresented: Binding) { + self.viewModel = .init(sessionID: sessionID) + self._isPresented = isPresented + } + + var body: some View { + switch viewModel.presentationState { + case .liveness: + FaceLivenessDetectorView.getMockFaceLivenessDetectorView( + sessionID: viewModel.sessionID, + region: "us-east-1", + isPresented: Binding( + get: { viewModel.presentationState == .liveness }, + set: { _ in } + ), + onCompletion: { result in + switch result { + case .success: + withAnimation { viewModel.presentationState = .result } + case .failure(.sessionNotFound), .failure(.cameraPermissionDenied), .failure(.accessDenied), .failure(.validation): + viewModel.presentationState = .liveness + isPresented = false + case .failure(.userCancelled): + viewModel.presentationState = .liveness + isPresented = false + case .failure(.sessionTimedOut): + viewModel.presentationState = .error(.sessionTimedOut) + case .failure(.socketClosed): + viewModel.presentationState = .error(.socketClosed) + case .failure(.countdownNoFace), .failure(.countdownFaceTooClose), .failure(.countdownMultipleFaces): + viewModel.presentationState = .error(.countdownFaceTooClose) + default: + viewModel.presentationState = .liveness + } + } + ) + .id(isPresented) + case .result: + LivenessResultView( + sessionID: viewModel.sessionID, + onTryAgain: { isPresented = false }, + content: { + LivenessResultContentView(fetchResults: viewModel.fetchLivenessResult) + } + ) + .animation(.default, value: viewModel.presentationState) + case .error(let detectionError): + LivenessResultView( + sessionID: viewModel.sessionID, + onTryAgain: { isPresented = false }, + content: { + switch detectionError { + case .socketClosed: + LivenessCheckErrorContentView.sessionTimeOut + case .sessionTimedOut: + LivenessCheckErrorContentView.faceMatchTimeOut + case .countdownNoFace, .countdownFaceTooClose, .countdownMultipleFaces: + LivenessCheckErrorContentView.failedDuringCountdown + default: + LivenessCheckErrorContentView.unexpected + } + } + ) + .animation(.default, value: viewModel.presentationState) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessViewModel.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessViewModel.swift new file mode 100644 index 00000000..a04571bc --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/ExampleLivenessViewModel.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import FaceLiveness +import Amplify + +class ExampleLivenessViewModel: ObservableObject { + @Published var presentationState = PresentationState.liveness + let sessionID: String + + init(sessionID: String) { + self.sessionID = sessionID + } + + func fetchLivenessResult() async throws -> LivenessResultContentView.Result { + let request = RESTRequest( + apiName: "liveness", + path: "/liveness/\(sessionID)" + ) + + let data = try await Amplify.API.get(request: request) + let result = try JSONDecoder().decode(LivenessResult.self, from: data) + let score = LivenessResultContentView.Result(livenessResult: result) + return score + } + + enum PresentationState: Equatable { + case liveness, result, error(FaceLivenessDetectionError) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessCheckErrorContentView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessCheckErrorContentView.swift new file mode 100644 index 00000000..04a03f9d --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessCheckErrorContentView.swift @@ -0,0 +1,59 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessCheckErrorContentView: View { + let name: String + let description: String + + var body: some View { + VStack(alignment: .leading) { + HStack { + Image(systemName: "exclamationmark.circle.fill") + .foregroundColor(.hex("#950404")) + Text("Error: \(name)") + .fontWeight(.semibold) + } + .padding(.bottom, 4) + Text(description) + } + } +} + +extension LivenessCheckErrorContentView { + static let mock = LivenessCheckErrorContentView( + name: "Time out", + description: "Face didn't fit inside oval in time limit. Try again and completely fill the oval with face within 7 seconds." + ) + + 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 within 7 seconds." + ) + + static let sessionTimeOut = LivenessCheckErrorContentView( + name: "Connection interrupted", + description: "Your connection was unexpectedly closed." + ) + + static let failedDuringCountdown = LivenessCheckErrorContentView( + name: "Check failed during countdown", + description: "Avoid moving closer during countdown and ensure only one face is in front of camera." + ) +} + +struct LivenessCheckErrorContentView_Previews: PreviewProvider { + static var previews: some View { + LivenessCheckErrorContentView.mock + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift new file mode 100644 index 00000000..749e88a7 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift @@ -0,0 +1,78 @@ +// +// 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? + + init(livenessResult: LivenessResult) { + guard livenessResult.confidenceScore > 0 else { + text = "" + value = "" + valueTextColor = .clear + valueBackgroundColor = .clear + auditImage = nil + return + } + + let truncated = String(format: "%.4f", livenessResult.confidenceScore) + value = truncated + if livenessResult.isLive { + valueTextColor = .hex("#365E3D") + valueBackgroundColor = .hex("#D6F5DB") + text = "Check successful" + } else { + valueTextColor = .hex("#660000") + valueBackgroundColor = .hex("#F5BCBC") + text = "Check unsuccessful" + } + auditImage = livenessResult.auditImageBytes.flatMap{ + Data(base64Encoded: $0) + } + } + } + + struct Score { + let resultText: String + let value: String + let valueTextColor: Color + let valueBackgroundColor: Color + + init( + value: Double, + colorRule: (Double) -> (Color, Color, String) = colorRule + ) { + let truncated = String(format: "%.4f", value) + let (textColor, backgroundColor, resultText) = colorRule(value) + self.resultText = resultText + self.value = truncated + self.valueTextColor = textColor + self.valueBackgroundColor = backgroundColor + } + } +} + +fileprivate func colorRule(v: Double) -> (Color, Color, String) { + let textColor, backgroundColor: Color + let resultText: String + if v >= 70 { + textColor = .hex("#365E3D") + backgroundColor = .hex("#D6F5DB") + resultText = "Check successful" + } else { + textColor = .hex("#660000") + backgroundColor = .hex("#F5BCBC") + resultText = "Check unsuccessful" + } + return (textColor, backgroundColor, resultText) +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift new file mode 100644 index 00000000..b1787015 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift @@ -0,0 +1,78 @@ +// +// 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) + + } + .padding(.bottom, 12) + + HStack { + Text("Liveness confidence score:") + Text(result.value) + .foregroundColor(result.valueTextColor) + .padding(6) + .background(result.valueBackgroundColor) + .cornerRadius(8) + } + + if let image = result.auditImage { + Image(uiImage: .init(data: image) ?? UIImage()) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 300) + .background(Color.secondary.opacity(0.1)) + } else { + Image(systemName: "person.fill") + .font(.system(size: 128)) + .frame(maxWidth: .infinity, idealHeight: 268) + .background(Color.secondary.opacity(0.1)) + } + } + .padding(.bottom, 16) + .onAppear { + Task { + do { + self.result = try await fetchResults() + } catch { + print("Error fetching result", error) + } + } + } + } +} + + +extension LivenessResultContentView { + static let mock = LivenessResultContentView( + fetchResults: { + .init( + livenessResult: .init( + auditImageBytes: nil, + confidenceScore: 99.8329, + isLive: true + ) + ) + } + ) +} + +struct LivenessResultContentView_Previews: PreviewProvider { + static var previews: some View { + LivenessResultContentView.mock + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultView.swift new file mode 100644 index 00000000..7f75844d --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultView.swift @@ -0,0 +1,149 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct LivenessResultView: View { + let title: String + let sessionID: String + let content: Content + let onTryAgain: () -> Void + @State var displayingCopiedNotification = false + + init( + title: String = "Liveness Check", + sessionID: String, + onTryAgain: @escaping () -> Void, + @ViewBuilder content: () -> Content + ) { + self.title = title + self.sessionID = sessionID + self.content = content() + self.onTryAgain = onTryAgain + } + + var body: some View { + VStack { + ScrollView { + VStack(alignment: .leading) { + Text(title) + .font(.system(size: 34, weight: .semibold)) + .padding(.bottom, 8) + + sessionIDBox + .padding(.bottom, 16) + + content + } + .padding() + } + + if displayingCopiedNotification { + Text("Copied Session ID") + .foregroundColor(.dynamicColors(light: .white, dark: .black)) + .padding(8) + .background(Color.dynamicColors(light: .darkGray, dark: .lightGray)) + .cornerRadius(6) + .onAppear { + Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + withAnimation { + displayingCopiedNotification = false + } + } + } + } + tryAgainButton + } + } + + private func copySessionID() { + withAnimation { + displayingCopiedNotification = true + } + UIPasteboard.general.string = sessionID + } + + private var sessionIDBox: some View { + HStack { + VStack(alignment: .leading) { + Text("Session ID:") + .fontWeight(.semibold) + Text(sessionID) + } + Spacer() + Button( + action: copySessionID, + label: { + Image(systemName: "square.on.square") + .foregroundColor(.primary) + .frame(width: 20, height: 20) + } + ) + .frame(width: 44, height: 44) + } + .padding() + .background( + Rectangle() + .foregroundColor( + .dynamicColors( + light: .hex("#ECECEC"), + dark: .darkGray + ) + ) + .cornerRadius(6) + ) + } + + private var tryAgainButton: some View { + Button( + action: onTryAgain, + label: { + Text("Try Again") + .foregroundColor( + .dynamicColors(light: .white, dark: .black) + ) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { + Color.dynamicColors(light: .hex("#047D95"), dark: .hex("#7dd6e8")) + } + .cornerRadius(14) + .padding(.leading) + .padding(.trailing) + .padding(.bottom, 16) + } +} + +extension LivenessResultView where Content == LivenessResultContentView { + static var sessionID: String { + String(UUID().uuidString.flatMap { $0.lowercased() }) + } + + static var mock: Self { + .init( + sessionID: sessionID, + onTryAgain: {}, + content: { LivenessResultContentView.mock } + ) + } +} + +struct LivenessCheckView_Previews: PreviewProvider { + static let sessionID = String(UUID().uuidString.flatMap { $0.lowercased() }) + static var previews: some View { + LivenessResultView( + sessionID: sessionID, + onTryAgain: {}, + content: { + LivenessCheckErrorContentView.mock + } + ) + } +} + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/RootView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/RootView.swift new file mode 100644 index 00000000..7600f1b4 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/RootView.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct RootView: View { + @EnvironmentObject var sceneDelegate: SceneDelegate + @State var sessionID = "" + @State var isPresentingContainerView = false + + var body: some View { + if isPresentingContainerView { + ExampleLivenessView( + sessionID: sessionID, + isPresented: $isPresentingContainerView + ) + } else { + StartSessionView( + sessionID: $sessionID, + isPresentingContainerView: $isPresentingContainerView + ) + .background(Color.dynamicColors(light: .white, dark: .secondarySystemBackground)) + .edgesIgnoringSafeArea(.all) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView+PresentationState.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView+PresentationState.swift new file mode 100644 index 00000000..035a2f77 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView+PresentationState.swift @@ -0,0 +1,55 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +extension StartSessionView { + struct PresentationState: Equatable { + let buttonText: String + let buttonBackgroundColor: Color + let buttonAction: () -> Void + let buttonEnabled: Bool + + static let loading = PresentationState( + buttonText: "...", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: {}, + buttonEnabled: false + ) + + static func signedIn(action: @escaping () -> Void) -> PresentationState { + PresentationState( + buttonText: "Sign Out", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: action, + buttonEnabled: true + ) + } + + static func signedOut(action: @escaping () -> Void) -> PresentationState { + PresentationState( + buttonText: "Sign In", + buttonBackgroundColor: .dynamicColors( + light: .darkGray, + dark: .lightGray + ), + buttonAction: action, + buttonEnabled: true + ) + } + + static func == (lhs: StartSessionView.PresentationState, rhs: StartSessionView.PresentationState) -> Bool { + lhs.buttonText == rhs.buttonText + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView.swift new file mode 100644 index 00000000..173cc9c5 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionView.swift @@ -0,0 +1,72 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import Amplify + +struct StartSessionView: View { + @EnvironmentObject var sceneDelegate: SceneDelegate + @ObservedObject var viewModel = StartSessionViewModel() + @Binding var sessionID: String + @Binding var isPresentingContainerView: Bool + + var body: some View { + VStack { + Spacer() + button( + text: viewModel.presentationState.buttonText, + backgroundColor: viewModel.presentationState.buttonBackgroundColor, + action: viewModel.presentationState.buttonAction, + enabled: viewModel.presentationState.buttonEnabled + ) + + button( + text: "Create Liveness Session", + backgroundColor: .dynamicColors( + light: .hex("#047D95"), + dark: .hex("#7dd6e8") + ), + action: { + viewModel.createSession { + sessionID = $0 + isPresentingContainerView = true + } + }, + enabled: true + ) + + Spacer() + } + .onAppear { viewModel.setup() } + } + + func button( + text: String, + backgroundColor: Color, + action: @escaping () -> Void, + enabled: Bool + ) -> some View { + Button( + action: action, + label: { + Text(text) + .foregroundColor(.dynamicColors(light: .white, dark: .black)) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { + backgroundColor.opacity(enabled ? 1.0 : 0.6) + } + .cornerRadius(14) + .padding(.leading) + .padding(.trailing) + .padding(.bottom, 16) + .disabled(!enabled) + } +} + diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionViewModel.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionViewModel.swift new file mode 100644 index 00000000..a8250fd1 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/StartSessionViewModel.swift @@ -0,0 +1,69 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI +import Amplify + +class StartSessionViewModel: ObservableObject { + @Published var presentationState: StartSessionView.PresentationState = .loading + var window: UIWindow? + + var isSignedIn: Bool { + presentationState == .signedIn {} + } + + func setup() { + Task { @MainActor in + presentationState = .loading + let session = try await Amplify.Auth.fetchAuthSession() + presentationState = session.isSignedIn + ? .signedIn(action: signOut) + : .signedOut(action: signIn) + } + } + + func createSession(_ completion: @escaping (String) -> Void) { + Task { @MainActor in + presentationState = .loading + let request = RESTRequest( + apiName: "liveness", + path: "/liveness/create" + ) + + do { + let data = try await Amplify.API.post(request: request) + let response = try JSONDecoder().decode( + CreateSessionResponse.self, + from: data + ) + completion(response.sessionId) + } catch { + print("Error creating session", error) + } + } + } + + func signIn() { + Task { @MainActor in + presentationState = .loading + let signInResult = try await Amplify.Auth.signInWithWebUI( + presentationAnchor: window + ) + if signInResult.isSignedIn { + presentationState = .signedIn(action: signOut) + } + } + } + + func signOut() { + Task { @MainActor in + presentationState = .loading + _ = await Amplify.Auth.signOut() + presentationState = .signedOut(action: signIn) + } + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestAppUITests/LivenessIntegrationUITests.swift b/Tests/IntegrationTestApp/IntegrationTestAppUITests/LivenessIntegrationUITests.swift new file mode 100644 index 00000000..237dafec --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestAppUITests/LivenessIntegrationUITests.swift @@ -0,0 +1,57 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +class CreateLivenessSessionUITests: XCTestCase { + + var app: XCUIApplication? + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app?.launch() + } + + func testBeginCheckUI() throws { + XCTAssertEqual(app!.label, UIConstants.appName) + XCTAssert(app!.buttons[UIConstants.primaryButton].exists) + XCTAssert(app!.buttons[UIConstants.primaryButton].isEnabled) + app!.buttons[UIConstants.primaryButton].tap() + Thread.sleep(forTimeInterval: 2) + XCTAssert(app!.buttons[UIConstants.BeginCheck.primaryButton].exists) + XCTAssertFalse(app!.buttons[UIConstants.primaryButton].exists) + let scrollViewsQuery = app!.scrollViews + let elementsQuery = scrollViewsQuery.otherElements + XCTAssertEqual(elementsQuery.staticTexts.element(boundBy: 1).label, UIConstants.BeginCheck.description) + XCTAssert(elementsQuery.buttons[UIConstants.BeginCheck.warning].exists) + XCTAssert(elementsQuery.staticTexts[UIConstants.BeginCheck.instruction].exists) + } + + func testStartLivenessIntegration() throws { + XCTAssertEqual(app!.label, UIConstants.appName) + XCTAssert(app!.buttons[UIConstants.primaryButton].exists) + XCTAssert(app!.buttons[UIConstants.primaryButton].isEnabled) + app?.buttons[UIConstants.primaryButton].tap() + Thread.sleep(forTimeInterval: 2) + XCTAssert(app!.buttons[UIConstants.BeginCheck.primaryButton].exists) + XCTAssertFalse(app!.buttons[UIConstants.primaryButton].exists) + app!.buttons[UIConstants.BeginCheck.primaryButton].tap() + Thread.sleep(forTimeInterval: 2) + XCTAssert(app!.staticTexts[UIConstants.LivenessCheck.countdownInstruction].exists) + XCTAssert(app!.buttons[UIConstants.LivenessCheck.closeButton].exists) + Thread.sleep(forTimeInterval: 3) + XCTAssert(app!.staticTexts[UIConstants.LivenessCheck.moveInstruction].exists) + Thread.sleep(forTimeInterval: 3) + XCTAssert(app!.staticTexts[UIConstants.LivenessCheck.holdInstruction].exists) + Thread.sleep(forTimeInterval: 8) + XCTAssert(app!.buttons[UIConstants.LivenessResult.primaryButton].exists) + XCTAssert(app!.staticTexts[UIConstants.LivenessResult.result].exists) + XCTAssert(app!.staticTexts[UIConstants.LivenessResult.confidence].exists) + } +} diff --git a/Tests/IntegrationTestApp/IntegrationTestAppUITests/UIConstants.swift b/Tests/IntegrationTestApp/IntegrationTestAppUITests/UIConstants.swift new file mode 100644 index 00000000..12b8e392 --- /dev/null +++ b/Tests/IntegrationTestApp/IntegrationTestAppUITests/UIConstants.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +struct UIConstants { + static let appName = "IntegrationTestApp" + static let primaryButton = "Create Liveness Session" + + struct BeginCheck { + static let primaryButton = "Begin Check" + static let description = "You will go through a face verification process to prove that you are a real person. Your screen's brightness will temporarily be set to 100% for highest accuracy." + static let warning = "Photosensitivity Warning, This check displays colored lights. Use caution if you are photosensitive." + static let instruction = "Follow the instructions to complete the check:" + } + + struct LivenessCheck { + static let countdownInstruction = "Hold face position during countdown." + static let moveInstruction = "Move closer" + static let holdInstruction = "Hold still" + static let closeButton = "Close" + } + + struct LivenessResult { + static let result = "Result:" + static let confidence = "Liveness confidence score:" + static let primaryButton = "Try Again" + } +} +