From 5d51b544c3132243a741636cb1019f26421918a6 Mon Sep 17 00:00:00 2001 From: JNdhlovu Date: Tue, 10 Dec 2024 10:28:49 +0200 Subject: [PATCH 1/5] feat: ios --- .../OrchestratedBiometricKycViewModel.swift | 63 +++- .../Classes/Helpers/LocalStorage.swift | 98 +++--- .../BaseDocumentVerificationSubmission.swift | 115 +++++++ .../Base/BaseJobSubmission.swift | 166 ++++++++++ .../Base/BaseSubmissionViewModel.swift | 120 +++++++ .../Base/BaseSynchronousJobSubmission.swift | 58 ++++ .../BiometricKYCSubmission.swift | 93 ++++++ .../DocumentVerificationSubmission.swift | 57 ++++ ...hancedDocumentVerificationSubmission.swift | 58 ++++ .../JobSubmission/Results/CaptureResult.swift | 45 +++ .../Results/SmileIDResults.swift | 81 +++++ .../JobSubmission/SelfieSubmission.swift | 173 ++++++++++ .../SelfieCapture/SelfieViewModel.swift | 302 +++++++----------- 13 files changed, 1199 insertions(+), 230 deletions(-) create mode 100644 Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/Results/CaptureResult.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift create mode 100644 Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift diff --git a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift index d9c25a5ef..0ef7c2776 100644 --- a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift +++ b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift @@ -6,7 +6,7 @@ internal enum BiometricKycStep { case processing(ProcessingState) } -internal class OrchestratedBiometricKycViewModel: ObservableObject { +internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel { // MARK: - Input Properties private let userId: String @@ -242,6 +242,67 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject { updateStep(.processing(.error)) } } + + public override func createSubmission() throws -> BaseJobSubmission { + guard let selfieImage = selfieImage, + !livenessFiles?.isEmpty == numLivenessImages else { + throw SmileIDError.unknown("Selfie images missing") + } + return BiometricKYCSubmission( + userId: userId, + jobId: jobId, + allowNewEnroll: allowNewEnroll, + selfieFile: selfieImage, + idInfo: idInfo, + livenessFiles: livenessFiles, + extraPartnerParams: extraPartnerParams, + metadata: localMetadata.metadata + ) + } + + public override func triggerProcessingState() { + DispatchQueue.main.async { self.processingState = .inProgress } + } + + public override func handleSuccess(data: BiometricKycResult) { + apiResponse = data.apiResponse + DispatchQueue.main.async { self.processingState = .success } + } + + public override func handleError(error: Error) { + if let smileError = error as? SmileIDError { + print("Error submitting job: \(error)") + let (errorMessageRes, errorMessage) = toErrorMessage(error: smileError) + self.error = error + self.errorMessageRes = errorMessageRes + self.errorMessage = errorMessage + DispatchQueue.main.async { self.processingState = .error } + } else { + print("Error submitting job: \(error)") + self.error = error + DispatchQueue.main.async { self.processingState = .error } + } + } + + public override func handleSubmissionFiles(jobId: String) throws { + self.selfieFile = try LocalStorage.getFileByType( + jobId: jobId, + fileType: FileType.selfie, + submitted: true + ) + self.livenessFiles = try LocalStorage.getFilesByType( + jobId: jobId, + fileType: FileType.liveness, + submitted: true + ) ?? [] + } + + public override func handleOfflineSuccess() { + DispatchQueue.main.async { + self.errorMessageRes = "Offline.Message" + self.step = .success + } + } } extension OrchestratedBiometricKycViewModel: SmartSelfieResultDelegate { diff --git a/Sources/SmileID/Classes/Helpers/LocalStorage.swift b/Sources/SmileID/Classes/Helpers/LocalStorage.swift index 0b9bc7ab8..410810f40 100644 --- a/Sources/SmileID/Classes/Helpers/LocalStorage.swift +++ b/Sources/SmileID/Classes/Helpers/LocalStorage.swift @@ -9,7 +9,7 @@ public class LocalStorage { private static let previewImageName = "PreviewImage.jpg" private static let jsonEncoder = JSONEncoder() private static let jsonDecoder = JSONDecoder() - + static var defaultDirectory: URL { get throws { let documentDirectory = try FileManager.default.url( @@ -21,19 +21,19 @@ public class LocalStorage { return documentDirectory.appendingPathComponent(defaultFolderName) } } - + static var unsubmittedJobDirectory: URL { get throws { try defaultDirectory.appendingPathComponent(unsubmittedFolderName) } } - + static var submittedJobDirectory: URL { get throws { try defaultDirectory.appendingPathComponent(submittedFolderName) } } - + private static func createSmileFile( to folder: String, name: String, @@ -43,25 +43,25 @@ public class LocalStorage { let destinationFolder = try unsubmittedJobDirectory.appendingPathComponent(folder) return try write(data, to: destinationFolder.appendingPathComponent(name)) } - + private static func filename(for name: String) -> String { "\(name)_\(Date().millisecondsSince1970).jpg" } - + public static func createSelfieFile( jobId: String, selfieFile data: Data ) throws -> URL { try createSmileFile(to: jobId, name: filename(for: FileType.selfie.name), file: data) } - + public static func createLivenessFile( jobId: String, livenessFile data: Data ) throws -> URL { try createSmileFile(to: jobId, name: filename(for: FileType.liveness.name), file: data) } - + public static func createDocumentFile( jobId: String, fileType: FileType, @@ -69,7 +69,7 @@ public class LocalStorage { ) throws -> URL { try createSmileFile(to: jobId, name: filename(for: fileType.name), file: data) } - + public static func getFileByType( jobId: String, fileType: FileType, @@ -78,7 +78,7 @@ public class LocalStorage { let contents = try getDirectoryContents(jobId: jobId, submitted: submitted) return contents.first(where: { $0.lastPathComponent.contains(fileType.name) }) } - + public static func getFilesByType( jobId: String, fileType: FileType, @@ -87,7 +87,7 @@ public class LocalStorage { let contents = try getDirectoryContents(jobId: jobId, submitted: submitted) return contents.filter { $0.lastPathComponent.contains(fileType.name) } } - + static func createInfoJsonFile( jobId: String, idInfo: IdInfo? = nil, @@ -130,22 +130,22 @@ public class LocalStorage { )) return try createSmileFile(to: jobId, name: "info.json", file: data) } - + static func getInfoJsonFile( jobId: String ) throws -> URL { let contents = try getDirectoryContents(jobId: jobId) return contents.first(where: { $0.lastPathComponent == "info.json" })! } - - private static func createPrepUploadFile( + + static func createPrepUploadFile( jobId: String, prepUpload: PrepUploadRequest ) throws -> URL { let data = try jsonEncoder.encode(prepUpload) return try createSmileFile(to: jobId, name: "prep_upload.json", file: data) } - + static func fetchPrepUploadFile( jobId: String ) throws -> PrepUploadRequest { @@ -154,15 +154,15 @@ public class LocalStorage { let data = try Data(contentsOf: preupload!) return try jsonDecoder.decode(PrepUploadRequest.self, from: data) } - - private static func createAuthenticationRequestFile( + + static func createAuthenticationRequestFile( jobId: String, authentationRequest: AuthenticationRequest ) throws -> URL { let data = try jsonEncoder.encode(authentationRequest) return try createSmileFile(to: jobId, name: "authentication_request.json", file: data) } - + static func fetchAuthenticationRequestFile( jobId: String ) throws -> AuthenticationRequest { @@ -171,7 +171,7 @@ public class LocalStorage { let data = try Data(contentsOf: authenticationrequest!) return try jsonDecoder.decode(AuthenticationRequest.self, from: data) } - + static func saveOfflineJob( jobId: String, userId: String, @@ -209,7 +209,7 @@ public class LocalStorage { ) } } - + private static func write(_ data: Data, to url: URL, options completeFileProtection: Bool = true) throws -> URL { let directoryURL = url.deletingLastPathComponent() try fileManager.createDirectory( @@ -226,11 +226,11 @@ public class LocalStorage { return url } } - + private static func createDirectory(at url: URL) throws { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) } - + private static func getDirectoryContents( jobId: String, submitted: Bool = false @@ -239,7 +239,7 @@ public class LocalStorage { let folderPathURL = baseDirectory.appendingPathComponent(jobId) return try fileManager.contentsOfDirectory(at: folderPathURL, includingPropertiesForKeys: nil) } - + static func getUnsubmittedJobs() -> [String] { do { return try fileManager.contentsOfDirectory(atPath: unsubmittedJobDirectory.relativePath) @@ -248,7 +248,7 @@ public class LocalStorage { return [] } } - + static func getSubmittedJobs() -> [String] { do { return try fileManager.contentsOfDirectory(atPath: submittedJobDirectory.relativePath) @@ -257,7 +257,7 @@ public class LocalStorage { return [] } } - + static func moveToSubmittedJobs(jobId: String) throws { try createDirectory(at: submittedJobDirectory) let unsubmittedFileDirectory = try unsubmittedJobDirectory.appendingPathComponent(jobId) @@ -267,7 +267,7 @@ public class LocalStorage { } try fileManager.moveItem(at: unsubmittedFileDirectory, to: submittedFileDirectory) } - + /// Moves files from unsubmitted to submitted when not in Offline Mode, or if it was not a /// network error /// Returns: true if files were moved @@ -282,7 +282,7 @@ public class LocalStorage { } return didMove } - + public static func toZip(uploadRequest: UploadRequest) throws -> Data { var destinationFolder: String? // Extract directory paths from all images and check for consistency @@ -296,19 +296,19 @@ public class LocalStorage { destinationFolder = folder } } - + // Ensure a destination folder was found guard let finalDestinationFolder = destinationFolder else { throw SmileIDError.fileNotFound("Job not found") } - + // Create full URLs for all images let imageUrls = uploadRequest.images.map { imageInfo in URL(fileURLWithPath: finalDestinationFolder).appendingPathComponent(imageInfo.fileName) } - + var allUrls = imageUrls - + do { // Get the URL for the JSON file let jsonUrl = try LocalStorage.getInfoJsonFile(jobId: finalDestinationFolder) @@ -316,11 +316,11 @@ public class LocalStorage { } catch { debugPrint("Warning: info.json file not found. Continuing without it.") } - + // Zip all files return try zipFiles(at: allUrls) } - + public static func zipFiles(at urls: [URL]) throws -> Data { let archive = try Archive(accessMode: .create) for url in urls { @@ -328,20 +328,20 @@ public class LocalStorage { } return archive.data! } - + private static func extractDirectoryPath(from path: String) -> String? { let url = URL(fileURLWithPath: path) // Remove the last component and add a trailing slash let directoryPath = url.deletingLastPathComponent().path + "/" return directoryPath } - + private static func delete(at url: URL) throws { if fileManager.fileExists(atPath: url.relativePath) { try fileManager.removeItem(atPath: url.relativePath) } } - + static func delete(at jobIds: [String]) throws { try jobIds.forEach { let unsubmittedJob = try unsubmittedJobDirectory.appendingPathComponent($0) @@ -350,13 +350,13 @@ public class LocalStorage { try delete(at: submittedJob) } } - + static func deleteAll() throws { if try fileManager.fileExists(atPath: defaultDirectory.relativePath) { try fileManager.removeItem(atPath: defaultDirectory.relativePath) } } - + static func deleteLivenessAndSelfieFiles(at jobIds: [String]) throws { func deleteMatchingFiles(in directory: URL) throws { let contents = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) @@ -367,11 +367,11 @@ public class LocalStorage { } } } - + try jobIds.forEach { jobId in let unsubmittedJob = try unsubmittedJobDirectory.appendingPathComponent(jobId) try deleteMatchingFiles(in: unsubmittedJob) - + let submittedJob = try submittedJobDirectory.appendingPathComponent(jobId) try deleteMatchingFiles(in: submittedJob) } @@ -383,3 +383,21 @@ public extension Date { Int64((timeIntervalSince1970 * 1000.0).rounded()) } } + +public extension URL { + func asDocumentFrontImage() -> UploadImageInfo { + return UploadImageInfo(imageTypeId: .idCardJpgFile, fileName: lastPathComponent) + } + + func asDocumentBackImage() -> UploadImageInfo { + return UploadImageInfo(imageTypeId: .idCardRearJpgFile, fileName: lastPathComponent) + } + + func asSelfieImage() -> UploadImageInfo { + return UploadImageInfo(imageTypeId: .selfieJpgFile, fileName: lastPathComponent) + } + + func asLivenessImage() -> UploadImageInfo { + return UploadImageInfo(imageTypeId: .livenessJpgFile, fileName: lastPathComponent) + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift new file mode 100644 index 000000000..59b395813 --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift @@ -0,0 +1,115 @@ +// +// BaseDocumentVerificationSubmission.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +public class BaseDocumentVerificationSubmission: BaseJobSubmission { + // MARK: - Properties + + private let userId: String + private let jobType: JobType + private let countryCode: String + private let documentType: String? + private let allowNewEnroll: Bool + private let documentFrontFile: URL + private let selfieFile: URL + private let documentBackFile: URL? + private let livenessFiles: [URL]? + private let extraPartnerParams: [String: String] + private let metadata: [Metadatum]? + + // MARK: - Initialization + + public init( + jobId: String, + userId: String, + jobType: JobType, + countryCode: String, + documentType: String?, + allowNewEnroll: Bool, + documentFrontFile: URL, + selfieFile: URL, + documentBackFile: URL? = nil, + livenessFiles: [URL]? = nil, + extraPartnerParams: [String: String], + metadata: [Metadatum]? = nil + ) { + self.userId = userId + self.jobType = jobType + self.countryCode = countryCode + self.documentType = documentType + self.allowNewEnroll = allowNewEnroll + self.documentFrontFile = documentFrontFile + self.selfieFile = selfieFile + self.documentBackFile = documentBackFile + self.livenessFiles = livenessFiles + self.extraPartnerParams = extraPartnerParams + self.metadata = metadata + super.init(jobId: jobId) + } + + // MARK: - Overridden Methods + + public override func createAuthRequest() -> AuthenticationRequest { + return AuthenticationRequest( + jobType: jobType, + enrollment: false, + jobId: jobId, + userId: userId, + country: countryCode, + idType: documentType + ) + } + + public override func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { + let partnerParams = authResponse?.partnerParams.copy(extras: extraPartnerParams) ?? PartnerParams(jobId: jobId, userId: userId, jobType: jobType, extras: extraPartnerParams) + + + return PrepUploadRequest( + partnerParams: partnerParams, + allowNewEnroll: String(allowNewEnroll), + metadata: metadata, + timestamp: authResponse?.timestamp ?? "", + signature: authResponse?.signature ?? "" + ) + } + + public override func createUploadRequest(authResponse: AuthenticationResponse?) -> UploadRequest { + let frontImageInfo = documentFrontFile.asDocumentFrontImage() + let backImageInfo = documentBackFile?.asDocumentBackImage() + let selfieImageInfo = selfieFile.asSelfieImage() + let livenessImageInfo = livenessFiles?.map { $0.asLivenessImage() } ?? [] + + return UploadRequest( + images: [frontImageInfo] + + (backImageInfo.map { [$0] } ?? []) + + [selfieImageInfo] + + livenessImageInfo, + idInfo : IdInfo(country: countryCode, idType: documentType) + ) + } + + // MARK: - Abstract Methods + + /// Creates the result instance for the document verification submission + /// - Parameters: + /// - selfieFile: The selfie file URL + /// - documentFrontFile: The document front file URL + /// - livenessFiles: Optional array of liveness file URLs + /// - documentBackFile: Optional document back file URL + /// - didSubmitJob: Whether the job was submitted + /// - Returns: Result instance of type ResultType + open func createResultInstance( + selfieFile: URL, + documentFrontFile: URL, + livenessFiles: [URL]?, + documentBackFile: URL?, + didSubmitJob: Bool + ) -> ResultType { + fatalError("Must be implemented by subclass") + } +} + +// MARK: - URL Extensions diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift new file mode 100644 index 000000000..9d6e6f987 --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift @@ -0,0 +1,166 @@ +// +// BaseJobSubmission.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +public class BaseJobSubmission { + // MARK: - Properties + + public let jobId: String + + // MARK: - Initialization + + public init(jobId: String) { + self.jobId = jobId + } + + // MARK: - Methods to Override + + /// Creates the authentication request for the job submission + /// - Returns: Authentication request object + public func createAuthRequest() -> AuthenticationRequest { + fatalError("Must be implemented by subclass") + } + + /// Creates the prep upload request for the job submission + /// - Parameter authResponse: Optional authentication response from previous step + /// - Returns: Prep upload request object + public func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { + fatalError("Must be implemented by subclass") + } + + /// Creates the upload request for the job submission + /// - Parameter authResponse: Optional authentication response from previous step + /// - Returns: Upload request object + public func createUploadRequest(authResponse: AuthenticationResponse?) -> UploadRequest { + fatalError("Must be implemented by subclass") + } + + /// Creates the success result for the job submission + /// - Parameter didSubmit: Whether the job was submitted to the backend + /// - Returns: Success result object + public func createSuccessResult(didSubmit: Bool) async throws -> SmileIDResult.Success { + fatalError("Must be implemented by subclass") + } + + // MARK: - Public Methods + + /// Executes the job submission process + /// - Parameters: + /// - skipApiSubmission: If true, skips the API submission + /// - offlineMode: If true, performs offline preparation + /// - Returns: Result of the job submission + public func executeSubmission( + skipApiSubmission: Bool = false, + offlineMode: Bool = false + ) async -> SmileIDResult { + do { + if skipApiSubmission { + let successResult = try await createSuccessResult(didSubmit: false) + return .success(successResult) + } else { + return try await executeApiSubmission(offlineMode: offlineMode) + } + } catch { + return .error(error) + } + } + + /// Handles offline preparation logic + /// Override this method to implement custom offline preparation + public func handleOfflinePreparation() async throws { + let authRequest = createAuthRequest() + try LocalStorage.createAuthenticationRequestFile(jobId: jobId, authentationRequest: authRequest) + try LocalStorage.createPrepUploadFile( + jobId: jobId, + prepUpload: createPrepUploadRequest() + ) + } + + // MARK: - Private Methods + + func executeApiSubmission(offlineMode: Bool) async throws -> SmileIDResult { + if offlineMode { + try await handleOfflinePreparation() + } + + do { + let authResponse = try await executeAuthentication() + let prepUploadResponse = try await executePrepUpload(authResponse: authResponse) + try await executeUpload(authResponse: authResponse, prepUploadResponse: prepUploadResponse) + let successResult = try await createSuccessResult(didSubmit: true) + return .success(successResult) + } catch { + return .error(error) + } + } + + func executeAuthentication() async throws -> AuthenticationResponse { + do { + return try await SmileID.api.authenticate(request: createAuthRequest()) + } catch let error as SmileIDError { + throw error + } catch { + throw error + } + } + + private func executePrepUpload( + authResponse: AuthenticationResponse? + ) async throws -> PrepUploadResponse { + let prepUploadRequest = createPrepUploadRequest(authResponse: authResponse) + return try await executePrepUploadWithRetry(prepUploadRequest: prepUploadRequest) + } + + private func executePrepUploadWithRetry( + prepUploadRequest: PrepUploadRequest, + isRetry: Bool = false + ) async throws -> PrepUploadResponse { + do { + return try await SmileID.api.prepUpload(request: prepUploadRequest) + } catch let error as SmileIDError { + if !isRetry && error.localizedDescription == SmileErrorConstants.RETRY { + var retryRequest = prepUploadRequest + retryRequest.retry = "true" + return try await executePrepUploadWithRetry(prepUploadRequest: retryRequest, isRetry: true) + } else { + throw error + } + } + } + + private func executeUpload( + authResponse: AuthenticationResponse?, + prepUploadResponse: PrepUploadResponse + ) async throws { + do { + let uploadRequest = createUploadRequest(authResponse: authResponse) + let allFiles: [URL] + do { + let livenessFiles = try LocalStorage.getFilesByType(jobId: jobId, fileType: .liveness) ?? [] + let additionalFiles = try [ + LocalStorage.getFileByType(jobId: jobId, fileType: .selfie), + LocalStorage.getFileByType(jobId: jobId, fileType: .documentFront), + LocalStorage.getFileByType(jobId: jobId, fileType: .documentBack), + LocalStorage.getInfoJsonFile(jobId: jobId) + ].compactMap { $0 } + allFiles = livenessFiles + additionalFiles + } catch { + throw error + } + let zipData = try LocalStorage.zipFiles(at: allFiles) + try await SmileID.api.upload(zip: zipData, to: prepUploadResponse.uploadUrl) + } catch { + throw error + } + } + + // MARK: - Constants + +} + +private enum SmileErrorConstants { + static let RETRY = "2215" +} diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift new file mode 100644 index 000000000..f33a3be46 --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift @@ -0,0 +1,120 @@ +// +// BaseSubmissionViewModel.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// +open class BaseSubmissionViewModel: ObservableObject { + // MARK: - Internal Properties + + internal var result: SmileIDResult? + + // MARK: - Methods to Override + + /// Creates the submission object for the job + /// - Returns: A BaseJobSubmission instance + open func createSubmission() throws -> BaseJobSubmission { + fatalError("Must be implemented by subclass") + } + + /// Called when job is in processing state + open func triggerProcessingState() { + fatalError("Must be implemented by subclass") + } + + /// Handles successful job submission + /// - Parameter data: The result data + open func handleSuccess(data: ResultType) { + fatalError("Must be implemented by subclass") + } + + /// Handles job submission error + /// - Parameter error: The error that occurred + open func handleError(error: Error) { + fatalError("Must be implemented by subclass") + } + + /// Handles submission files for the given job ID + /// - Parameter jobId: The job ID + open func handleSubmissionFiles(jobId: String) throws { + fatalError("Must be implemented by subclass") + } + + /// Handles offline success scenario + open func handleOfflineSuccess() { + fatalError("Must be implemented by subclass") + } + + // MARK: - Internal Methods + + /// Handles proxy error scenarios + /// - Parameters: + /// - jobId: The job ID + /// - error: The error that occurred + internal func proxyErrorHandler(jobId: String, error: Error) { + // First handle SmileIDError specific cases + if let smileError = error as? SmileIDError { + do { + let didMoveToSubmitted = try LocalStorage.handleOfflineJobFailure(jobId: jobId, error: smileError) + + if didMoveToSubmitted { + try handleSubmissionFiles(jobId: jobId) + } + + // Check if we should handle this as an offline success case + if SmileID.allowOfflineMode && SmileIDError.isNetworkFailure(error: smileError) { + handleOfflineSuccess() + return + } + + // If not a network failure or offline mode isn't allowed, handle as regular error + handleError(error: smileError) + } catch { + // If handling offline failure throws, pass through the original error + handleError(error: smileError) + } + return + } + + // Handle non-SmileIDError cases + handleError(error: error) + } + + /// Submit job with given parameters + /// - Parameters: + /// - jobId: The job ID + /// - skipApiSubmission: If true, skips API submission + /// - offlineMode: If true, runs in offline mode + internal func submitJob( + jobId: String, + skipApiSubmission: Bool = false, + offlineMode: Bool = SmileID.allowOfflineMode + ) { + triggerProcessingState() + + Task { + do { + let submission = try createSubmission() + let submissionResult = try await submission.executeSubmission( + skipApiSubmission: skipApiSubmission, + offlineMode: offlineMode + ) + + await MainActor.run { + self.result = submissionResult + + switch submissionResult { + case .success(let success): + handleSuccess(data: success.result) + case .error(let error): + proxyErrorHandler(jobId: jobId, error: error) + } + } + } catch { + await MainActor.run { + proxyErrorHandler(jobId: jobId, error: error) + } + } + } + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift new file mode 100644 index 000000000..b7a94bafd --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift @@ -0,0 +1,58 @@ +// +// BaseSynchronousJobSubmission.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +public class BaseSynchronousJobSubmission: BaseJobSubmission { + // MARK: - Initialization + + public override init(jobId: String) { + super.init(jobId: jobId) + } + + // MARK: - Methods to Override + + /// Gets the API response for the job submission + /// - Returns: Optional API response + public func getApiResponse(authResponse:AuthenticationResponse) async throws -> ApiResponse? { + fatalError("Must be implemented by subclass") + } + + /// Creates the synchronous result for the job submission + /// - Parameter result: Optional API response + /// - Returns: Success result object + public func createSynchronousResult(result: ApiResponse?) async throws -> SmileIDResult.Success { + fatalError("Must be implemented by subclass") + } + + // MARK: - Overridden Methods + + /// Executes the API submission process + /// - Parameter offlineMode: If true, performs offline preparation + /// - Returns: Result of the job submission + public override func executeApiSubmission(offlineMode: Bool) async throws -> SmileIDResult { + if offlineMode { + try await handleOfflinePreparation() + } + + do { + let authResponse = try await executeAuthentication() + let apiResponse = try await getApiResponse(authResponse: authResponse) + let successResult = try await createSynchronousResult(result: apiResponse) + return .success(successResult) + } catch { + return .error(error) + } + } + + /// Creates the success result for the job submission + /// - Parameter didSubmit: Whether the job was submitted to the backend + /// - Returns: Success result object + public override func createSuccessResult(didSubmit: Bool) async throws -> SmileIDResult.Success { + let authResponse = try await executeAuthentication() + let apiResponse = try await getApiResponse(authResponse: authResponse) + return try await createSynchronousResult(result: apiResponse) + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift b/Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift new file mode 100644 index 000000000..850e879bd --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift @@ -0,0 +1,93 @@ +// +// BiometricKYCSubmission.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +public class BiometricKYCSubmission: BaseJobSubmission { + // MARK: - Properties + + private let userId: String + private let allowNewEnroll: Bool + private let livenessFiles: [URL]? + private let selfieFile: URL + private let idInfo: IdInfo + private let extraPartnerParams: [String: String] + private let metadata: Metadata + + // MARK: - Initialization + + public init( + userId: String, + jobId: String, + allowNewEnroll: Bool, + livenessFiles: [URL]?, + selfieFile: URL, + idInfo: IdInfo, + extraPartnerParams: [String: String], + metadata: Metadata + ) { + self.userId = userId + self.allowNewEnroll = allowNewEnroll + self.livenessFiles = livenessFiles + self.selfieFile = selfieFile + self.idInfo = idInfo + self.extraPartnerParams = extraPartnerParams + self.metadata = metadata + super.init(jobId: jobId) + } + + // MARK: - BaseJobSubmission Overrides + + public override func createAuthRequest() -> AuthenticationRequest { + return AuthenticationRequest( + jobType: .biometricKyc, + enrollment: false, + jobId: jobId, + userId: userId, + country: idInfo.country, + idType: idInfo.idType + ) + } + + public override func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { + let partnerParams = authResponse?.partnerParams.copy(extras: extraPartnerParams) ?? + PartnerParams( + jobId: jobId, + userId: userId, + jobType: .biometricKyc, + extras: extraPartnerParams + ) + + return PrepUploadRequest( + partnerParams: partnerParams, + allowNewEnroll: String(allowNewEnroll), + metadata: metadata.items, + timestamp: authResponse?.timestamp ?? "", + signature: authResponse?.signature ?? "" + ) + } + + public override func createUploadRequest(authResponse: AuthenticationResponse?) -> UploadRequest { + let selfieImageInfo = selfieFile.asSelfieImage() + let livenessImageInfo = livenessFiles?.map { $0.asLivenessImage() } ?? [] + + return UploadRequest( + images: [selfieImageInfo] + livenessImageInfo, + idInfo: idInfo.copy(entered: true) + ) + } + + public override func createSuccessResult(didSubmit: Bool) async throws -> SmileIDResult.Success { + let result = BiometricKycResult( + captureData: SelfieCaptureResult( + selfieImage: selfieFile, + livenessImages: livenessFiles + ), + didSubmitJob: didSubmit + ) + + return SmileIDResult.Success(result: result) + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift b/Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift new file mode 100644 index 000000000..1810f3a9b --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift @@ -0,0 +1,57 @@ +// +// DocumentVerificationSubmission.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +class DocumentVerificationSubmission: BaseDocumentVerificationSubmission { + + public init( + jobId: String, + userId: String, + countryCode: String, + allowNewEnroll: Bool, + documentFrontFile: URL, + selfieFile: URL, + documentType: String? = nil, + documentBackFile: URL? = nil, + livenessFiles: [URL]? = nil, + extraPartnerParams: [String: String], + metadata: [Metadatum]? = nil + ) { + super.init( + jobId: jobId, + userId: userId, + jobType: .documentVerification, + countryCode: countryCode, + documentType: documentType, + allowNewEnroll: allowNewEnroll, + documentFrontFile: documentFrontFile, + selfieFile: selfieFile, + documentBackFile: documentBackFile, + livenessFiles: livenessFiles, + extraPartnerParams: extraPartnerParams, + metadata: metadata + ) + } + + + override func createResultInstance( + selfieFile: URL, + documentFrontFile: URL, + livenessFiles: [URL]?, + documentBackFile: URL?, + didSubmitJob: Bool + ) -> DocumentVerificationResult { + return DocumentVerificationResult( + captureData: DocumentCaptureResult( + selfieImage: selfieFile, + livenessImages: livenessFiles, + frontImage: documentFrontFile, + backImage: documentBackFile + ), + didSubmitJob: didSubmitJob + ) + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift b/Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift new file mode 100644 index 000000000..7e8cdf60f --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift @@ -0,0 +1,58 @@ +// +// EnhancedDocumentVerificationSubmission.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +public class EnhancedDocumentVerificationSubmission: BaseDocumentVerificationSubmission { + // MARK: - Initialization + + public init( + jobId: String, + userId: String, + countryCode: String, + documentType: String?, + allowNewEnroll: Bool, + documentFrontFile: URL, + selfieFile: URL, + documentBackFile: URL? = nil, + livenessFiles: [URL]? = nil, + extraPartnerParams: [String: String], + metadata: [Metadatum]? = nil + ) { + super.init( + jobId: jobId, + userId: userId, + jobType: .enhancedDocumentVerification, + countryCode: countryCode, + documentType: documentType, + allowNewEnroll: allowNewEnroll, + documentFrontFile: documentFrontFile, + selfieFile: selfieFile, + documentBackFile: documentBackFile, + livenessFiles: livenessFiles, + extraPartnerParams: extraPartnerParams, + metadata: metadata + ) + } + + + public override func createResultInstance( + selfieFile: URL, + documentFrontFile: URL, + livenessFiles: [URL]?, + documentBackFile: URL?, + didSubmitJob: Bool + ) -> EnhancedDocumentVerificationResult { + return EnhancedDocumentVerificationResult( + captureData: DocumentCaptureResult( + selfieImage: selfieFile, + livenessImages: livenessFiles, + frontImage: documentFrontFile, + backImage: documentBackFile + ), + didSubmitJob: didSubmitJob + ) + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/Results/CaptureResult.swift b/Sources/SmileID/Classes/JobSubmission/Results/CaptureResult.swift new file mode 100644 index 000000000..750644a76 --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/Results/CaptureResult.swift @@ -0,0 +1,45 @@ +// +// CaptureResult.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +public protocol CommonCaptureData { + var selfieImage: URL { get } + var livenessImages: [URL]? { get } +} + +// MARK: - Base Capture Results + +public struct SelfieCaptureResult: CommonCaptureData { + public let selfieImage: URL + public let livenessImages: [URL]? + + public init( + selfieImage: URL, + livenessImages: [URL]? = nil + ) { + self.selfieImage = selfieImage + self.livenessImages = livenessImages + } +} + +public struct DocumentCaptureResult: CommonCaptureData { + public let selfieImage: URL + public let livenessImages: [URL]? + public let frontImage: URL + public let backImage: URL? + + public init( + selfieImage: URL, + livenessImages: [URL]? = nil, + frontImage: URL, + backImage: URL? = nil + ) { + self.selfieImage = selfieImage + self.livenessImages = livenessImages + self.frontImage = frontImage + self.backImage = backImage + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift b/Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift new file mode 100644 index 000000000..d993b73e7 --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift @@ -0,0 +1,81 @@ +// +// SmileIDResults.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +// Specific result types conforming to CaptureResult +public protocol CaptureResult { + var didSubmitJob: Bool { get } +} + +public struct SmartSelfieResult: CaptureResult { + public let captureData: SelfieCaptureResult + public let didSubmitJob: Bool + public let apiResponse: SmartSelfieResponse? + + public init( + captureData: SelfieCaptureResult, + didSubmitJob: Bool, + apiResponse: SmartSelfieResponse? + ) { + self.captureData = captureData + self.didSubmitJob = didSubmitJob + self.apiResponse = apiResponse + } +} + +public struct DocumentVerificationResult: CaptureResult { + public let captureData: DocumentCaptureResult + public let didSubmitJob: Bool + + public init( + captureData: DocumentCaptureResult, + didSubmitJob: Bool + ) { + self.captureData = captureData + self.didSubmitJob = didSubmitJob + } +} + +public struct EnhancedDocumentVerificationResult: CaptureResult { + public let captureData: DocumentCaptureResult + public let didSubmitJob: Bool + + public init( + captureData: DocumentCaptureResult, + didSubmitJob: Bool + ) { + self.captureData = captureData + self.didSubmitJob = didSubmitJob + } +} + +public struct BiometricKycResult: CaptureResult { + public let captureData: SelfieCaptureResult + public let didSubmitJob: Bool + + public init( + captureData: SelfieCaptureResult, + didSubmitJob: Bool + ) { + self.captureData = captureData + self.didSubmitJob = didSubmitJob + } +} + +// MARK: - Generic Result Type + +public enum SmileIDResult { + case success(Success) + case error(Error) + + public struct Success { + public let result: ResultType + + public init(result: ResultType) { + self.result = result + } + } +} diff --git a/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift b/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift new file mode 100644 index 000000000..328ee5cc3 --- /dev/null +++ b/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift @@ -0,0 +1,173 @@ +// +// SelfieSubmission.swift +// Pods +// +// Created by Japhet Ndhlovu on 12/5/24. +// + +public class SelfieSubmission: BaseSynchronousJobSubmission { + // MARK: - Properties + + private let isEnroll: Bool + private let userId: String + private let allowNewEnroll: Bool + private let selfieFile: URL + private let livenessFiles: [URL] + private let extraPartnerParams: [String: String] + private let metadata: Metadata + + // MARK: - Initialization + + public init( + isEnroll: Bool, + userId: String, + jobId: String, + allowNewEnroll: Bool, + selfieFile: URL, + livenessFiles: [URL], + extraPartnerParams: [String: String], + metadata: Metadata + ) { + self.isEnroll = isEnroll + self.userId = userId + self.allowNewEnroll = allowNewEnroll + self.selfieFile = selfieFile + self.livenessFiles = livenessFiles + self.extraPartnerParams = extraPartnerParams + self.metadata = metadata + super.init(jobId: jobId) + } + + // MARK: - Overridden Methods + + public override func createAuthRequest() -> AuthenticationRequest { + return AuthenticationRequest( + jobType: isEnroll ? .smartSelfieEnrollment : .smartSelfieAuthentication, + enrollment: isEnroll, + jobId: jobId, + userId: userId + ) + } + + public override func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { + return PrepUploadRequest( + partnerParams: PartnerParams( + jobId: jobId, + userId: userId, + jobType: isEnroll ? .smartSelfieEnrollment : .smartSelfieAuthentication, + extras: extraPartnerParams + ), + allowNewEnroll: String(allowNewEnroll), + metadata: metadata.items, + timestamp: authResponse?.timestamp ?? "", + signature: authResponse?.signature ?? "" + ) + } + + public override func createUploadRequest(authResponse: AuthenticationResponse?) -> UploadRequest { + return UploadRequest( + images: [selfieFile.asSelfieImage()] + livenessFiles.map { url in + url.asLivenessImage() + } + ) + } + + public override func getApiResponse(authResponse:AuthenticationResponse) async throws -> SmartSelfieResponse? { + var smartSelfieImage: MultipartBody? + var smartSelfieLivenessImages = [MultipartBody]() + if let selfie = try? Data(contentsOf: selfieFile), let media = MultipartBody( + withImage: selfie, + forKey: selfieFile.lastPathComponent, + forName: selfieFile.lastPathComponent + ) { + smartSelfieImage = media + } + if !livenessFiles.isEmpty { + let livenessImageInfos = livenessFiles.compactMap { liveness -> MultipartBody? in + if let data = try? Data(contentsOf: liveness) { + return MultipartBody( + withImage: data, + forKey: liveness.lastPathComponent, + forName: liveness.lastPathComponent + ) + } + return nil + } + + smartSelfieLivenessImages.append(contentsOf: livenessImageInfos.compactMap { $0 }) + } + guard let smartSelfieImage = smartSelfieImage, + !smartSelfieLivenessImages.isEmpty + else { + throw SmileIDError.unknown("Selfie submission failed") + } + + let response = if isEnroll { + try await SmileID.api.doSmartSelfieEnrollment( + signature: authResponse.signature, + timestamp: authResponse.timestamp, + selfieImage: smartSelfieImage, + livenessImages: smartSelfieLivenessImages, + userId: userId, + partnerParams: extraPartnerParams, + callbackUrl: SmileID.callbackUrl, + sandboxResult: nil, + allowNewEnroll: allowNewEnroll, + metadata: metadata + ) + } else { + try await SmileID.api.doSmartSelfieAuthentication( + signature: authResponse.signature, + timestamp: authResponse.timestamp, + userId: userId, + selfieImage: smartSelfieImage, + livenessImages: smartSelfieLivenessImages, + partnerParams: extraPartnerParams, + callbackUrl: SmileID.callbackUrl, + sandboxResult: nil, + metadata: metadata + ) + } + return response + } + + public override func createSynchronousResult(result: SmartSelfieResponse?) async throws -> SmileIDResult.Success { + // Move files from unsubmitted to submitted directories + var selfieFileResult: URL? + var livenessImagesResult: [URL]? + + do { + try LocalStorage.moveToSubmittedJobs(jobId: self.jobId) + selfieFileResult = try LocalStorage.getFileByType( + jobId: jobId, + fileType: FileType.selfie, + submitted: true + ) + livenessImagesResult = try LocalStorage.getFilesByType( + jobId: jobId, + fileType: FileType.liveness, + submitted: true + ) ?? [] + } catch { + print("Error moving job to submitted directory: \(error)") + throw error + } + + guard let selfieFile = selfieFileResult else { + throw SmileIDError.unknown("Selfie submission failed") + } + + let captureResult = SelfieCaptureResult( + selfieImage: selfieFile, + livenessImages: livenessImagesResult + ) + + let finalResult = SmartSelfieResult( + captureData: captureResult, + didSubmitJob: true, + apiResponse: result + ) + + return SmileIDResult.Success(result: finalResult) + } +} diff --git a/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift b/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift index c62ef7b6e..be9ab25cf 100644 --- a/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift +++ b/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift @@ -3,7 +3,7 @@ import Combine import Foundation // swiftlint:disable opening_brace -public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { +public class SelfieViewModel: BaseSubmissionViewModel, ARKitSmileDelegate { // Constants private let intraImageMinDelay: TimeInterval = 0.35 private let noFaceResetDelay: TimeInterval = 3 @@ -18,7 +18,7 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { private let numTotalSteps = 8 // numLivenessImages + 1 selfie image private let livenessImageSize = 320 private let selfieImageSize = 640 - + private let isEnroll: Bool private let userId: String private let jobId: String @@ -27,7 +27,7 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { private let extraPartnerParams: [String: String] private var localMetadata: LocalMetadata private let faceDetector = FaceDetector() - + var cameraManager = CameraManager(orientation: .portrait) var shouldAnalyzeImages = true var lastAutoCaptureTime = Date() @@ -36,15 +36,15 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { var previousHeadYaw = Double.infinity var isSmiling = false var currentlyUsingArKit: Bool { ARFaceTrackingConfiguration.isSupported && !useBackCamera } - + var selfieImage: URL? var livenessImages: [URL] = [] var apiResponse: SmartSelfieResponse? var error: Error? - + private let arKitFramePublisher = PassthroughSubject() private var subscribers = Set() - + // UI Properties @Published var unauthorizedAlert: AlertState? @Published var directive: String = "Instructions.Start" @@ -60,7 +60,7 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { // This is toggled by a Binding didSet { switchCamera() } } - + public init( isEnroll: Bool, userId: String, @@ -77,42 +77,43 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { self.skipApiSubmission = skipApiSubmission self.extraPartnerParams = extraPartnerParams self.localMetadata = localMetadata - + super.init() + + cameraManager.$status .receive(on: DispatchQueue.main) .filter { $0 == .unauthorized } .map { _ in AlertState.cameraUnauthorized } .sink { alert in self.unauthorizedAlert = alert } .store(in: &subscribers) - + cameraManager.sampleBufferPublisher .merge(with: arKitFramePublisher) .throttle(for: 0.35, scheduler: DispatchQueue.global(qos: .userInitiated), latest: true) - // Drop the first ~2 seconds to allow the user to settle in + // Drop the first ~2 seconds to allow the user to settle in .dropFirst(5) .compactMap { $0 } .sink(receiveValue: analyzeImage) .store(in: &subscribers) - + cleanUpSelfieCapture() localMetadata.addMetadata( - useBackCamera ? Metadatum.SelfieImageOrigin(cameraFacing: .backCamera) - : Metadatum.SelfieImageOrigin(cameraFacing: .frontCamera)) + useBackCamera ? Metadatum.SelfieImageOrigin(cameraFacing: .backCamera) : Metadatum.SelfieImageOrigin(cameraFacing: .frontCamera)) } - + let metadataTimerStart = MonotonicTime() - + func updateLocalMetadata(_ newMetadata: LocalMetadata) { localMetadata = newMetadata objectWillChange.send() } - + // swiftlint:disable cyclomatic_complexity func analyzeImage(image: CVPixelBuffer) { let elapsedtime = Date().timeIntervalSince(lastAutoCaptureTime) if !shouldAnalyzeImages || elapsedtime < intraImageMinDelay { return } - + do { try faceDetector.detect(imageBuffer: image) { [self] request, error in if let error { @@ -120,12 +121,12 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { self.error = error return } - + guard let results = request.results as? [VNFaceObservation] else { print("Did not receive the expected [VNFaceObservation]") return } - + if results.count == 0 { DispatchQueue.main.async { self.directive = "Instructions.UnableToDetectFace" } // If no faces are detected for a while, reset the state @@ -137,28 +138,27 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { } selfieImage = nil livenessImages = [] - cleanUpSelfieCapture() } return } - + // Ensure only 1 face is in frame if results.count > 1 { DispatchQueue.main.async { self.directive = "Instructions.MultipleFaces" } return } - + guard let face = results.first else { print("Unexpectedly got an empty face array") return } - + // The coordinate system of the bounding box in VNFaceObservation is such that // the camera view spans [0-1]x[0-1] and the face is within that. Since we don't // need to draw on the camera view, we don't need to convert this to the view's // coordinate system. We can calculate out of bounds and face area directly on this let boundingBox = face.boundingBox - + // Check that the corners of the face bounding box are within frame if boundingBox.minX < minFaceCenteredThreshold || boundingBox.minY < minFaceCenteredThreshold @@ -168,42 +168,42 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { DispatchQueue.main.async { self.directive = "Instructions.PutFaceInOval" } return } - + // image's area is equal to 1. so (bbox area / image area) == bbox area let faceFillRatio = boundingBox.width * boundingBox.height if faceFillRatio < minFaceAreaThreshold { DispatchQueue.main.async { self.directive = "Instructions.MoveCloser" } return } - + if faceFillRatio > maxFaceAreaThreshold { DispatchQueue.main.async { self.directive = "Instructions.MoveFarther" } return } - + if let quality = face.faceCaptureQuality, quality < faceCaptureQualityThreshold { DispatchQueue.main.async { self.directive = "Instructions.Quality" } return } - + let userNeedsToSmile = livenessImages.count > numLivenessImages / 2 - + DispatchQueue.main.async { [self] in directive = userNeedsToSmile ? "Instructions.Smile" : "Instructions.Capturing" } - + // TODO: Use mouth deformation as an alternate signal for non-ARKit capture if userNeedsToSmile, currentlyUsingArKit, !isSmiling { return } - + // Perform the rotation checks *after* changing directive to Capturing -- we don't // want to explicitly tell the user to move their head if !hasFaceRotatedEnough(face: face) { print("Not enough face rotation between captures. Waiting...") return } - + let orientation = currentlyUsingArKit ? CGImagePropertyOrientation.right : .up lastAutoCaptureTime = Date() do { @@ -248,7 +248,7 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { return } } - + func hasFaceRotatedEnough(face: VNFaceObservation) -> Bool { guard let roll = face.roll?.doubleValue, let yaw = face.yaw?.doubleValue else { print("Roll and yaw unexpectedly nil") @@ -262,32 +262,32 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { } let rollDelta = abs(roll - previousHeadRoll) let yawDelta = abs(yaw - previousHeadYaw) - + previousHeadRoll = face.roll?.doubleValue ?? Double.infinity previousHeadYaw = face.yaw?.doubleValue ?? Double.infinity if #available(iOS 15, *) { self.previousHeadPitch = face.pitch?.doubleValue ?? Double.infinity } - + return didPitchChange || rollDelta > faceRollThreshold || yawDelta > faceRotationThreshold } - + func onSmiling(isSmiling: Bool) { self.isSmiling = isSmiling } - + func onARKitFrame(frame: ARFrame) { arKitFramePublisher.send(frame.capturedImage) } - + func switchCamera() { cameraManager.switchCamera(to: useBackCamera ? .back : .front) localMetadata.metadata.removeAllOfType(Metadatum.SelfieImageOrigin.self) localMetadata.addMetadata( useBackCamera ? Metadatum.SelfieImageOrigin(cameraFacing: .backCamera) - : Metadatum.SelfieImageOrigin(cameraFacing: .frontCamera)) + : Metadatum.SelfieImageOrigin(cameraFacing: .frontCamera)) } - + public func onSelfieRejected() { DispatchQueue.main.async { self.captureProgress = 0 @@ -301,7 +301,7 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { localMetadata.metadata.removeAllOfType(Metadatum.SelfieImageOrigin.self) localMetadata.metadata.removeAllOfType(Metadatum.SelfieCaptureDuration.self) } - + func cleanUpSelfieCapture() { do { try LocalStorage.deleteLivenessAndSelfieFiles(at: [jobId]) @@ -309,7 +309,7 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { debugPrint(error.localizedDescription) } } - + func onRetry() { // If selfie file is present, all captures were completed, so we're retrying a network issue if selfieImage != nil, livenessImages.count == numLivenessImages { @@ -319,156 +319,18 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { DispatchQueue.main.async { self.processingState = nil } } } - + public func submitJob() { localMetadata.addMetadata(Metadatum.SelfieCaptureDuration(duration: metadataTimerStart.elapsedTime())) - if skipApiSubmission { - DispatchQueue.main.async { self.processingState = .success } + guard let selfie = self.selfieImage, + livenessImages.count == numLivenessImages + else { + handleError(error: SmileIDError.unknown("Selfie capture failed")) return } - DispatchQueue.main.async { self.processingState = .inProgress } - Task { - do { - guard let selfieImage, livenessImages.count == numLivenessImages else { - throw SmileIDError.unknown("Selfie capture failed") - } - let jobType = isEnroll ? JobType.smartSelfieEnrollment : JobType.smartSelfieAuthentication - let authRequest = AuthenticationRequest( - jobType: jobType, - enrollment: isEnroll, - jobId: jobId, - userId: userId - ) - if SmileID.allowOfflineMode { - try LocalStorage.saveOfflineJob( - jobId: jobId, - userId: userId, - jobType: jobType, - enrollment: isEnroll, - allowNewEnroll: allowNewEnroll, - localMetadata: localMetadata, - partnerParams: extraPartnerParams - ) - } - let authResponse = try await SmileID.api.authenticate(request: authRequest) - - var smartSelfieLivenessImages = [MultipartBody]() - var smartSelfieImage: MultipartBody? - if let selfie = try? Data(contentsOf: selfieImage), let media = MultipartBody( - withImage: selfie, - forKey: selfieImage.lastPathComponent, - forName: selfieImage.lastPathComponent - ) { - smartSelfieImage = media - } - if !livenessImages.isEmpty { - let livenessImageInfos = livenessImages.compactMap { liveness -> MultipartBody? in - if let data = try? Data(contentsOf: liveness) { - return MultipartBody( - withImage: data, - forKey: liveness.lastPathComponent, - forName: liveness.lastPathComponent - ) - } - return nil - } - - smartSelfieLivenessImages.append(contentsOf: livenessImageInfos.compactMap { $0 }) - } - guard let smartSelfieImage = smartSelfieImage, - smartSelfieLivenessImages.count == numLivenessImages - else { - throw SmileIDError.unknown("Selfie capture failed") - } - - let response = if isEnroll { - try await SmileID.api.doSmartSelfieEnrollment( - signature: authResponse.signature, - timestamp: authResponse.timestamp, - selfieImage: smartSelfieImage, - livenessImages: smartSelfieLivenessImages, - userId: userId, - partnerParams: extraPartnerParams, - callbackUrl: SmileID.callbackUrl, - sandboxResult: nil, - allowNewEnroll: allowNewEnroll, - metadata: localMetadata.metadata - ) - } else { - try await SmileID.api.doSmartSelfieAuthentication( - signature: authResponse.signature, - timestamp: authResponse.timestamp, - userId: userId, - selfieImage: smartSelfieImage, - livenessImages: smartSelfieLivenessImages, - partnerParams: extraPartnerParams, - callbackUrl: SmileID.callbackUrl, - sandboxResult: nil, - metadata: localMetadata.metadata - ) - } - apiResponse = response - do { - try LocalStorage.moveToSubmittedJobs(jobId: self.jobId) - self.selfieImage = try LocalStorage.getFileByType( - jobId: jobId, - fileType: FileType.selfie, - submitted: true - ) - self.livenessImages = try LocalStorage.getFilesByType( - jobId: jobId, - fileType: FileType.liveness, - submitted: true - ) ?? [] - } catch { - print("Error moving job to submitted directory: \(error)") - self.error = error - } - DispatchQueue.main.async { self.processingState = .success } - } catch let error as SmileIDError { - do { - let didMove = try LocalStorage.handleOfflineJobFailure( - jobId: self.jobId, - error: error - ) - if didMove { - self.selfieImage = try LocalStorage.getFileByType( - jobId: jobId, - fileType: FileType.selfie, - submitted: true - ) - self.livenessImages = try LocalStorage.getFilesByType( - jobId: jobId, - fileType: FileType.liveness, - submitted: true - ) ?? [] - } - } catch { - print("Error moving job to submitted directory: \(error)") - self.error = error - return - } - if SmileID.allowOfflineMode, SmileIDError.isNetworkFailure(error: error) { - DispatchQueue.main.async { - self.errorMessageRes = "Offline.Message" - self.processingState = .success - } - } else { - print("Error submitting job: \(error)") - let (errorMessageRes, errorMessage) = toErrorMessage(error: error) - self.error = error - self.errorMessageRes = errorMessageRes - self.errorMessage = errorMessage - DispatchQueue.main.async { self.processingState = .error } - } - } catch { - print("Error submitting job: \(error)") - self.error = error - DispatchQueue.main.async { self.processingState = .error } - } - } + submitJob(jobId: jobId, skipApiSubmission: skipApiSubmission, offlineMode: SmileID.allowOfflineMode) } - + public func onFinished(callback: SmartSelfieResultDelegate) { if let selfieImage = selfieImage, let selfiePath = getRelativePath(from: selfieImage), @@ -476,7 +338,7 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { !livenessImages.contains(where: { getRelativePath(from: $0) == nil }) { let livenessImagesPaths = livenessImages.compactMap { getRelativePath(from: $0) } - + callback.didSucceed( selfieImage: selfiePath, livenessImages: livenessImagesPaths, @@ -488,9 +350,71 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { callback.didError(error: SmileIDError.unknown("Unknown error")) } } - + func openSettings() { guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(settingsURL) } + + + public override func createSubmission() throws -> BaseJobSubmission { + guard let selfieImage = selfieImage, + livenessImages.count == numLivenessImages else { + throw SmileIDError.unknown("Selfie images missing") + } + return SelfieSubmission( + isEnroll: isEnroll, + userId: userId, + jobId: jobId, + allowNewEnroll: allowNewEnroll, + selfieFile: selfieImage, + livenessFiles: livenessImages, + extraPartnerParams: extraPartnerParams, + metadata: localMetadata.metadata + ) + } + + public override func triggerProcessingState() { + DispatchQueue.main.async { self.processingState = .inProgress } + } + + public override func handleSuccess(data: SmartSelfieResult) { + apiResponse = data.apiResponse + DispatchQueue.main.async { self.processingState = .success } + } + + public override func handleError(error: Error) { + if let smileError = error as? SmileIDError { + print("Error submitting job: \(error)") + let (errorMessageRes, errorMessage) = toErrorMessage(error: smileError) + self.error = error + self.errorMessageRes = errorMessageRes + self.errorMessage = errorMessage + DispatchQueue.main.async { self.processingState = .error } + } else { + print("Error submitting job: \(error)") + self.error = error + DispatchQueue.main.async { self.processingState = .error } + } + } + + public override func handleSubmissionFiles(jobId: String) throws { + self.selfieImage = try LocalStorage.getFileByType( + jobId: jobId, + fileType: FileType.selfie, + submitted: true + ) + self.livenessImages = try LocalStorage.getFilesByType( + jobId: jobId, + fileType: FileType.liveness, + submitted: true + ) ?? [] + } + + public override func handleOfflineSuccess() { + DispatchQueue.main.async { + self.errorMessageRes = "Offline.Message" + self.processingState = .success + } + } } From 046089acdd9dfdc13b70daccd43b1e7eac8916a9 Mon Sep 17 00:00:00 2001 From: JNdhlovu Date: Tue, 10 Dec 2024 15:42:37 +0200 Subject: [PATCH 2/5] feat: ios submissions for biometric kyc --- .../OrchestratedBiometricKycViewModel.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift index 0ef7c2776..e46fe3ae3 100644 --- a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift +++ b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift @@ -244,29 +244,28 @@ internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel BaseJobSubmission { - guard let selfieImage = selfieImage, - !livenessFiles?.isEmpty == numLivenessImages else { + guard let selfieImage = selfieFile, + livenessFiles != nil else { throw SmileIDError.unknown("Selfie images missing") } return BiometricKYCSubmission( userId: userId, jobId: jobId, allowNewEnroll: allowNewEnroll, + livenessFiles: livenessFiles, selfieFile: selfieImage, idInfo: idInfo, - livenessFiles: livenessFiles, extraPartnerParams: extraPartnerParams, metadata: localMetadata.metadata ) } public override func triggerProcessingState() { - DispatchQueue.main.async { self.processingState = .inProgress } + DispatchQueue.main.async { self.step = .processing(ProcessingState.inProgress) } } public override func handleSuccess(data: BiometricKycResult) { - apiResponse = data.apiResponse - DispatchQueue.main.async { self.processingState = .success } + DispatchQueue.main.async { self.step = .processing(ProcessingState.success) } } public override func handleError(error: Error) { @@ -276,11 +275,11 @@ internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel) } } } From 2cee84d14afc4f3e3c2964f92ce95e9b02edca98 Mon Sep 17 00:00:00 2001 From: JNdhlovu Date: Wed, 11 Dec 2024 15:15:04 +0200 Subject: [PATCH 3/5] feat: ios docv --- .../OrchestratedBiometricKycViewModel.swift | 15 +- ...stratedDocumentVerificationViewModel.swift | 135 +----------------- .../Base/BaseSynchronousJobSubmission.swift | 7 +- .../JobSubmission/SelfieSubmission.swift | 36 ++--- 4 files changed, 30 insertions(+), 163 deletions(-) diff --git a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift index e46fe3ae3..927e78c98 100644 --- a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift +++ b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift @@ -75,7 +75,6 @@ internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel) + self.step = .processing(ProcessingState.success) } } } diff --git a/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift b/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift index 75425e7a1..32ff6e9da 100644 --- a/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift +++ b/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift @@ -8,7 +8,7 @@ enum DocumentCaptureFlow: Equatable { case processing(ProcessingState) } -class IOrchestratedDocumentVerificationViewModel: ObservableObject { +class IOrchestratedDocumentVerificationViewModel: BaseSubmissionViewModel { // Input properties let userId: String let jobId: String @@ -163,131 +163,7 @@ class IOrchestratedDocumentVerificationViewModel: ObservableObj if let livenessFiles { allFiles.append(contentsOf: livenessFiles) } - let info = try LocalStorage.createInfoJsonFile( - jobId: jobId, - idInfo: IdInfo(country: countryCode), - documentFront: frontDocumentUrl, - documentBack: backDocumentUrl, - selfie: selfieFile, - livenessImages: livenessFiles - ) - allFiles.append(info) - let zipData = try LocalStorage.zipFiles(at: allFiles) - self.savedFiles = DocumentCaptureResultStore( - allFiles: allFiles, - documentFront: frontDocumentUrl, - documentBack: backDocumentUrl, - selfie: selfieFile, - livenessImages: livenessFiles ?? [] - ) - if skipApiSubmission { - DispatchQueue.main.async { self.step = .processing(.success) } - return - } - let authRequest = AuthenticationRequest( - jobType: jobType, - enrollment: false, - jobId: jobId, - userId: userId - ) - if SmileID.allowOfflineMode { - try LocalStorage.saveOfflineJob( - jobId: jobId, - userId: userId, - jobType: jobType, - enrollment: false, - allowNewEnroll: allowNewEnroll, - localMetadata: localMetadata, - partnerParams: extraPartnerParams - ) - } - let authResponse = try await SmileID.api.authenticate(request: authRequest) - let prepUploadRequest = PrepUploadRequest( - partnerParams: authResponse.partnerParams.copy(extras: self.extraPartnerParams), - allowNewEnroll: String(allowNewEnroll), // TODO: - Fix when Michael changes this to boolean - metadata: localMetadata.metadata.items, - timestamp: authResponse.timestamp, - signature: authResponse.signature - ) - let prepUploadResponse: PrepUploadResponse - do { - prepUploadResponse = try await SmileID.api.prepUpload( - request: prepUploadRequest - ) - } catch let error as SmileIDError { - switch error { - case .api("2215", _): - prepUploadResponse = try await SmileID.api.prepUpload( - request: prepUploadRequest.copy(retry: "true") - ) - default: - throw error - } - } - let _ = try await SmileID.api.upload( - zip: zipData, - to: prepUploadResponse.uploadUrl - ) - didSubmitJob = true - do { - try LocalStorage.moveToSubmittedJobs(jobId: self.jobId) - self.selfieFile = - try LocalStorage.getFileByType( - jobId: jobId, - fileType: FileType.selfie, - submitted: true - ) ?? selfieFile - self.livenessFiles = - try LocalStorage.getFilesByType( - jobId: jobId, - fileType: FileType.liveness, - submitted: true - ) ?? [] - } catch { - print("Error moving job to submitted directory: \(error)") - self.onError(error: error) - return - } - DispatchQueue.main.async { self.step = .processing(.success) } - } catch let error as SmileIDError { - do { - let didMove = try LocalStorage.handleOfflineJobFailure( - jobId: self.jobId, - error: error - ) - if didMove { - self.selfieFile = - try LocalStorage.getFileByType( - jobId: jobId, - fileType: FileType.selfie, - submitted: true - ) ?? selfieFile - self.livenessFiles = - try LocalStorage.getFilesByType( - jobId: jobId, - fileType: FileType.liveness, - submitted: true - ) ?? [] - } - } catch { - print("Error moving job to submitted directory: \(error)") - self.onError(error: error) - return - } - if SmileID.allowOfflineMode, SmileIDError.isNetworkFailure(error: error) { - didSubmitJob = true - DispatchQueue.main.async { - self.errorMessageRes = "Offline.Message" - self.step = .processing(.success) - } - } else { - didSubmitJob = false - print("Error submitting job: \(error)") - self.onError(error: error) - let (errorMessageRes, errorMessage) = toErrorMessage(error: error) - self.errorMessageRes = errorMessageRes - self.errorMessage = errorMessage - } + } catch { didSubmitJob = false print("Error submitting job: \(error)") @@ -329,7 +205,7 @@ extension IOrchestratedDocumentVerificationViewModel: SmartSelfieResultDelegate // swiftlint:disable opening_brace class OrchestratedDocumentVerificationViewModel: - IOrchestratedDocumentVerificationViewModel + IOrchestratedDocumentVerificationViewModel { override func onFinished(delegate: DocumentVerificationResultDelegate) { if let savedFiles, @@ -354,9 +230,8 @@ class OrchestratedDocumentVerificationViewModel: } // swiftlint:disable opening_brace -class OrchestratedEnhancedDocumentVerificationViewModel: - IOrchestratedDocumentVerificationViewModel< - EnhancedDocumentVerificationResultDelegate, EnhancedDocumentVerificationJobResult +class OrchestratedEnhancedDocumentVerificationViewModel:IOrchestratedDocumentVerificationViewModel< +EnhancedDocumentVerificationResultDelegate, EnhancedDocumentVerificationJobResult,EnhancedDocumentVerificationResult > { override func onFinished(delegate: EnhancedDocumentVerificationResultDelegate) { diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift index b7a94bafd..94a0a12c6 100644 --- a/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift @@ -51,8 +51,11 @@ public class BaseSynchronousJobSubmission SmileIDResult.Success { - let authResponse = try await executeAuthentication() - let apiResponse = try await getApiResponse(authResponse: authResponse) + var apiResponse : ApiResponse? = nil + if(didSubmit){ + let authResponse = try await executeAuthentication() + apiResponse = try await getApiResponse(authResponse: authResponse) + } return try await createSynchronousResult(result: apiResponse) } } diff --git a/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift b/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift index 328ee5cc3..09be96198 100644 --- a/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift @@ -133,32 +133,32 @@ public class SelfieSubmission: BaseSynchronousJobSubmission SmileIDResult.Success { // Move files from unsubmitted to submitted directories - var selfieFileResult: URL? - var livenessImagesResult: [URL]? + var selfieFileResult = self.selfieFile + var livenessImagesResult = self.livenessFiles do { - try LocalStorage.moveToSubmittedJobs(jobId: self.jobId) - selfieFileResult = try LocalStorage.getFileByType( - jobId: jobId, - fileType: FileType.selfie, - submitted: true - ) - livenessImagesResult = try LocalStorage.getFilesByType( - jobId: jobId, - fileType: FileType.liveness, - submitted: true - ) ?? [] + if let result = result{ + try LocalStorage.moveToSubmittedJobs(jobId: self.jobId) + guard let selfieFileResult = try LocalStorage.getFileByType( + jobId: jobId, + fileType: FileType.selfie, + submitted: true + ) else{ + throw SmileIDError.unknown("Selfie file not found") + } + livenessImagesResult = try LocalStorage.getFilesByType( + jobId: jobId, + fileType: FileType.liveness, + submitted: true + ) ?? [] + } } catch { print("Error moving job to submitted directory: \(error)") throw error } - guard let selfieFile = selfieFileResult else { - throw SmileIDError.unknown("Selfie submission failed") - } - let captureResult = SelfieCaptureResult( - selfieImage: selfieFile, + selfieImage: selfieFileResult, livenessImages: livenessImagesResult ) From d22c8c74ecc0334dd68258ee1aed1b4806eddb18 Mon Sep 17 00:00:00 2001 From: JNdhlovu Date: Thu, 12 Dec 2024 14:32:31 +0200 Subject: [PATCH 4/5] feat: job submissions --- .../OrchestratedBiometricKycViewModel.swift | 122 +++++------------- ...stratedDocumentVerificationViewModel.swift | 100 +++++++++++++- ...chestratedDocumentVerificationScreen.swift | 6 +- .../BaseDocumentVerificationSubmission.swift | 60 +++++---- 4 files changed, 164 insertions(+), 124 deletions(-) diff --git a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift index 927e78c98..4f9520583 100644 --- a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift +++ b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift @@ -1,12 +1,12 @@ import Combine import Foundation -internal enum BiometricKycStep { +enum BiometricKycStep { case selfie case processing(ProcessingState) } -internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel { +class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel { // MARK: - Input Properties private let userId: String @@ -18,8 +18,8 @@ internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel Data { - var allFiles = [URL]() - let infoJson = try LocalStorage.createInfoJsonFile( - jobId: jobId, - idInfo: idInfo.copy(entered: true), - selfie: selfieFile, - livenessImages: livenessFiles - ) - if let selfieFile { - allFiles.append(contentsOf: [selfieFile, infoJson]) - } - if let livenessFiles { - allFiles.append(contentsOf: livenessFiles) - } - return try LocalStorage.zipFiles(at: allFiles) - } - - private func authenticate() async throws -> AuthenticationResponse { - let authRequest = AuthenticationRequest( - jobType: .biometricKyc, - enrollment: false, - jobId: jobId, - userId: userId, - country: idInfo.country, - idType: idInfo.idType - ) - - if SmileID.allowOfflineMode { - try saveOfflineJobIfAllowed() - } - - return try await SmileID.api.authenticate(request: authRequest) - } - private func saveOfflineJobIfAllowed() throws { try LocalStorage.saveOfflineJob( jobId: jobId, @@ -156,38 +129,8 @@ internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel PrepUploadResponse { - let prepUploadRequest = PrepUploadRequest( - partnerParams: authResponse.partnerParams.copy(extras: extraPartnerParams), - allowNewEnroll: String(allowNewEnroll), // TODO: - Fix when Michael changes this to boolean - metadata: localMetadata.metadata.items, - timestamp: authResponse.timestamp, - signature: authResponse.signature - ) - do { - return try await SmileID.api.prepUpload( - request: prepUploadRequest - ) - } catch let error as SmileIDError { - guard case let .api(errorCode, _) = error, - errorCode == "2215" - else { - throw error - } - return try await SmileID.api.prepUpload( - request: prepUploadRequest.copy(retry: "true")) - } - } - - private func uploadFiles(zipData: Data, uploadUrl: String) async throws { - try await SmileID.api.upload( - zip: zipData, - to: uploadUrl - ) - } - private func moveJobToSubmittedDirectory() throws { - try LocalStorage.moveToSubmittedJobs(jobId: self.jobId) + try LocalStorage.moveToSubmittedJobs(jobId: jobId) } private func updateStep(_ newStep: BiometricKycStep) { @@ -209,7 +152,7 @@ internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel BaseJobSubmission { + + override public func createSubmission() throws -> BaseJobSubmission { guard let selfieImage = selfieFile, - livenessFiles != nil else { + livenessFiles != nil + else { throw SmileIDError.unknown("Selfie images missing") } return BiometricKYCSubmission( @@ -248,16 +192,16 @@ internal class OrchestratedBiometricKycViewModel: BaseSubmissionViewModel: BaseSubmissionViewModel { +class IOrchestratedDocumentVerificationViewModel: BaseSubmissionViewModel { // Input properties let userId: String let jobId: String @@ -163,7 +163,15 @@ class IOrchestratedDocumentVerificationViewModel + IOrchestratedDocumentVerificationViewModel { + override func createSubmission() throws -> BaseJobSubmission { + guard let selfieImage = selfieFile, + livenessFiles != nil + else { + throw SmileIDError.unknown("Selfie image missing") + } + guard let documentFront = try LocalStorage.getFileByType(jobId: jobId, fileType: .documentFront) else { + throw SmileIDError.unknown("Document image missing") + } + let documentBack = try LocalStorage.getFileByType(jobId: jobId, fileType: .documentBack) + return DocumentVerificationSubmission(jobId: jobId, userId: userId, countryCode: countryCode, + allowNewEnroll: allowNewEnroll, documentFrontFile: documentFront, + selfieFile: selfieImage, documentBackFile: documentBack, + livenessFiles: livenessFiles, + extraPartnerParams: extraPartnerParams) + } + override func onFinished(delegate: DocumentVerificationResultDelegate) { if let savedFiles, let selfiePath = getRelativePath(from: selfieFile), @@ -230,10 +299,27 @@ class OrchestratedDocumentVerificationViewModel: } // swiftlint:disable opening_brace -class OrchestratedEnhancedDocumentVerificationViewModel:IOrchestratedDocumentVerificationViewModel< -EnhancedDocumentVerificationResultDelegate, EnhancedDocumentVerificationJobResult,EnhancedDocumentVerificationResult - > -{ +class OrchestratedEnhancedDocumentVerificationViewModel: IOrchestratedDocumentVerificationViewModel + { + override func createSubmission() throws -> BaseJobSubmission { + guard let selfieImage = selfieFile, + livenessFiles != nil + else { + throw SmileIDError.unknown("Selfie image missing") + } + guard let documentFront = try LocalStorage.getFileByType(jobId: jobId, fileType: .documentFront) else { + throw SmileIDError.unknown("Document image missing") + } + let documentBack = try LocalStorage.getFileByType(jobId: jobId, fileType: .documentBack) + return EnhancedDocumentVerificationSubmission(jobId: jobId, userId: userId, countryCode: countryCode, + documentType: documentType, allowNewEnroll: allowNewEnroll, + documentFrontFile: documentFront, selfieFile: selfieImage, + documentBackFile: documentBack, livenessFiles: livenessFiles, + extraPartnerParams: extraPartnerParams) + } + override func onFinished(delegate: EnhancedDocumentVerificationResultDelegate) { if let savedFiles, let selfiePath = getRelativePath(from: selfieFile), diff --git a/Sources/SmileID/Classes/DocumentVerification/View/OrchestratedDocumentVerificationScreen.swift b/Sources/SmileID/Classes/DocumentVerification/View/OrchestratedDocumentVerificationScreen.swift index c6695876b..62103d26d 100644 --- a/Sources/SmileID/Classes/DocumentVerification/View/OrchestratedDocumentVerificationScreen.swift +++ b/Sources/SmileID/Classes/DocumentVerification/View/OrchestratedDocumentVerificationScreen.swift @@ -104,7 +104,7 @@ struct OrchestratedEnhancedDocumentVerificationScreen: View { } } -private struct IOrchestratedDocumentVerificationScreen: View { +private struct IOrchestratedDocumentVerificationScreen: View { let countryCode: String let documentType: String? let captureBothSides: Bool @@ -120,7 +120,7 @@ private struct IOrchestratedDocumentVerificationScreen: View { let skipApiSubmission: Bool var extraPartnerParams: [String: String] let onResult: T - @ObservedObject var viewModel: IOrchestratedDocumentVerificationViewModel + @ObservedObject var viewModel: IOrchestratedDocumentVerificationViewModel init( countryCode: String, @@ -138,7 +138,7 @@ private struct IOrchestratedDocumentVerificationScreen: View { skipApiSubmission: Bool, extraPartnerParams: [String: String], onResult: T, - viewModel: IOrchestratedDocumentVerificationViewModel + viewModel: IOrchestratedDocumentVerificationViewModel ) { self.countryCode = countryCode self.documentType = documentType diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift index 59b395813..8ab059b0d 100644 --- a/Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseDocumentVerificationSubmission.swift @@ -7,7 +7,7 @@ public class BaseDocumentVerificationSubmission: BaseJobSubmission { // MARK: - Properties - + private let userId: String private let jobType: JobType private let countryCode: String @@ -19,9 +19,9 @@ public class BaseDocumentVerificationSubmission: Base private let livenessFiles: [URL]? private let extraPartnerParams: [String: String] private let metadata: [Metadatum]? - + // MARK: - Initialization - + public init( jobId: String, userId: String, @@ -49,10 +49,10 @@ public class BaseDocumentVerificationSubmission: Base self.metadata = metadata super.init(jobId: jobId) } - + // MARK: - Overridden Methods - - public override func createAuthRequest() -> AuthenticationRequest { + + override public func createAuthRequest() -> AuthenticationRequest { return AuthenticationRequest( jobType: jobType, enrollment: false, @@ -62,11 +62,11 @@ public class BaseDocumentVerificationSubmission: Base idType: documentType ) } - - public override func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { - let partnerParams = authResponse?.partnerParams.copy(extras: extraPartnerParams) ?? PartnerParams(jobId: jobId, userId: userId, jobType: jobType, extras: extraPartnerParams) - - + + override public func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { + let partnerParams = authResponse?.partnerParams.copy(extras: extraPartnerParams) + ?? PartnerParams(jobId: jobId, userId: userId, jobType: jobType, extras: extraPartnerParams) + return PrepUploadRequest( partnerParams: partnerParams, allowNewEnroll: String(allowNewEnroll), @@ -75,24 +75,34 @@ public class BaseDocumentVerificationSubmission: Base signature: authResponse?.signature ?? "" ) } - - public override func createUploadRequest(authResponse: AuthenticationResponse?) -> UploadRequest { + + override public func createUploadRequest(authResponse _: AuthenticationResponse?) -> UploadRequest { let frontImageInfo = documentFrontFile.asDocumentFrontImage() let backImageInfo = documentBackFile?.asDocumentBackImage() let selfieImageInfo = selfieFile.asSelfieImage() let livenessImageInfo = livenessFiles?.map { $0.asLivenessImage() } ?? [] - + return UploadRequest( images: [frontImageInfo] + - (backImageInfo.map { [$0] } ?? []) + - [selfieImageInfo] + - livenessImageInfo, - idInfo : IdInfo(country: countryCode, idType: documentType) + (backImageInfo.map { [$0] } ?? []) + + [selfieImageInfo] + + livenessImageInfo, + idInfo: IdInfo(country: countryCode, idType: documentType) ) } - + + override public func createSuccessResult(didSubmit: Bool) async throws -> + SmileIDResult.Success { + let result = createResultInstance(selfieFile: selfieFile, + documentFrontFile: documentFrontFile, + livenessFiles: livenessFiles, + documentBackFile: documentBackFile, + didSubmitJob: didSubmit) + return SmileIDResult.Success(result: result) + } + // MARK: - Abstract Methods - + /// Creates the result instance for the document verification submission /// - Parameters: /// - selfieFile: The selfie file URL @@ -102,11 +112,11 @@ public class BaseDocumentVerificationSubmission: Base /// - didSubmitJob: Whether the job was submitted /// - Returns: Result instance of type ResultType open func createResultInstance( - selfieFile: URL, - documentFrontFile: URL, - livenessFiles: [URL]?, - documentBackFile: URL?, - didSubmitJob: Bool + selfieFile _: URL, + documentFrontFile _: URL, + livenessFiles _: [URL]?, + documentBackFile _: URL?, + didSubmitJob _: Bool ) -> ResultType { fatalError("Must be implemented by subclass") } From 4ed768bd222591f2306852d6bd11ed243a91a6a6 Mon Sep 17 00:00:00 2001 From: JNdhlovu Date: Mon, 6 Jan 2025 13:40:08 +0200 Subject: [PATCH 5/5] feat: lint files --- .../Classes/Helpers/LocalStorage.swift | 82 +++++------ .../Base/BaseJobSubmission.swift | 49 ++++--- .../Base/BaseSubmissionViewModel.swift | 54 +++---- .../Base/BaseSynchronousJobSubmission.swift | 30 ++-- .../BiometricKYCSubmission.swift | 42 +++--- .../DocumentVerificationSubmission.swift | 58 ++++---- ...hancedDocumentVerificationSubmission.swift | 5 +- .../Results/SmileIDResults.swift | 12 +- .../JobSubmission/SelfieSubmission.swift | 50 +++---- .../SelfieCapture/SelfieViewModel.swift | 134 +++++++++--------- 10 files changed, 257 insertions(+), 259 deletions(-) diff --git a/Sources/SmileID/Classes/Helpers/LocalStorage.swift b/Sources/SmileID/Classes/Helpers/LocalStorage.swift index 410810f40..56b4a3094 100644 --- a/Sources/SmileID/Classes/Helpers/LocalStorage.swift +++ b/Sources/SmileID/Classes/Helpers/LocalStorage.swift @@ -9,7 +9,7 @@ public class LocalStorage { private static let previewImageName = "PreviewImage.jpg" private static let jsonEncoder = JSONEncoder() private static let jsonDecoder = JSONDecoder() - + static var defaultDirectory: URL { get throws { let documentDirectory = try FileManager.default.url( @@ -21,19 +21,19 @@ public class LocalStorage { return documentDirectory.appendingPathComponent(defaultFolderName) } } - + static var unsubmittedJobDirectory: URL { get throws { try defaultDirectory.appendingPathComponent(unsubmittedFolderName) } } - + static var submittedJobDirectory: URL { get throws { try defaultDirectory.appendingPathComponent(submittedFolderName) } } - + private static func createSmileFile( to folder: String, name: String, @@ -43,25 +43,25 @@ public class LocalStorage { let destinationFolder = try unsubmittedJobDirectory.appendingPathComponent(folder) return try write(data, to: destinationFolder.appendingPathComponent(name)) } - + private static func filename(for name: String) -> String { "\(name)_\(Date().millisecondsSince1970).jpg" } - + public static func createSelfieFile( jobId: String, selfieFile data: Data ) throws -> URL { try createSmileFile(to: jobId, name: filename(for: FileType.selfie.name), file: data) } - + public static func createLivenessFile( jobId: String, livenessFile data: Data ) throws -> URL { try createSmileFile(to: jobId, name: filename(for: FileType.liveness.name), file: data) } - + public static func createDocumentFile( jobId: String, fileType: FileType, @@ -69,7 +69,7 @@ public class LocalStorage { ) throws -> URL { try createSmileFile(to: jobId, name: filename(for: fileType.name), file: data) } - + public static func getFileByType( jobId: String, fileType: FileType, @@ -78,7 +78,7 @@ public class LocalStorage { let contents = try getDirectoryContents(jobId: jobId, submitted: submitted) return contents.first(where: { $0.lastPathComponent.contains(fileType.name) }) } - + public static func getFilesByType( jobId: String, fileType: FileType, @@ -87,7 +87,7 @@ public class LocalStorage { let contents = try getDirectoryContents(jobId: jobId, submitted: submitted) return contents.filter { $0.lastPathComponent.contains(fileType.name) } } - + static func createInfoJsonFile( jobId: String, idInfo: IdInfo? = nil, @@ -130,14 +130,14 @@ public class LocalStorage { )) return try createSmileFile(to: jobId, name: "info.json", file: data) } - + static func getInfoJsonFile( jobId: String ) throws -> URL { let contents = try getDirectoryContents(jobId: jobId) return contents.first(where: { $0.lastPathComponent == "info.json" })! } - + static func createPrepUploadFile( jobId: String, prepUpload: PrepUploadRequest @@ -145,7 +145,7 @@ public class LocalStorage { let data = try jsonEncoder.encode(prepUpload) return try createSmileFile(to: jobId, name: "prep_upload.json", file: data) } - + static func fetchPrepUploadFile( jobId: String ) throws -> PrepUploadRequest { @@ -154,7 +154,7 @@ public class LocalStorage { let data = try Data(contentsOf: preupload!) return try jsonDecoder.decode(PrepUploadRequest.self, from: data) } - + static func createAuthenticationRequestFile( jobId: String, authentationRequest: AuthenticationRequest @@ -162,7 +162,7 @@ public class LocalStorage { let data = try jsonEncoder.encode(authentationRequest) return try createSmileFile(to: jobId, name: "authentication_request.json", file: data) } - + static func fetchAuthenticationRequestFile( jobId: String ) throws -> AuthenticationRequest { @@ -171,7 +171,7 @@ public class LocalStorage { let data = try Data(contentsOf: authenticationrequest!) return try jsonDecoder.decode(AuthenticationRequest.self, from: data) } - + static func saveOfflineJob( jobId: String, userId: String, @@ -209,7 +209,7 @@ public class LocalStorage { ) } } - + private static func write(_ data: Data, to url: URL, options completeFileProtection: Bool = true) throws -> URL { let directoryURL = url.deletingLastPathComponent() try fileManager.createDirectory( @@ -226,11 +226,11 @@ public class LocalStorage { return url } } - + private static func createDirectory(at url: URL) throws { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) } - + private static func getDirectoryContents( jobId: String, submitted: Bool = false @@ -239,7 +239,7 @@ public class LocalStorage { let folderPathURL = baseDirectory.appendingPathComponent(jobId) return try fileManager.contentsOfDirectory(at: folderPathURL, includingPropertiesForKeys: nil) } - + static func getUnsubmittedJobs() -> [String] { do { return try fileManager.contentsOfDirectory(atPath: unsubmittedJobDirectory.relativePath) @@ -248,7 +248,7 @@ public class LocalStorage { return [] } } - + static func getSubmittedJobs() -> [String] { do { return try fileManager.contentsOfDirectory(atPath: submittedJobDirectory.relativePath) @@ -257,7 +257,7 @@ public class LocalStorage { return [] } } - + static func moveToSubmittedJobs(jobId: String) throws { try createDirectory(at: submittedJobDirectory) let unsubmittedFileDirectory = try unsubmittedJobDirectory.appendingPathComponent(jobId) @@ -267,7 +267,7 @@ public class LocalStorage { } try fileManager.moveItem(at: unsubmittedFileDirectory, to: submittedFileDirectory) } - + /// Moves files from unsubmitted to submitted when not in Offline Mode, or if it was not a /// network error /// Returns: true if files were moved @@ -282,7 +282,7 @@ public class LocalStorage { } return didMove } - + public static func toZip(uploadRequest: UploadRequest) throws -> Data { var destinationFolder: String? // Extract directory paths from all images and check for consistency @@ -296,19 +296,19 @@ public class LocalStorage { destinationFolder = folder } } - + // Ensure a destination folder was found guard let finalDestinationFolder = destinationFolder else { throw SmileIDError.fileNotFound("Job not found") } - + // Create full URLs for all images let imageUrls = uploadRequest.images.map { imageInfo in URL(fileURLWithPath: finalDestinationFolder).appendingPathComponent(imageInfo.fileName) } - + var allUrls = imageUrls - + do { // Get the URL for the JSON file let jsonUrl = try LocalStorage.getInfoJsonFile(jobId: finalDestinationFolder) @@ -316,11 +316,11 @@ public class LocalStorage { } catch { debugPrint("Warning: info.json file not found. Continuing without it.") } - + // Zip all files return try zipFiles(at: allUrls) } - + public static func zipFiles(at urls: [URL]) throws -> Data { let archive = try Archive(accessMode: .create) for url in urls { @@ -328,20 +328,20 @@ public class LocalStorage { } return archive.data! } - + private static func extractDirectoryPath(from path: String) -> String? { let url = URL(fileURLWithPath: path) // Remove the last component and add a trailing slash let directoryPath = url.deletingLastPathComponent().path + "/" return directoryPath } - + private static func delete(at url: URL) throws { if fileManager.fileExists(atPath: url.relativePath) { try fileManager.removeItem(atPath: url.relativePath) } } - + static func delete(at jobIds: [String]) throws { try jobIds.forEach { let unsubmittedJob = try unsubmittedJobDirectory.appendingPathComponent($0) @@ -350,13 +350,13 @@ public class LocalStorage { try delete(at: submittedJob) } } - + static func deleteAll() throws { if try fileManager.fileExists(atPath: defaultDirectory.relativePath) { try fileManager.removeItem(atPath: defaultDirectory.relativePath) } } - + static func deleteLivenessAndSelfieFiles(at jobIds: [String]) throws { func deleteMatchingFiles(in directory: URL) throws { let contents = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) @@ -367,11 +367,11 @@ public class LocalStorage { } } } - + try jobIds.forEach { jobId in let unsubmittedJob = try unsubmittedJobDirectory.appendingPathComponent(jobId) try deleteMatchingFiles(in: unsubmittedJob) - + let submittedJob = try submittedJobDirectory.appendingPathComponent(jobId) try deleteMatchingFiles(in: submittedJob) } @@ -388,15 +388,15 @@ public extension URL { func asDocumentFrontImage() -> UploadImageInfo { return UploadImageInfo(imageTypeId: .idCardJpgFile, fileName: lastPathComponent) } - + func asDocumentBackImage() -> UploadImageInfo { return UploadImageInfo(imageTypeId: .idCardRearJpgFile, fileName: lastPathComponent) } - + func asSelfieImage() -> UploadImageInfo { return UploadImageInfo(imageTypeId: .selfieJpgFile, fileName: lastPathComponent) } - + func asLivenessImage() -> UploadImageInfo { return UploadImageInfo(imageTypeId: .livenessJpgFile, fileName: lastPathComponent) } diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift index 9d6e6f987..15f9311b0 100644 --- a/Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseJobSubmission.swift @@ -7,46 +7,46 @@ public class BaseJobSubmission { // MARK: - Properties - + public let jobId: String - + // MARK: - Initialization - + public init(jobId: String) { self.jobId = jobId } - + // MARK: - Methods to Override - + /// Creates the authentication request for the job submission /// - Returns: Authentication request object public func createAuthRequest() -> AuthenticationRequest { fatalError("Must be implemented by subclass") } - + /// Creates the prep upload request for the job submission /// - Parameter authResponse: Optional authentication response from previous step /// - Returns: Prep upload request object - public func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { + public func createPrepUploadRequest(authResponse _: AuthenticationResponse? = nil) -> PrepUploadRequest { fatalError("Must be implemented by subclass") } - + /// Creates the upload request for the job submission /// - Parameter authResponse: Optional authentication response from previous step /// - Returns: Upload request object - public func createUploadRequest(authResponse: AuthenticationResponse?) -> UploadRequest { + public func createUploadRequest(authResponse _: AuthenticationResponse?) -> UploadRequest { fatalError("Must be implemented by subclass") } - + /// Creates the success result for the job submission /// - Parameter didSubmit: Whether the job was submitted to the backend /// - Returns: Success result object - public func createSuccessResult(didSubmit: Bool) async throws -> SmileIDResult.Success { + public func createSuccessResult(didSubmit _: Bool) async throws -> SmileIDResult.Success { fatalError("Must be implemented by subclass") } - + // MARK: - Public Methods - + /// Executes the job submission process /// - Parameters: /// - skipApiSubmission: If true, skips the API submission @@ -67,25 +67,25 @@ public class BaseJobSubmission { return .error(error) } } - + /// Handles offline preparation logic /// Override this method to implement custom offline preparation public func handleOfflinePreparation() async throws { let authRequest = createAuthRequest() - try LocalStorage.createAuthenticationRequestFile(jobId: jobId, authentationRequest: authRequest) + try LocalStorage.createAuthenticationRequestFile(jobId: jobId, authentationRequest: authRequest) try LocalStorage.createPrepUploadFile( jobId: jobId, prepUpload: createPrepUploadRequest() ) } - + // MARK: - Private Methods - + func executeApiSubmission(offlineMode: Bool) async throws -> SmileIDResult { if offlineMode { try await handleOfflinePreparation() } - + do { let authResponse = try await executeAuthentication() let prepUploadResponse = try await executePrepUpload(authResponse: authResponse) @@ -96,7 +96,7 @@ public class BaseJobSubmission { return .error(error) } } - + func executeAuthentication() async throws -> AuthenticationResponse { do { return try await SmileID.api.authenticate(request: createAuthRequest()) @@ -106,14 +106,14 @@ public class BaseJobSubmission { throw error } } - + private func executePrepUpload( authResponse: AuthenticationResponse? ) async throws -> PrepUploadResponse { let prepUploadRequest = createPrepUploadRequest(authResponse: authResponse) return try await executePrepUploadWithRetry(prepUploadRequest: prepUploadRequest) } - + private func executePrepUploadWithRetry( prepUploadRequest: PrepUploadRequest, isRetry: Bool = false @@ -130,7 +130,7 @@ public class BaseJobSubmission { } } } - + private func executeUpload( authResponse: AuthenticationResponse?, prepUploadResponse: PrepUploadResponse @@ -144,7 +144,7 @@ public class BaseJobSubmission { LocalStorage.getFileByType(jobId: jobId, fileType: .selfie), LocalStorage.getFileByType(jobId: jobId, fileType: .documentFront), LocalStorage.getFileByType(jobId: jobId, fileType: .documentBack), - LocalStorage.getInfoJsonFile(jobId: jobId) + LocalStorage.getInfoJsonFile(jobId: jobId), ].compactMap { $0 } allFiles = livenessFiles + additionalFiles } catch { @@ -156,9 +156,8 @@ public class BaseJobSubmission { throw error } } - + // MARK: - Constants - } private enum SmileErrorConstants { diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift index f33a3be46..59260c916 100644 --- a/Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseSubmissionViewModel.swift @@ -6,67 +6,67 @@ // open class BaseSubmissionViewModel: ObservableObject { // MARK: - Internal Properties - - internal var result: SmileIDResult? - + + var result: SmileIDResult? + // MARK: - Methods to Override - + /// Creates the submission object for the job /// - Returns: A BaseJobSubmission instance - open func createSubmission() throws -> BaseJobSubmission { + open func createSubmission() throws -> BaseJobSubmission { fatalError("Must be implemented by subclass") } - + /// Called when job is in processing state open func triggerProcessingState() { fatalError("Must be implemented by subclass") } - + /// Handles successful job submission /// - Parameter data: The result data - open func handleSuccess(data: ResultType) { + open func handleSuccess(data _: ResultType) { fatalError("Must be implemented by subclass") } - + /// Handles job submission error /// - Parameter error: The error that occurred - open func handleError(error: Error) { + open func handleError(error _: Error) { fatalError("Must be implemented by subclass") } - + /// Handles submission files for the given job ID /// - Parameter jobId: The job ID - open func handleSubmissionFiles(jobId: String) throws { + open func handleSubmissionFiles(jobId _: String) throws { fatalError("Must be implemented by subclass") } - + /// Handles offline success scenario open func handleOfflineSuccess() { fatalError("Must be implemented by subclass") } - + // MARK: - Internal Methods - + /// Handles proxy error scenarios /// - Parameters: /// - jobId: The job ID /// - error: The error that occurred - internal func proxyErrorHandler(jobId: String, error: Error) { + func proxyErrorHandler(jobId: String, error: Error) { // First handle SmileIDError specific cases if let smileError = error as? SmileIDError { do { let didMoveToSubmitted = try LocalStorage.handleOfflineJobFailure(jobId: jobId, error: smileError) - + if didMoveToSubmitted { try handleSubmissionFiles(jobId: jobId) } - + // Check if we should handle this as an offline success case if SmileID.allowOfflineMode && SmileIDError.isNetworkFailure(error: smileError) { handleOfflineSuccess() return } - + // If not a network failure or offline mode isn't allowed, handle as regular error handleError(error: smileError) } catch { @@ -75,23 +75,23 @@ open class BaseSubmissionViewModel: ObservableObject } return } - + // Handle non-SmileIDError cases handleError(error: error) } - + /// Submit job with given parameters /// - Parameters: /// - jobId: The job ID /// - skipApiSubmission: If true, skips API submission /// - offlineMode: If true, runs in offline mode - internal func submitJob( + func submitJob( jobId: String, skipApiSubmission: Bool = false, offlineMode: Bool = SmileID.allowOfflineMode ) { triggerProcessingState() - + Task { do { let submission = try createSubmission() @@ -99,14 +99,14 @@ open class BaseSubmissionViewModel: ObservableObject skipApiSubmission: skipApiSubmission, offlineMode: offlineMode ) - + await MainActor.run { self.result = submissionResult - + switch submissionResult { - case .success(let success): + case let .success(success): handleSuccess(data: success.result) - case .error(let error): + case let .error(error): proxyErrorHandler(jobId: jobId, error: error) } } diff --git a/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift b/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift index 94a0a12c6..e4319d373 100644 --- a/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/Base/BaseSynchronousJobSubmission.swift @@ -7,36 +7,36 @@ public class BaseSynchronousJobSubmission: BaseJobSubmission { // MARK: - Initialization - - public override init(jobId: String) { + + override public init(jobId: String) { super.init(jobId: jobId) } - + // MARK: - Methods to Override - + /// Gets the API response for the job submission /// - Returns: Optional API response - public func getApiResponse(authResponse:AuthenticationResponse) async throws -> ApiResponse? { + public func getApiResponse(authResponse _: AuthenticationResponse) async throws -> ApiResponse? { fatalError("Must be implemented by subclass") } - + /// Creates the synchronous result for the job submission /// - Parameter result: Optional API response /// - Returns: Success result object - public func createSynchronousResult(result: ApiResponse?) async throws -> SmileIDResult.Success { + public func createSynchronousResult(result _: ApiResponse?) async throws -> SmileIDResult.Success { fatalError("Must be implemented by subclass") } - + // MARK: - Overridden Methods - + /// Executes the API submission process /// - Parameter offlineMode: If true, performs offline preparation /// - Returns: Result of the job submission - public override func executeApiSubmission(offlineMode: Bool) async throws -> SmileIDResult { + override public func executeApiSubmission(offlineMode: Bool) async throws -> SmileIDResult { if offlineMode { try await handleOfflinePreparation() } - + do { let authResponse = try await executeAuthentication() let apiResponse = try await getApiResponse(authResponse: authResponse) @@ -46,13 +46,13 @@ public class BaseSynchronousJobSubmission SmileIDResult.Success { - var apiResponse : ApiResponse? = nil - if(didSubmit){ + override public func createSuccessResult(didSubmit: Bool) async throws -> SmileIDResult.Success { + var apiResponse: ApiResponse? = nil + if didSubmit { let authResponse = try await executeAuthentication() apiResponse = try await getApiResponse(authResponse: authResponse) } diff --git a/Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift b/Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift index 850e879bd..9a45c8108 100644 --- a/Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/BiometricKYCSubmission.swift @@ -7,7 +7,7 @@ public class BiometricKYCSubmission: BaseJobSubmission { // MARK: - Properties - + private let userId: String private let allowNewEnroll: Bool private let livenessFiles: [URL]? @@ -15,9 +15,9 @@ public class BiometricKYCSubmission: BaseJobSubmission { private let idInfo: IdInfo private let extraPartnerParams: [String: String] private let metadata: Metadata - + // MARK: - Initialization - + public init( userId: String, jobId: String, @@ -37,10 +37,10 @@ public class BiometricKYCSubmission: BaseJobSubmission { self.metadata = metadata super.init(jobId: jobId) } - + // MARK: - BaseJobSubmission Overrides - - public override func createAuthRequest() -> AuthenticationRequest { + + override public func createAuthRequest() -> AuthenticationRequest { return AuthenticationRequest( jobType: .biometricKyc, enrollment: false, @@ -50,16 +50,16 @@ public class BiometricKYCSubmission: BaseJobSubmission { idType: idInfo.idType ) } - - public override func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { + + override public func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { let partnerParams = authResponse?.partnerParams.copy(extras: extraPartnerParams) ?? - PartnerParams( - jobId: jobId, - userId: userId, - jobType: .biometricKyc, - extras: extraPartnerParams - ) - + PartnerParams( + jobId: jobId, + userId: userId, + jobType: .biometricKyc, + extras: extraPartnerParams + ) + return PrepUploadRequest( partnerParams: partnerParams, allowNewEnroll: String(allowNewEnroll), @@ -68,18 +68,18 @@ public class BiometricKYCSubmission: BaseJobSubmission { signature: authResponse?.signature ?? "" ) } - - public override func createUploadRequest(authResponse: AuthenticationResponse?) -> UploadRequest { + + override public func createUploadRequest(authResponse _: AuthenticationResponse?) -> UploadRequest { let selfieImageInfo = selfieFile.asSelfieImage() let livenessImageInfo = livenessFiles?.map { $0.asLivenessImage() } ?? [] - + return UploadRequest( images: [selfieImageInfo] + livenessImageInfo, idInfo: idInfo.copy(entered: true) ) } - - public override func createSuccessResult(didSubmit: Bool) async throws -> SmileIDResult.Success { + + override public func createSuccessResult(didSubmit: Bool) async throws -> SmileIDResult.Success { let result = BiometricKycResult( captureData: SelfieCaptureResult( selfieImage: selfieFile, @@ -87,7 +87,7 @@ public class BiometricKYCSubmission: BaseJobSubmission { ), didSubmitJob: didSubmit ) - + return SmileIDResult.Success(result: result) } } diff --git a/Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift b/Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift index 1810f3a9b..01748a0bc 100644 --- a/Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/DocumentVerificationSubmission.swift @@ -6,37 +6,35 @@ // class DocumentVerificationSubmission: BaseDocumentVerificationSubmission { - public init( - jobId: String, - userId: String, - countryCode: String, - allowNewEnroll: Bool, - documentFrontFile: URL, - selfieFile: URL, - documentType: String? = nil, - documentBackFile: URL? = nil, - livenessFiles: [URL]? = nil, - extraPartnerParams: [String: String], - metadata: [Metadatum]? = nil - ) { - super.init( - jobId: jobId, - userId: userId, - jobType: .documentVerification, - countryCode: countryCode, - documentType: documentType, - allowNewEnroll: allowNewEnroll, - documentFrontFile: documentFrontFile, - selfieFile: selfieFile, - documentBackFile: documentBackFile, - livenessFiles: livenessFiles, - extraPartnerParams: extraPartnerParams, - metadata: metadata - ) - } - - + jobId: String, + userId: String, + countryCode: String, + allowNewEnroll: Bool, + documentFrontFile: URL, + selfieFile: URL, + documentType: String? = nil, + documentBackFile: URL? = nil, + livenessFiles: [URL]? = nil, + extraPartnerParams: [String: String], + metadata: [Metadatum]? = nil + ) { + super.init( + jobId: jobId, + userId: userId, + jobType: .documentVerification, + countryCode: countryCode, + documentType: documentType, + allowNewEnroll: allowNewEnroll, + documentFrontFile: documentFrontFile, + selfieFile: selfieFile, + documentBackFile: documentBackFile, + livenessFiles: livenessFiles, + extraPartnerParams: extraPartnerParams, + metadata: metadata + ) + } + override func createResultInstance( selfieFile: URL, documentFrontFile: URL, diff --git a/Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift b/Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift index 7e8cdf60f..bc8a98d79 100644 --- a/Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/EnhancedDocumentVerificationSubmission.swift @@ -7,7 +7,7 @@ public class EnhancedDocumentVerificationSubmission: BaseDocumentVerificationSubmission { // MARK: - Initialization - + public init( jobId: String, userId: String, @@ -37,8 +37,7 @@ public class EnhancedDocumentVerificationSubmission: BaseDocumentVerificationSub ) } - - public override func createResultInstance( + override public func createResultInstance( selfieFile: URL, documentFrontFile: URL, livenessFiles: [URL]?, diff --git a/Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift b/Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift index d993b73e7..b250a4bb4 100644 --- a/Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift +++ b/Sources/SmileID/Classes/JobSubmission/Results/SmileIDResults.swift @@ -14,7 +14,7 @@ public struct SmartSelfieResult: CaptureResult { public let captureData: SelfieCaptureResult public let didSubmitJob: Bool public let apiResponse: SmartSelfieResponse? - + public init( captureData: SelfieCaptureResult, didSubmitJob: Bool, @@ -29,7 +29,7 @@ public struct SmartSelfieResult: CaptureResult { public struct DocumentVerificationResult: CaptureResult { public let captureData: DocumentCaptureResult public let didSubmitJob: Bool - + public init( captureData: DocumentCaptureResult, didSubmitJob: Bool @@ -42,7 +42,7 @@ public struct DocumentVerificationResult: CaptureResult { public struct EnhancedDocumentVerificationResult: CaptureResult { public let captureData: DocumentCaptureResult public let didSubmitJob: Bool - + public init( captureData: DocumentCaptureResult, didSubmitJob: Bool @@ -55,7 +55,7 @@ public struct EnhancedDocumentVerificationResult: CaptureResult { public struct BiometricKycResult: CaptureResult { public let captureData: SelfieCaptureResult public let didSubmitJob: Bool - + public init( captureData: SelfieCaptureResult, didSubmitJob: Bool @@ -70,10 +70,10 @@ public struct BiometricKycResult: CaptureResult { public enum SmileIDResult { case success(Success) case error(Error) - + public struct Success { public let result: ResultType - + public init(result: ResultType) { self.result = result } diff --git a/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift b/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift index 09be96198..f53a179c6 100644 --- a/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift +++ b/Sources/SmileID/Classes/JobSubmission/SelfieSubmission.swift @@ -7,7 +7,7 @@ public class SelfieSubmission: BaseSynchronousJobSubmission { // MARK: - Properties - + private let isEnroll: Bool private let userId: String private let allowNewEnroll: Bool @@ -15,9 +15,9 @@ public class SelfieSubmission: BaseSynchronousJobSubmission AuthenticationRequest { + + override public func createAuthRequest() -> AuthenticationRequest { return AuthenticationRequest( jobType: isEnroll ? .smartSelfieEnrollment : .smartSelfieAuthentication, enrollment: isEnroll, @@ -48,8 +48,8 @@ public class SelfieSubmission: BaseSynchronousJobSubmission PrepUploadRequest { + + override public func createPrepUploadRequest(authResponse: AuthenticationResponse? = nil) -> PrepUploadRequest { return PrepUploadRequest( partnerParams: PartnerParams( jobId: jobId, @@ -63,16 +63,16 @@ public class SelfieSubmission: BaseSynchronousJobSubmission UploadRequest { + + override public func createUploadRequest(authResponse _: AuthenticationResponse?) -> UploadRequest { return UploadRequest( images: [selfieFile.asSelfieImage()] + livenessFiles.map { url in url.asLivenessImage() } ) } - - public override func getApiResponse(authResponse:AuthenticationResponse) async throws -> SmartSelfieResponse? { + + override public func getApiResponse(authResponse: AuthenticationResponse) async throws -> SmartSelfieResponse? { var smartSelfieImage: MultipartBody? var smartSelfieLivenessImages = [MultipartBody]() if let selfie = try? Data(contentsOf: selfieFile), let media = MultipartBody( @@ -93,7 +93,7 @@ public class SelfieSubmission: BaseSynchronousJobSubmission SmileIDResult.Success { + + override public func createSynchronousResult(result: SmartSelfieResponse?) async throws -> SmileIDResult.Success { // Move files from unsubmitted to submitted directories - var selfieFileResult = self.selfieFile - var livenessImagesResult = self.livenessFiles - + var selfieFileResult = selfieFile + var livenessImagesResult = livenessFiles + do { - if let result = result{ - try LocalStorage.moveToSubmittedJobs(jobId: self.jobId) + if let result = result { + try LocalStorage.moveToSubmittedJobs(jobId: jobId) guard let selfieFileResult = try LocalStorage.getFileByType( jobId: jobId, fileType: FileType.selfie, submitted: true - ) else{ + ) else { throw SmileIDError.unknown("Selfie file not found") } livenessImagesResult = try LocalStorage.getFilesByType( @@ -156,18 +156,18 @@ public class SelfieSubmission: BaseSynchronousJobSubmission, ARKitS private let minFaceAreaThreshold = 0.125 private let maxFaceAreaThreshold = 0.25 private let faceRotationThreshold = 0.03 - private let faceRollThreshold = 0.025 // roll has a smaller range than yaw + private let faceRollThreshold = 0.025 // roll has a smaller range than yaw private let numLivenessImages = 7 - private let numTotalSteps = 8 // numLivenessImages + 1 selfie image + private let numTotalSteps = 8 // numLivenessImages + 1 selfie image private let livenessImageSize = 320 private let selfieImageSize = 640 - + private let isEnroll: Bool private let userId: String private let jobId: String @@ -27,7 +27,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS private let extraPartnerParams: [String: String] private var localMetadata: LocalMetadata private let faceDetector = FaceDetector() - + var cameraManager = CameraManager(orientation: .portrait) var shouldAnalyzeImages = true var lastAutoCaptureTime = Date() @@ -48,7 +48,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS CVPixelBuffer?, Never >() private var subscribers = Set() - + // UI Properties @Published var unauthorizedAlert: AlertState? @Published var directive: String = "Instructions.Start" @@ -64,7 +64,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS // This is toggled by a Binding didSet { switchCamera() } } - + public init( isEnroll: Bool, userId: String, @@ -91,7 +91,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS .map { _ in AlertState.cameraUnauthorized } .sink { [weak self] alert in self?.unauthorizedAlert = alert } .store(in: &subscribers) - + cameraManager.sampleBufferPublisher .merge(with: arKitFramePublisher) .throttle( @@ -112,21 +112,21 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS : Metadatum.SelfieImageOrigin(cameraFacing: .frontCamera) ) } - + let metadataTimerStart = MonotonicTime() - + func updateLocalMetadata(_ newMetadata: LocalMetadata) { localMetadata = newMetadata objectWillChange.send() } - + // swiftlint:disable cyclomatic_complexity func analyzeImage(image: CVPixelBuffer) { let elapsedtime = Date().timeIntervalSince(lastAutoCaptureTime) if !shouldAnalyzeImages || elapsedtime < intraImageMinDelay { return } - + do { try faceDetector.detect(imageBuffer: image) { [weak self] request, error in guard let self else { return } @@ -142,7 +142,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS print("Did not receive the expected [VNFaceObservation]") return } - + if results.count == 0 { DispatchQueue.main.async { self.directive = "Instructions.UnableToDetectFace" @@ -159,7 +159,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS } return } - + // Ensure only 1 face is in frame if results.count > 1 { DispatchQueue.main.async { @@ -167,18 +167,18 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS } return } - + guard let face = results.first else { print("Unexpectedly got an empty face array") return } - + // The coordinate system of the bounding box in VNFaceObservation is such that // the camera view spans [0-1]x[0-1] and the face is within that. Since we don't // need to draw on the camera view, we don't need to convert this to the view's // coordinate system. We can calculate out of bounds and face area directly on this let boundingBox = face.boundingBox - + // Check that the corners of the face bounding box are within frame if boundingBox.minX < minFaceCenteredThreshold || boundingBox.minY < minFaceCenteredThreshold @@ -190,7 +190,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS } return } - + // image's area is equal to 1. so (bbox area / image area) == bbox area let faceFillRatio = boundingBox.width * boundingBox.height if faceFillRatio < minFaceAreaThreshold { @@ -199,7 +199,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS } return } - + if faceFillRatio > maxFaceAreaThreshold { DispatchQueue.main.async { self.directive = "Instructions.MoveFarther" @@ -208,7 +208,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS } if let quality = face.faceCaptureQuality, - quality < faceCaptureQualityThreshold + quality < faceCaptureQualityThreshold { DispatchQueue.main.async { self.directive = "Instructions.Quality" @@ -222,14 +222,14 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS DispatchQueue.main.async { self.directive = userNeedsToSmile - ? "Instructions.Smile" : "Instructions.Capturing" + ? "Instructions.Smile" : "Instructions.Capturing" } - + // TODO: Use mouth deformation as an alternate signal for non-ARKit capture if userNeedsToSmile, currentlyUsingArKit, !isSmiling { return } - + // Perform the rotation checks *after* changing directive to Capturing -- we don't // want to explicitly tell the user to move their head if !hasFaceRotatedEnough(face: face) { @@ -245,38 +245,40 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS if livenessImages.count < numLivenessImages { guard let imageData = - ImageUtils.resizePixelBufferToHeight( - image, - height: livenessImageSize, - orientation: orientation - ) + ImageUtils.resizePixelBufferToHeight( + image, + height: livenessImageSize, + orientation: orientation + ) else { throw SmileIDError.unknown( "Error resizing liveness image") } let imageUrl = try LocalStorage.createLivenessFile( - jobId: jobId, livenessFile: imageData) + jobId: jobId, livenessFile: imageData + ) livenessImages.append(imageUrl) DispatchQueue.main.async { self.captureProgress = Double(self.livenessImages.count) - / Double(self.numTotalSteps) + / Double(self.numTotalSteps) } } else { shouldAnalyzeImages = false guard let imageData = - ImageUtils.resizePixelBufferToHeight( - image, - height: selfieImageSize, - orientation: orientation - ) + ImageUtils.resizePixelBufferToHeight( + image, + height: selfieImageSize, + orientation: orientation + ) else { throw SmileIDError.unknown( "Error resizing selfie image") } let selfieImage = try LocalStorage.createSelfieFile( - jobId: jobId, selfieFile: imageData) + jobId: jobId, selfieFile: imageData + ) self.selfieImage = selfieImage DispatchQueue.main.async { self.captureProgress = 1 @@ -295,7 +297,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS return } } - + func hasFaceRotatedEnough(face: VNFaceObservation) -> Bool { guard let roll = face.roll?.doubleValue, let yaw = face.yaw?.doubleValue else { @@ -311,7 +313,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS } let rollDelta = abs(roll - previousHeadRoll) let yawDelta = abs(yaw - previousHeadYaw) - + previousHeadRoll = face.roll?.doubleValue ?? Double.infinity previousHeadYaw = face.yaw?.doubleValue ?? Double.infinity if #available(iOS 15, *) { @@ -321,15 +323,15 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS return didPitchChange || rollDelta > faceRollThreshold || yawDelta > faceRotationThreshold } - + func onSmiling(isSmiling: Bool) { self.isSmiling = isSmiling } - + func onARKitFrame(frame: ARFrame) { arKitFramePublisher.send(frame.capturedImage) } - + func switchCamera() { cameraManager.switchCamera(to: useBackCamera ? .back : .front) localMetadata.metadata.removeAllOfType(Metadatum.SelfieImageOrigin.self) @@ -338,7 +340,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS ? Metadatum.SelfieImageOrigin(cameraFacing: .backCamera) : Metadatum.SelfieImageOrigin(cameraFacing: .frontCamera)) } - + public func onSelfieRejected() { DispatchQueue.main.async { self.captureProgress = 0 @@ -355,7 +357,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS localMetadata.metadata.removeAllOfType( Metadatum.SelfieCaptureDuration.self) } - + func cleanUpSelfieCapture() { do { try LocalStorage.deleteLivenessAndSelfieFiles(at: [jobId]) @@ -363,7 +365,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS debugPrint(error.localizedDescription) } } - + func onRetry() { // If selfie file is present, all captures were completed, so we're retrying a network issue if selfieImage != nil, livenessImages.count == numLivenessImages { @@ -373,7 +375,7 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS DispatchQueue.main.async { self.processingState = nil } } } - + public func submitJob() { localMetadata.addMetadata( Metadatum.SelfieCaptureDuration( @@ -386,13 +388,13 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS } submitJob(jobId: jobId, skipApiSubmission: skipApiSubmission, offlineMode: SmileID.allowOfflineMode) } - + public func onFinished(callback: SmartSelfieResultDelegate) { if let selfieImage = selfieImage, - let selfiePath = getRelativePath(from: selfieImage), - livenessImages.count == numLivenessImages, - !livenessImages.contains(where: { getRelativePath(from: $0) == nil } - ) + let selfiePath = getRelativePath(from: selfieImage), + livenessImages.count == numLivenessImages, + !livenessImages.contains(where: { getRelativePath(from: $0) == nil } + ) { let livenessImagesPaths = livenessImages.compactMap { getRelativePath(from: $0) @@ -409,17 +411,17 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS callback.didError(error: SmileIDError.unknown("Unknown error")) } } - + func openSettings() { guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(settingsURL) } - - - public override func createSubmission() throws -> BaseJobSubmission { + + override public func createSubmission() throws -> BaseJobSubmission { guard let selfieImage = selfieImage, - livenessImages.count == numLivenessImages else { + livenessImages.count == numLivenessImages + else { throw SmileIDError.unknown("Selfie images missing") } return SelfieSubmission( @@ -433,17 +435,17 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS metadata: localMetadata.metadata ) } - - public override func triggerProcessingState() { + + override public func triggerProcessingState() { DispatchQueue.main.async { self.processingState = .inProgress } } - - public override func handleSuccess(data: SmartSelfieResult) { + + override public func handleSuccess(data: SmartSelfieResult) { apiResponse = data.apiResponse DispatchQueue.main.async { self.processingState = .success } } - - public override func handleError(error: Error) { + + override public func handleError(error: Error) { if let smileError = error as? SmileIDError { print("Error submitting job: \(error)") let (errorMessageRes, errorMessage) = toErrorMessage(error: smileError) @@ -457,21 +459,21 @@ public class SelfieViewModel: BaseSubmissionViewModel, ARKitS DispatchQueue.main.async { self.processingState = .error } } } - - public override func handleSubmissionFiles(jobId: String) throws { - self.selfieImage = try LocalStorage.getFileByType( + + override public func handleSubmissionFiles(jobId: String) throws { + selfieImage = try LocalStorage.getFileByType( jobId: jobId, fileType: FileType.selfie, submitted: true ) - self.livenessImages = try LocalStorage.getFilesByType( + livenessImages = try LocalStorage.getFilesByType( jobId: jobId, fileType: FileType.liveness, submitted: true ) ?? [] } - - public override func handleOfflineSuccess() { + + override public func handleOfflineSuccess() { DispatchQueue.main.async { self.errorMessageRes = "Offline.Message" self.processingState = .success