Skip to content

Commit 94194cb

Browse files
authored
feat: add no-light/facemovementonly challenge and back camera support (#131)
* feat: add no light challenge implementation (#127) * feat: add no light challenge implementation * update package.swift for CI build * Fix unit tests * Address review comments * chore: Add attempt count changes (#137) * chore: Add attempt count changes * Fix unit tests * add unit tests * Update region for example liveness view * Update amplify-swift dependency * chore: update dependencies after rebase * fix: handle error on loading view (#154) * chore: update amplify package dependency after rebase * update Package.resolved files * feat: add back camera support (#180) * chore: back camera support * Add code for error scenarios * update error codes and message * Add challengeOption parameter and remove error codes * Update ChallengeOptions and use camera position based on challenge type received * Add UI changes for selecting back camera in HostApp * add default parameter to ChallengeOptions init * fix formatting * fix test build * address review comments * remove unused property * update amplify dependency
1 parent 3affdd1 commit 94194cb

33 files changed

+668
-206
lines changed

HostApp/HostApp.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
9070FFBD285112B5009867D5 /* HostAppUITests */,
132132
9070FFA1285112B4009867D5 /* Products */,
133133
90215EED291E9FB60050F2AD /* Frameworks */,
134+
A5A9AF5054D0FF13505B212A /* AmplifyConfig */,
134135
);
135136
sourceTree = "<group>";
136137
};
@@ -213,6 +214,15 @@
213214
path = Model;
214215
sourceTree = "<group>";
215216
};
217+
A5A9AF5054D0FF13505B212A /* AmplifyConfig */ = {
218+
isa = PBXGroup;
219+
children = (
220+
973619242BA378690003A590 /* awsconfiguration.json */,
221+
973619232BA378690003A590 /* amplifyconfiguration.json */,
222+
);
223+
name = AmplifyConfig;
224+
sourceTree = "<group>";
225+
};
216226
/* End PBXGroup section */
217227

218228
/* Begin PBXNativeTarget section */

HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

HostApp/HostApp/Model/LivenessResult.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
//
77

88
import Foundation
9+
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
910

1011
struct LivenessResult: Codable {
1112
let auditImageBytes: String?
1213
let confidenceScore: Double
1314
let isLive: Bool
15+
let challenge: Event?
1416
}
1517

