Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Liveness Timeout Images #253

Open
wants to merge 19 commits into
base: new-smart-selfie-capture
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions Sources/SmileID/Classes/FaceDetector/LivenessCheckManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ enum LivenessTask {
case lookUp
}

protocol LivenessCheckManagerDelegate: AnyObject {
func didCompleteLivenessTask()
func didCompleteLivenessChallenge()
func livenessChallengeTimeout()
}

class LivenessCheckManager: ObservableObject {
/// The sequence of liveness tasks to be performed.
private var livenessTaskSequence: [LivenessTask] = []
/// The index pointing to the current task in the sequence.
private var currentTaskIndex: Int = 0
/// The view model associated with the selfie capture process.
weak var selfieViewModel: SelfieViewModelV2?
/// A closure to trigger photo capture during the liveness check.
var captureImage: (() -> Void)?

weak var delegate: LivenessCheckManagerDelegate?

// MARK: Constants
/// The minimum threshold for yaw (left-right head movement)
Expand Down Expand Up @@ -88,7 +92,7 @@ class LivenessCheckManager: ObservableObject {
/// Handles the timeout event for a task.
private func handleTaskTimeout() {
stopTaskTimer()
selfieViewModel?.perform(action: .activeLivenessTimeout)
delegate?.livenessChallengeTimeout()
}

/// Advances to the next task in the sequence
Expand Down Expand Up @@ -160,12 +164,11 @@ class LivenessCheckManager: ObservableObject {
/// Completes the current task and moves to the next one.
/// If all tasks are completed, it signals the completion of the liveness challenge.
private func completeCurrentTask() {
captureImage?()
captureImage?()
delegate?.didCompleteLivenessTask()

if !advanceToNextTask() {
// Liveness challenge complete
selfieViewModel?.perform(action: .activeLivenessCompleted)
delegate?.didCompleteLivenessChallenge()
self.currentTask = nil
}
}
Expand Down
14 changes: 10 additions & 4 deletions Sources/SmileID/Classes/Networking/Models/FailureReason.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import Foundation

public enum FailureReason {
case activeLivenessTimedOut
public enum FailureReason: Encodable {
case mobileActiveLivenessTimeout

var key: String {
private enum CodingKeys: String, CodingKey {
case mobileActiveLivenessTimeout = "mobile_active_liveness_timed_out"
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .activeLivenessTimedOut: return "mobile_active_liveness_timed_out"
case .mobileActiveLivenessTimeout:
try container.encode(true, forKey: .mobileActiveLivenessTimeout)
}
}
}
16 changes: 7 additions & 9 deletions Sources/SmileID/Classes/Networking/ServiceRunnable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,13 @@ extension ServiceRunnable {
body.append(lineBreak.data(using: .utf8)!)

// Append failure reason if available
if let failureReason {
let activeLivenessTimedOutString = "\(failureReason == .activeLivenessTimedOut)"
if let valueData = "\(activeLivenessTimedOutString)\(lineBreak)".data(using: .utf8) {
body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!)
body.append(
"Content-Disposition: form-data; name=\"\(failureReason.key)\"\(lineBreak + lineBreak)".data(
using: .utf8)!)
body.append(valueData)
}
if let failureReason,
let failureReasonData = try? encoder.encode(failureReason) {
body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"failure_reason\"\(lineBreak)".data(using: .utf8)!)
body.append("Content-Type: application/json\(lineBreak + lineBreak)".data(using: .utf8)!)
body.append(failureReasonData)
body.append(lineBreak.data(using: .utf8)!)
}

// Append final boundary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class SelfieSubmissionManager {
private let isEnroll: Bool
private let numLivenessImages: Int
private let allowNewEnroll: Bool
private var selfieImage: URL?
private var selfieImageUrl: URL?
private var livenessImages: [URL]
private var extraPartnerParams: [String: String]
private let localMetadata: LocalMetadata
Expand All @@ -26,7 +26,7 @@ final class SelfieSubmissionManager {
isEnroll: Bool,
numLivenessImages: Int,
allowNewEnroll: Bool,
selfieImage: URL?,
selfieImageUrl: URL?,
livenessImages: [URL],
extraPartnerParams: [String: String],
localMetadata: LocalMetadata
Expand All @@ -36,16 +36,16 @@ final class SelfieSubmissionManager {
self.isEnroll = isEnroll
self.numLivenessImages = numLivenessImages
self.allowNewEnroll = allowNewEnroll
self.selfieImage = selfieImage
self.selfieImageUrl = selfieImageUrl
self.livenessImages = livenessImages
self.extraPartnerParams = extraPartnerParams
self.localMetadata = localMetadata
}

func submitJob(forcedFailure: Bool = false) async throws {
func submitJob(failureReason: FailureReason? = nil) async throws {
do {
// Validate that the necessary selfie data is present
try validateImages(forcedFailure: forcedFailure)
try validateImages()

// Determine the type of job (enrollment or authentication)
let jobType = determineJobType()
Expand All @@ -68,7 +68,7 @@ final class SelfieSubmissionManager {
authResponse: authResponse,
smartSelfieImage: smartSelfieImage,
smartSelfieLivenessImages: smartSelfieLivenessImages,
forcedFailure: forcedFailure
failureReason: failureReason
)

// Update local storage after successful submission
Expand All @@ -81,15 +81,10 @@ final class SelfieSubmissionManager {
}
}

private func validateImages(forcedFailure: Bool) throws {
if forcedFailure {
guard selfieImage != nil else {
throw SmileIDError.unknown("Selfie capture failed")
}
} else {
guard selfieImage != nil, livenessImages.count == numLivenessImages else {
throw SmileIDError.unknown("Selfie capture failed")
}
private func validateImages() throws {
guard selfieImageUrl != nil,
livenessImages.count == numLivenessImages else {
throw SmileIDError.unknown("Selfie capture failed")
}
}

Expand Down Expand Up @@ -119,8 +114,8 @@ final class SelfieSubmissionManager {
}

private func prepareImagesForSubmission() throws -> (MultipartBody, [MultipartBody]) {
guard let smartSelfieImage = createMultipartBody(from: selfieImage) else {
throw SmileIDError.unknown("Failed to process selfie image")
guard let smartSelfieImage = createMultipartBody(from: selfieImageUrl) else {
throw SmileIDError.fileNotFound("Could not create multipart body for file")
}

let smartSelfieLivenessImages = livenessImages.compactMap {
Expand All @@ -136,7 +131,9 @@ final class SelfieSubmissionManager {
private func createMultipartBody(from fileURL: URL?) -> MultipartBody? {
guard let fileURL = fileURL,
let imageData = try? Data(contentsOf: fileURL)
else { return nil }
else {
return nil
}
return MultipartBody(
withImage: imageData,
forKey: fileURL.lastPathComponent,
Expand All @@ -148,7 +145,7 @@ final class SelfieSubmissionManager {
authResponse: AuthenticationResponse,
smartSelfieImage: MultipartBody,
smartSelfieLivenessImages: [MultipartBody],
forcedFailure: Bool
failureReason: FailureReason?
) async throws -> SmartSelfieResponse {
if isEnroll {
return try await SmileID.api
Expand All @@ -162,7 +159,7 @@ final class SelfieSubmissionManager {
callbackUrl: SmileID.callbackUrl,
sandboxResult: nil,
allowNewEnroll: allowNewEnroll,
failureReason: forcedFailure ? .activeLivenessTimedOut : nil,
failureReason: failureReason,
metadata: localMetadata.metadata
)
} else {
Expand All @@ -176,7 +173,7 @@ final class SelfieSubmissionManager {
partnerParams: extraPartnerParams,
callbackUrl: SmileID.callbackUrl,
sandboxResult: nil,
failureReason: forcedFailure ? .activeLivenessTimedOut : nil,
failureReason: failureReason,
metadata: localMetadata.metadata
)
}
Expand All @@ -187,7 +184,7 @@ final class SelfieSubmissionManager {
try LocalStorage.moveToSubmittedJobs(jobId: self.jobId)

// Update the references to the submitted selfie and liveness images
self.selfieImage = try LocalStorage.getFileByType(
self.selfieImageUrl = try LocalStorage.getFileByType(
jobId: jobId,
fileType: FileType.selfie,
submitted: true
Expand All @@ -204,7 +201,7 @@ final class SelfieSubmissionManager {
do {
let didMove = try LocalStorage.handleOfflineJobFailure(jobId: self.jobId, error: smileIDError)
if didMove {
self.selfieImage = try LocalStorage.getFileByType(jobId: jobId, fileType: .selfie, submitted: true)
self.selfieImageUrl = try LocalStorage.getFileByType(jobId: jobId, fileType: .selfie, submitted: true)
self.livenessImages =
try LocalStorage.getFilesByType(jobId: jobId, fileType: .liveness, submitted: true) ?? []
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ enum SelfieViewModelAction {
case onViewAppear
case windowSizeDetected(CGSize, EdgeInsets)

// Face Detection Actions
case activeLivenessCompleted
case activeLivenessTimeout

// Job Submission Actions
case jobProcessingDone
case retryJobSubmission

Expand Down
Loading
Loading