1618
extension LivenessResult: CustomDebugStringConvertible {
@@ -20,6 +22,17 @@ extension LivenessResult: CustomDebugStringConvertible {
2022
- confidenceScore: \(confidenceScore)
2123
- isLive: \(isLive)
2224
- auditImageBytes: \(auditImageBytes == nil ? "nil" : "<placeholder>")
25+
- challenge: type: \(String(describing: challenge?.type)) + " version: " + \(String(describing: challenge?.version))
2326
"""
2427
}
2528
}
29+
30+
struct Event: Codable {
31+
let version: String
32+
let type: ChallengeType
33+
34+
enum CodingKeys: String, CodingKey {
35+
case version = "Version"
36+
case type = "Type"
37+
}
38+
}

HostApp/HostApp/Views/ExampleLivenessView.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,27 @@ import SwiftUI
99
import FaceLiveness
1010

1111
struct ExampleLivenessView: View {
12-
@Binding var isPresented: Bool
12+
@Binding var containerViewState: ContainerViewState
1313
@ObservedObject var viewModel: ExampleLivenessViewModel
1414

15-
init(sessionID: String, isPresented: Binding<Bool>) {
16-
self.viewModel = .init(sessionID: sessionID)
17-
self._isPresented = isPresented
15+
init(sessionID: String, containerViewState: Binding<ContainerViewState>) {
16+
self._containerViewState = containerViewState
17+
if case let .liveness(selectedCamera) = _containerViewState.wrappedValue {
18+
self.viewModel = .init(sessionID: sessionID, presentationState: .liveness(selectedCamera))
19+
} else {
20+
self.viewModel = .init(sessionID: sessionID)
21+
}
1822
}
1923

2024
var body: some View {
2125
switch viewModel.presentationState {
22-
case .liveness:
26+
case .liveness(let camera):
2327
FaceLivenessDetectorView(
2428
sessionID: viewModel.sessionID,
2529
region: "us-east-1",
30+
challengeOptions: .init(faceMovementChallengeOption: FaceMovementChallengeOption(camera: camera)),
2631
isPresented: Binding(
27-
get: { viewModel.presentationState == .liveness },
32+
get: { viewModel.presentationState == .liveness(camera) },
2833
set: { _ in }
2934
),
3035
onCompletion: { result in
@@ -33,11 +38,11 @@ struct ExampleLivenessView: View {
3338
case .success:
3439
withAnimation { viewModel.presentationState = .result }
3540
case .failure(.sessionNotFound), .failure(.cameraPermissionDenied), .failure(.accessDenied):
36-
viewModel.presentationState = .liveness
37-
isPresented = false
41+
viewModel.presentationState = .liveness(camera)
42+
containerViewState = .startSession
3843
case .failure(.userCancelled):
39-
viewModel.presentationState = .liveness
40-
isPresented = false
44+
viewModel.presentationState = .liveness(camera)
45+
containerViewState = .startSession
4146
case .failure(.sessionTimedOut):
4247
viewModel.presentationState = .error(.sessionTimedOut)
4348
case .failure(.socketClosed):
@@ -46,6 +51,10 @@ struct ExampleLivenessView: View {
4651
viewModel.presentationState = .error(.countdownFaceTooClose)
4752
case .failure(.invalidSignature):
4853
viewModel.presentationState = .error(.invalidSignature)
54+
case .failure(.faceInOvalMatchExceededTimeLimitError):
55+
viewModel.presentationState = .error(.faceInOvalMatchExceededTimeLimitError)
56+
case .failure(.internalServer):
57+
viewModel.presentationState = .error(.internalServer)
4958
case .failure(.cameraNotAvailable):
5059
viewModel.presentationState = .error(.cameraNotAvailable)
5160
case .failure(.validation):
@@ -58,11 +67,11 @@ struct ExampleLivenessView: View {
5867
}
5968
}
6069
)
61-
.id(isPresented)
70+
.id(containerViewState)
6271
case .result:
6372
LivenessResultView(
6473
sessionID: viewModel.sessionID,
65-
onTryAgain: { isPresented = false },
74+
onTryAgain: { containerViewState = .startSession },
6675
content: {
6776
LivenessResultContentView(fetchResults: viewModel.fetchLivenessResult)
6877
}
@@ -71,7 +80,7 @@ struct ExampleLivenessView: View {
7180
case .error(let detectionError):
7281
LivenessResultView(
7382
sessionID: viewModel.sessionID,
74-
onTryAgain: { isPresented = false },
83+
onTryAgain: { containerViewState = .startSession },
7584
content: {
7685
switch detectionError {
7786
case .socketClosed:

HostApp/HostApp/Views/ExampleLivenessViewModel.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import FaceLiveness
1010
import Amplify
1111

1212
class ExampleLivenessViewModel: ObservableObject {
13-
@Published var presentationState = PresentationState.liveness
13+
@Published var presentationState: PresentationState = .liveness(.front)
1414
let sessionID: String
1515

16-
init(sessionID: String) {
16+
init(sessionID: String, presentationState: PresentationState = .liveness(.front)) {
1717
self.sessionID = sessionID
18+
self.presentationState = presentationState
1819
}
1920

2021
func fetchLivenessResult() async throws -> LivenessResultContentView.Result {
@@ -30,6 +31,6 @@ class ExampleLivenessViewModel: ObservableObject {
3031
}
3132

3233
enum PresentationState: Equatable {
33-
case liveness, result, error(FaceLivenessDetectionError)
34+
case liveness(LivenessCamera), result, error(FaceLivenessDetectionError)
3435
}
3536
}

HostApp/HostApp/Views/LivenessResultContentView+Result.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
910

1011
extension LivenessResultContentView {
1112
struct Result {
@@ -15,6 +16,7 @@ extension LivenessResultContentView {
1516
let valueBackgroundColor: Color
1617
let auditImage: Data?
1718
let isLive: Bool
19+
let challenge: Event?
1820

1921
init(livenessResult: LivenessResult) {
2022
guard livenessResult.confidenceScore > 0 else {
@@ -24,6 +26,7 @@ extension LivenessResultContentView {
2426
valueBackgroundColor = .clear
2527
auditImage = nil
2628
isLive = false
29+
challenge = nil
2730
return
2831
}
2932
isLive = livenessResult.isLive
@@ -41,6 +44,7 @@ extension LivenessResultContentView {
4144
auditImage = livenessResult.auditImageBytes.flatMap{
4245
Data(base64Encoded: $0)
4346
}
47+
challenge = livenessResult.challenge
4448
}
4549
}
4650

HostApp/HostApp/Views/LivenessResultContentView.swift

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
//
77

88
import SwiftUI
9+
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
910

1011
struct LivenessResultContentView: View {
11-
@State var result: Result = .init(livenessResult: .init(auditImageBytes: nil, confidenceScore: -1, isLive: false))
12+
@State var result: Result = .init(livenessResult: .init(auditImageBytes: nil, confidenceScore: -1, isLive: false, challenge: nil))
1213
let fetchResults: () async throws -> Result
1314

1415
var body: some View {
@@ -67,26 +68,48 @@ struct LivenessResultContentView: View {
6768
}
6869
}
6970

71+
func step(number: Int, text: String) -> some View {
72+
HStack(alignment: .top) {
73+
Text("\(number).")
74+
Text(text)
75+
}
76+
}
77+
78+
@ViewBuilder
7079
private func steps() -> some View {
71-
func step(number: Int, text: String) -> some View {
72-
HStack(alignment: .top) {
73-
Text("\(number).")
74-
Text(text)
80+
switch result.challenge?.type {
81+
case .faceMovementChallenge:
82+
VStack(
83+
alignment: .leading,
84+
spacing: 8
85+
) {
86+
Text("Tips to pass the video check:")
87+
.fontWeight(.semibold)
88+
89+
Text("Remove sunglasses, mask, hat, or anything blocking your face.")
90+
.accessibilityElement(children: .combine)
91+
}
92+
case .faceMovementAndLightChallenge:
93+
VStack(
94+
alignment: .leading,
95+
spacing: 8
96+
) {
97+
Text("Tips to pass the video check:")
98+
.fontWeight(.semibold)
99+
100+
step(number: 1, text: "Avoid very bright lighting conditions, such as direct sunlight.")
101+
.accessibilityElement(children: .combine)
102+
103+
step(number: 2, text: "Remove sunglasses, mask, hat, or anything blocking your face.")
104+
.accessibilityElement(children: .combine)
105+
}
106+
case .none:
107+
VStack(
108+
alignment: .leading,
109+
spacing: 8
110+
) {
111+
EmptyView()
75112
}
76-
}
77-
78-
return VStack(
79-
alignment: .leading,
80-
spacing: 8
81-
) {
82-
Text("Tips to pass the video check:")
83-
.fontWeight(.semibold)
84-
85-
step(number: 1, text: "Avoid very bright lighting conditions, such as direct sunlight.")
86-
.accessibilityElement(children: .combine)
87-
88-
step(number: 2, text: "Remove sunglasses, mask, hat, or anything blocking your face.")
89-
.accessibilityElement(children: .combine)
90113
}
91114
}
92115
}
@@ -99,7 +122,8 @@ extension LivenessResultContentView {
99122
livenessResult: .init(
100123
auditImageBytes: nil,
101124
confidenceScore: 99.8329,
102-
isLive: true
125+
isLive: true,
126+
challenge: nil
103127
)
104128
)
105129
}

HostApp/HostApp/Views/RootView.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,32 @@
66
//
77

88
import SwiftUI
9+
import FaceLiveness
910

1011
struct RootView: View {
1112
@EnvironmentObject var sceneDelegate: SceneDelegate
1213
@State var sessionID = ""
13-
@State var isPresentingContainerView = false
14+
@State var containerViewState = ContainerViewState.startSession
1415

1516
var body: some View {
16-
if isPresentingContainerView {
17+
switch containerViewState {
18+
case .liveness:
1719
ExampleLivenessView(
1820
sessionID: sessionID,
19-
isPresented: $isPresentingContainerView
21+
containerViewState: $containerViewState
2022
)
21-
} else {
23+
case .startSession:
2224
StartSessionView(
2325
sessionID: $sessionID,
24-
isPresentingContainerView: $isPresentingContainerView
26+
containerViewState: $containerViewState
2527
)
2628
.background(Color.dynamicColors(light: .white, dark: .secondarySystemBackground))
2729
.edgesIgnoringSafeArea(.all)
2830
}
2931
}
3032
}
33+
34+
enum ContainerViewState: Hashable {
35+
case liveness(LivenessCamera)
36+
case startSession
37+
}

HostApp/HostApp/Views/StartSessionView.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ struct StartSessionView: View {
1212
@EnvironmentObject var sceneDelegate: SceneDelegate
1313
@ObservedObject var viewModel = StartSessionViewModel()
1414
@Binding var sessionID: String
15-
@Binding var isPresentingContainerView: Bool
15+
@Binding var containerViewState: ContainerViewState
1616
@State private var showAlert = false
1717

1818
var body: some View {
@@ -35,7 +35,8 @@ struct StartSessionView: View {
3535
viewModel.createSession { sessionId, err in
3636
if let sessionId = sessionId {
3737
sessionID = sessionId
38-
isPresentingContainerView = true
38+
// modify camera preference for `FaceMovementChallenge`
39+
containerViewState = .liveness(.front)
3940
}
4041

4142
showAlert = err != nil
@@ -50,7 +51,7 @@ struct StartSessionView: View {
5051
dismissButton: .default(
5152
Text("OK"),
5253
action: {
53-
isPresentingContainerView = false
54+
containerViewState = .startSession
5455
}
5556
)
5657
)

0 commit comments

Comments
 (0)