diff --git a/.editorconfig b/.editorconfig index 765c4cec..df8aef05 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,5 +2,6 @@ max_line_length=100 [{*.kt,*.kts}] +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_allow_trailing_comma=true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 442745ac..d162e596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Document verification * Enhanced document verification * Added an Offline Mode, enabled by calling `SmileID.setAllowOfflineMode(true)`. If a job is attempted while the device is offline, and offline mode has been enabled, the UI will complete successfully and the job can be submitted at a later time by calling `SmileID.submitJob(jobId)` +* Improved SmartSelfie Enrollment and Authentication times by moving to a synchronous API endpoint ## 10.0.12 diff --git a/android/src/main/kotlin/com/smileidentity/flutter/Mapper.kt b/android/src/main/kotlin/com/smileidentity/flutter/Mapper.kt index ffa6d744..a7ccc14f 100644 --- a/android/src/main/kotlin/com/smileidentity/flutter/Mapper.kt +++ b/android/src/main/kotlin/com/smileidentity/flutter/Mapper.kt @@ -27,6 +27,7 @@ import FlutterImageLinks import FlutterImageType import FlutterJobStatusRequest import FlutterJobType +import FlutterJobTypeV2 import FlutterPartnerParams import FlutterPrepUploadRequest import FlutterPrepUploadResponse @@ -35,6 +36,8 @@ import FlutterProductsConfigResponse import FlutterServicesResponse import FlutterSmartSelfieJobResult import FlutterSmartSelfieJobStatusResponse +import FlutterSmartSelfieResponse +import FlutterSmartSelfieStatus import FlutterSuspectUser import FlutterUploadImageInfo import FlutterUploadRequest @@ -81,7 +84,10 @@ import com.smileidentity.models.UploadImageInfo import com.smileidentity.models.UploadRequest import com.smileidentity.models.ValidDocument import com.smileidentity.models.ValidDocumentsResponse +import com.smileidentity.models.v2.SmartSelfieResponse +import com.smileidentity.models.v2.SmartSelfieStatus import java.io.File +import com.smileidentity.models.v2.JobType as JobTypeV2 /** * Pigeon does not allow non nullable types in this example here @@ -122,6 +128,17 @@ fun JobType.toResponse() = when (this) { else -> TODO("Not yet implemented") } +fun FlutterJobTypeV2.toRequest() = when (this) { + FlutterJobTypeV2.SMARTSELFIEAUTHENTICATION -> JobTypeV2.SmartSelfieAuthentication + FlutterJobTypeV2.SMARTSELFIEENROLLMENT -> JobTypeV2.SmartSelfieEnrollment +} + +fun JobTypeV2.toResponse() = when (this) { + JobTypeV2.SmartSelfieAuthentication -> FlutterJobTypeV2.SMARTSELFIEAUTHENTICATION + JobTypeV2.SmartSelfieEnrollment -> FlutterJobTypeV2.SMARTSELFIEENROLLMENT + else -> TODO("Not yet implemented") +} + fun FlutterAuthenticationRequest.toRequest() = AuthenticationRequest( jobType = jobType.toRequest(), country = country, @@ -330,6 +347,26 @@ fun SmartSelfieJobResult.toResponse(): Any = when (this) { ) } +fun SmartSelfieStatus.toResponse() = when (this) { + SmartSelfieStatus.Approved -> FlutterSmartSelfieStatus.APPROVED + SmartSelfieStatus.Pending -> FlutterSmartSelfieStatus.PENDING + SmartSelfieStatus.Rejected -> FlutterSmartSelfieStatus.REJECTED + SmartSelfieStatus.Unknown -> FlutterSmartSelfieStatus.UNKNOWN +} + +fun SmartSelfieResponse.toResponse() = FlutterSmartSelfieResponse( + code = code, + createdAt = createdAt, + jobId = jobId, + jobType = jobType.toResponse(), + message = message, + partnerId = partnerId, + partnerParams = convertNonNullMapToNullable(partnerParams), + status = status.toResponse(), + updatedAt = updatedAt, + userId = userId, +) + fun DocumentVerificationJobStatusResponse.toResponse() = FlutterDocumentVerificationJobStatusResponse( timestamp = timestamp, diff --git a/android/src/main/kotlin/com/smileidentity/flutter/SmileIDPlugin.kt b/android/src/main/kotlin/com/smileidentity/flutter/SmileIDPlugin.kt index b821071a..0199efde 100644 --- a/android/src/main/kotlin/com/smileidentity/flutter/SmileIDPlugin.kt +++ b/android/src/main/kotlin/com/smileidentity/flutter/SmileIDPlugin.kt @@ -15,12 +15,15 @@ import FlutterProductsConfigRequest import FlutterProductsConfigResponse import FlutterServicesResponse import FlutterSmartSelfieJobStatusResponse +import FlutterSmartSelfieResponse import FlutterUploadRequest import FlutterValidDocumentsResponse import SmileIDApi import android.app.Activity import android.content.Context import com.smileidentity.SmileID +import com.smileidentity.SmileIDOptIn +import com.smileidentity.networking.asFormDataPart import com.smileidentity.networking.pollBiometricKycJobStatus import com.smileidentity.networking.pollDocumentVerificationJobStatus import com.smileidentity.networking.pollEnhancedDocumentVerificationJobStatus @@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.single import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File import java.net.URL import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -162,6 +166,74 @@ class SmileIDPlugin : FlutterPlugin, SmileIDApi, ActivityAware { callback = callback, ) + @OptIn(SmileIDOptIn::class) + override fun doSmartSelfieEnrollment( + signature: String, + timestamp: String, + selfieImage: String, + livenessImages: List, + userId: String, + partnerParams: Map?, + callbackUrl: String?, + sandboxResult: Long?, + allowNewEnroll: Boolean?, + callback: (Result) -> Unit, + ) = launch( + work = { + SmileID.api.doSmartSelfieEnrollment( + userId = userId, + selfieImage = File(selfieImage).asFormDataPart( + partName = "selfie_image", + mediaType = "image/jpeg", + ), + livenessImages = livenessImages.map { + File(selfieImage).asFormDataPart( + partName = "liveness_images", + mediaType = "image/jpeg", + ) + }, + partnerParams = convertNullableMapToNonNull(partnerParams), + callbackUrl = callbackUrl, + sandboxResult = sandboxResult?.toInt(), + allowNewEnroll = allowNewEnroll, + ).toResponse() + }, + callback = callback, + ) + + @OptIn(SmileIDOptIn::class) + override fun doSmartSelfieAuthentication( + signature: String, + timestamp: String, + selfieImage: String, + livenessImages: List, + userId: String, + partnerParams: Map?, + callbackUrl: String?, + sandboxResult: Long?, + callback: (Result) -> Unit, + ) = launch( + work = { + SmileID.api.doSmartSelfieAuthentication( + userId = userId, + selfieImage = File(selfieImage).asFormDataPart( + partName = "selfie_image", + mediaType = "image/jpeg", + ), + livenessImages = livenessImages.map { + File(selfieImage).asFormDataPart( + partName = "liveness_images", + mediaType = "image/jpeg", + ) + }, + partnerParams = convertNullableMapToNonNull(partnerParams), + callbackUrl = callbackUrl, + sandboxResult = sandboxResult?.toInt(), + ).toResponse() + }, + callback = callback, + ) + override fun getDocumentVerificationJobStatus( request: FlutterJobStatusRequest, callback: (Result) -> Unit, diff --git a/android/src/main/kotlin/com/smileidentity/flutter/generated/SmileIDMessages.g.kt b/android/src/main/kotlin/com/smileidentity/flutter/generated/SmileIDMessages.g.kt index 79a823e9..20c357de 100644 --- a/android/src/main/kotlin/com/smileidentity/flutter/generated/SmileIDMessages.g.kt +++ b/android/src/main/kotlin/com/smileidentity/flutter/generated/SmileIDMessages.g.kt @@ -57,6 +57,17 @@ enum class FlutterJobType(val raw: Int) { } } +enum class FlutterJobTypeV2(val raw: Int) { + SMARTSELFIEAUTHENTICATION(0), + SMARTSELFIEENROLLMENT(1); + + companion object { + fun ofRaw(raw: Int): FlutterJobTypeV2? { + return values().firstOrNull { it.raw == raw } + } + } +} + enum class FlutterImageType(val raw: Int) { SELFIEJPGFILE(0), IDCARDJPGFILE(1), @@ -99,6 +110,19 @@ enum class FlutterActionResult(val raw: Int) { } } +enum class FlutterSmartSelfieStatus(val raw: Int) { + APPROVED(0), + PENDING(1), + REJECTED(2), + UNKNOWN(3); + + companion object { + fun ofRaw(raw: Int): FlutterSmartSelfieStatus? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** * Custom values specific to partners can be placed in [extras] * @@ -786,6 +810,52 @@ data class FlutterSmartSelfieJobStatusResponse ( } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class FlutterSmartSelfieResponse ( + val code: String, + val createdAt: String, + val jobId: String, + val jobType: FlutterJobTypeV2, + val message: String, + val partnerId: String, + val partnerParams: Map? = null, + val status: FlutterSmartSelfieStatus, + val updatedAt: String, + val userId: String + +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): FlutterSmartSelfieResponse { + val code = list[0] as String + val createdAt = list[1] as String + val jobId = list[2] as String + val jobType = FlutterJobTypeV2.ofRaw(list[3] as Int)!! + val message = list[4] as String + val partnerId = list[5] as String + val partnerParams = list[6] as Map? + val status = FlutterSmartSelfieStatus.ofRaw(list[7] as Int)!! + val updatedAt = list[8] as String + val userId = list[9] as String + return FlutterSmartSelfieResponse(code, createdAt, jobId, jobType, message, partnerId, partnerParams, status, updatedAt, userId) + } + } + fun toList(): List { + return listOf( + code, + createdAt, + jobId, + jobType.raw, + message, + partnerId, + partnerParams, + status.raw, + updatedAt, + userId, + ) + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class FlutterDocumentVerificationJobResult ( val actions: FlutterActions, @@ -1645,25 +1715,30 @@ private object SmileIDApiCodec : StandardMessageCodec() { } 164.toByte() -> { return (readValue(buffer) as? List)?.let { - FlutterSuspectUser.fromList(it) + FlutterSmartSelfieResponse.fromList(it) } } 165.toByte() -> { return (readValue(buffer) as? List)?.let { - FlutterUploadImageInfo.fromList(it) + FlutterSuspectUser.fromList(it) } } 166.toByte() -> { return (readValue(buffer) as? List)?.let { - FlutterUploadRequest.fromList(it) + FlutterUploadImageInfo.fromList(it) } } 167.toByte() -> { return (readValue(buffer) as? List)?.let { - FlutterValidDocument.fromList(it) + FlutterUploadRequest.fromList(it) } } 168.toByte() -> { + return (readValue(buffer) as? List)?.let { + FlutterValidDocument.fromList(it) + } + } + 169.toByte() -> { return (readValue(buffer) as? List)?.let { FlutterValidDocumentsResponse.fromList(it) } @@ -1817,26 +1892,30 @@ private object SmileIDApiCodec : StandardMessageCodec() { stream.write(163) writeValue(stream, value.toList()) } - is FlutterSuspectUser -> { + is FlutterSmartSelfieResponse -> { stream.write(164) writeValue(stream, value.toList()) } - is FlutterUploadImageInfo -> { + is FlutterSuspectUser -> { stream.write(165) writeValue(stream, value.toList()) } - is FlutterUploadRequest -> { + is FlutterUploadImageInfo -> { stream.write(166) writeValue(stream, value.toList()) } - is FlutterValidDocument -> { + is FlutterUploadRequest -> { stream.write(167) writeValue(stream, value.toList()) } - is FlutterValidDocumentsResponse -> { + is FlutterValidDocument -> { stream.write(168) writeValue(stream, value.toList()) } + is FlutterValidDocumentsResponse -> { + stream.write(169) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -1859,6 +1938,8 @@ interface SmileIDApi { fun doEnhancedKyc(request: FlutterEnhancedKycRequest, callback: (Result) -> Unit) fun doEnhancedKycAsync(request: FlutterEnhancedKycRequest, callback: (Result) -> Unit) fun getSmartSelfieJobStatus(request: FlutterJobStatusRequest, callback: (Result) -> Unit) + fun doSmartSelfieEnrollment(signature: String, timestamp: String, selfieImage: String, livenessImages: List, userId: String, partnerParams: Map?, callbackUrl: String?, sandboxResult: Long?, allowNewEnroll: Boolean?, callback: (Result) -> Unit) + fun doSmartSelfieAuthentication(signature: String, timestamp: String, selfieImage: String, livenessImages: List, userId: String, partnerParams: Map?, callbackUrl: String?, sandboxResult: Long?, callback: (Result) -> Unit) fun getDocumentVerificationJobStatus(request: FlutterJobStatusRequest, callback: (Result) -> Unit) fun getBiometricKycJobStatus(request: FlutterJobStatusRequest, callback: (Result) -> Unit) fun getEnhancedDocumentVerificationJobStatus(request: FlutterJobStatusRequest, callback: (Result) -> Unit) @@ -2162,6 +2243,61 @@ interface SmileIDApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.smileid.SmileIDApi.doSmartSelfieEnrollment", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val signatureArg = args[0] as String + val timestampArg = args[1] as String + val selfieImageArg = args[2] as String + val livenessImagesArg = args[3] as List + val userIdArg = args[4] as String + val partnerParamsArg = args[5] as Map? + val callbackUrlArg = args[6] as String? + val sandboxResultArg = args[7].let { if (it is Int) it.toLong() else it as Long? } + val allowNewEnrollArg = args[8] as Boolean? + api.doSmartSelfieEnrollment(signatureArg, timestampArg, selfieImageArg, livenessImagesArg, userIdArg, partnerParamsArg, callbackUrlArg, sandboxResultArg, allowNewEnrollArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.smileid.SmileIDApi.doSmartSelfieAuthentication", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val signatureArg = args[0] as String + val timestampArg = args[1] as String + val selfieImageArg = args[2] as String + val livenessImagesArg = args[3] as List + val userIdArg = args[4] as String + val partnerParamsArg = args[5] as Map? + val callbackUrlArg = args[6] as String? + val sandboxResultArg = args[7].let { if (it is Int) it.toLong() else it as Long? } + api.doSmartSelfieAuthentication(signatureArg, timestampArg, selfieImageArg, livenessImagesArg, userIdArg, partnerParamsArg, callbackUrlArg, sandboxResultArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.smileid.SmileIDApi.getDocumentVerificationJobStatus", codec) if (api != null) { diff --git a/android/src/test/kotlin/com/smileidentity/flutter/SmileIDPluginTest.kt b/android/src/test/kotlin/com/smileidentity/flutter/SmileIDPluginTest.kt index 33335ad1..845de642 100644 --- a/android/src/test/kotlin/com/smileidentity/flutter/SmileIDPluginTest.kt +++ b/android/src/test/kotlin/com/smileidentity/flutter/SmileIDPluginTest.kt @@ -19,7 +19,6 @@ import kotlin.test.Test * you can run them directly from IDEs that support JUnit such as Android Studio. */ internal class SmileIDPluginTest { - @Test fun `when we call authenticate and pass a request object, we get a successful callback`() { val request = mockk() diff --git a/ios/Classes/Mapper.swift b/ios/Classes/Mapper.swift index dce53a2e..c1498146 100644 --- a/ios/Classes/Mapper.swift +++ b/ios/Classes/Mapper.swift @@ -1,5 +1,34 @@ import SmileID +func convertNullableMapToNonNull(data: [String? : String?]?) -> [String : String]? { + guard let unwrappedData = data else { return nil } + var convertedDictionary = [String : String]() + for (key, value) in unwrappedData { + if let unwrappedKey = key, let unwrappedValue = value { + convertedDictionary[unwrappedKey] = unwrappedValue + } + } + return convertedDictionary +} + +func getFile(atPath path: String) -> Data? { + // Create a URL from the provided path + let fileURL = URL(fileURLWithPath: path) + do { + // Check if the file exists + let fileExists = try fileURL.checkResourceIsReachable() + if fileExists { + // Read the contents of the file + let fileData = try Data(contentsOf: fileURL) + return fileData + } else { + return nil + } + } catch { + return nil + } +} + extension FlutterPartnerParams { func toRequest() -> PartnerParams { PartnerParams( @@ -85,6 +114,18 @@ extension JobType { } } +extension JobTypeV2 { + func toResponse() -> FlutterJobTypeV2 { + switch(self) { + case .smartSelfieAuthentication: + FlutterJobTypeV2.smartSelfieAuthentication + case .smartSelfieEnrollment: + FlutterJobTypeV2.smartSelfieEnrollment + default: fatalError("Not yet supported") + } + } +} + extension FlutterPrepUploadRequest { func toRequest() -> PrepUploadRequest { PrepUploadRequest( @@ -214,6 +255,38 @@ extension EnhancedKycAsyncResponse { } } +extension SmartSelfieStatus { + func toResponse() -> FlutterSmartSelfieStatus { + switch(self) { + case .approved: + FlutterSmartSelfieStatus.approved + case .pending: + FlutterSmartSelfieStatus.pending + case .rejected: + FlutterSmartSelfieStatus.rejected + case .unknown: + FlutterSmartSelfieStatus.unknown + } + } +} + +extension SmartSelfieResponse { + func toResponse() -> FlutterSmartSelfieResponse { + FlutterSmartSelfieResponse( + code: code, + createdAt: createdAt, + jobId: jobId, + jobType: jobType.toResponse(), + message: message, + partnerId: partnerId, + partnerParams: partnerParams, + status: status.toResponse(), + updatedAt: updatedAt, + userId: userId + ) + } +} + extension Actions { func toResponse() -> FlutterActions { FlutterActions( diff --git a/ios/Classes/SmileIDMessages.g.swift b/ios/Classes/SmileIDMessages.g.swift index 5d2af61d..5ebd0f2d 100644 --- a/ios/Classes/SmileIDMessages.g.swift +++ b/ios/Classes/SmileIDMessages.g.swift @@ -48,6 +48,11 @@ enum FlutterJobType: Int { case smartSelfieAuthentication = 5 } +enum FlutterJobTypeV2: Int { + case smartSelfieAuthentication = 0 + case smartSelfieEnrollment = 1 +} + enum FlutterImageType: Int { case selfieJpgFile = 0 case idCardJpgFile = 1 @@ -78,6 +83,13 @@ enum FlutterActionResult: Int { case unknown = 15 } +enum FlutterSmartSelfieStatus: Int { + case approved = 0 + case pending = 1 + case rejected = 2 + case unknown = 3 +} + /// Custom values specific to partners can be placed in [extras] /// /// Generated class from Pigeon that represents data sent in messages. @@ -833,6 +845,60 @@ struct FlutterSmartSelfieJobStatusResponse { } } +/// Generated class from Pigeon that represents data sent in messages. +struct FlutterSmartSelfieResponse { + var code: String + var createdAt: String + var jobId: String + var jobType: FlutterJobTypeV2 + var message: String + var partnerId: String + var partnerParams: [String?: String?]? = nil + var status: FlutterSmartSelfieStatus + var updatedAt: String + var userId: String + + static func fromList(_ list: [Any?]) -> FlutterSmartSelfieResponse? { + let code = list[0] as! String + let createdAt = list[1] as! String + let jobId = list[2] as! String + let jobType = FlutterJobTypeV2(rawValue: list[3] as! Int)! + let message = list[4] as! String + let partnerId = list[5] as! String + let partnerParams: [String?: String?]? = nilOrValue(list[6]) + let status = FlutterSmartSelfieStatus(rawValue: list[7] as! Int)! + let updatedAt = list[8] as! String + let userId = list[9] as! String + + return FlutterSmartSelfieResponse( + code: code, + createdAt: createdAt, + jobId: jobId, + jobType: jobType, + message: message, + partnerId: partnerId, + partnerParams: partnerParams, + status: status, + updatedAt: updatedAt, + userId: userId + ) + } + func toList() -> [Any?] { + return [ + code, + createdAt, + jobId, + jobType.rawValue, + message, + partnerId, + partnerParams, + status.rawValue, + updatedAt, + userId, + ] + } +} + /// Generated class from Pigeon that represents data sent in messages. struct FlutterDocumentVerificationJobResult { var actions: FlutterActions @@ -1677,14 +1743,16 @@ private class SmileIDApiCodecReader: FlutterStandardReader { case 163: return FlutterSmartSelfieJobStatusResponse.fromList(self.readValue() as! [Any?]) case 164: - return FlutterSuspectUser.fromList(self.readValue() as! [Any?]) + return FlutterSmartSelfieResponse.fromList(self.readValue() as! [Any?]) case 165: - return FlutterUploadImageInfo.fromList(self.readValue() as! [Any?]) + return FlutterSuspectUser.fromList(self.readValue() as! [Any?]) case 166: - return FlutterUploadRequest.fromList(self.readValue() as! [Any?]) + return FlutterUploadImageInfo.fromList(self.readValue() as! [Any?]) case 167: - return FlutterValidDocument.fromList(self.readValue() as! [Any?]) + return FlutterUploadRequest.fromList(self.readValue() as! [Any?]) case 168: + return FlutterValidDocument.fromList(self.readValue() as! [Any?]) + case 169: return FlutterValidDocumentsResponse.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) @@ -1802,21 +1870,24 @@ private class SmileIDApiCodecWriter: FlutterStandardWriter { } else if let value = value as? FlutterSmartSelfieJobStatusResponse { super.writeByte(163) super.writeValue(value.toList()) - } else if let value = value as? FlutterSuspectUser { + } else if let value = value as? FlutterSmartSelfieResponse { super.writeByte(164) super.writeValue(value.toList()) - } else if let value = value as? FlutterUploadImageInfo { + } else if let value = value as? FlutterSuspectUser { super.writeByte(165) super.writeValue(value.toList()) - } else if let value = value as? FlutterUploadRequest { + } else if let value = value as? FlutterUploadImageInfo { super.writeByte(166) super.writeValue(value.toList()) - } else if let value = value as? FlutterValidDocument { + } else if let value = value as? FlutterUploadRequest { super.writeByte(167) super.writeValue(value.toList()) - } else if let value = value as? FlutterValidDocumentsResponse { + } else if let value = value as? FlutterValidDocument { super.writeByte(168) super.writeValue(value.toList()) + } else if let value = value as? FlutterValidDocumentsResponse { + super.writeByte(169) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -1854,6 +1925,8 @@ protocol SmileIDApi { func doEnhancedKyc(request: FlutterEnhancedKycRequest, completion: @escaping (Result) -> Void) func doEnhancedKycAsync(request: FlutterEnhancedKycRequest, completion: @escaping (Result) -> Void) func getSmartSelfieJobStatus(request: FlutterJobStatusRequest, completion: @escaping (Result) -> Void) + func doSmartSelfieEnrollment(signature: String, timestamp: String, selfieImage: String, livenessImages: [String], userId: String, partnerParams: [String?: String?]?, callbackUrl: String?, sandboxResult: Int64?, allowNewEnroll: Bool?, completion: @escaping (Result) -> Void) + func doSmartSelfieAuthentication(signature: String, timestamp: String, selfieImage: String, livenessImages: [String], userId: String, partnerParams: [String?: String?]?, callbackUrl: String?, sandboxResult: Int64?, completion: @escaping (Result) -> Void) func getDocumentVerificationJobStatus(request: FlutterJobStatusRequest, completion: @escaping (Result) -> Void) func getBiometricKycJobStatus(request: FlutterJobStatusRequest, completion: @escaping (Result) -> Void) func getEnhancedDocumentVerificationJobStatus(request: FlutterJobStatusRequest, completion: @escaping (Result) -> Void) @@ -2105,6 +2178,55 @@ class SmileIDApiSetup { } else { getSmartSelfieJobStatusChannel.setMessageHandler(nil) } + let doSmartSelfieEnrollmentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.smileid.SmileIDApi.doSmartSelfieEnrollment", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + doSmartSelfieEnrollmentChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let signatureArg = args[0] as! String + let timestampArg = args[1] as! String + let selfieImageArg = args[2] as! String + let livenessImagesArg = args[3] as! [String] + let userIdArg = args[4] as! String + let partnerParamsArg: [String?: String?]? = nilOrValue(args[5]) + let callbackUrlArg: String? = nilOrValue(args[6]) + let sandboxResultArg: Int64? = isNullish(args[7]) ? nil : (args[7] is Int64? ? args[7] as! Int64? : Int64(args[7] as! Int32)) + let allowNewEnrollArg: Bool? = nilOrValue(args[8]) + api.doSmartSelfieEnrollment(signature: signatureArg, timestamp: timestampArg, selfieImage: selfieImageArg, livenessImages: livenessImagesArg, userId: userIdArg, partnerParams: partnerParamsArg, callbackUrl: callbackUrlArg, sandboxResult: sandboxResultArg, allowNewEnroll: allowNewEnrollArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + doSmartSelfieEnrollmentChannel.setMessageHandler(nil) + } + let doSmartSelfieAuthenticationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.smileid.SmileIDApi.doSmartSelfieAuthentication", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + doSmartSelfieAuthenticationChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let signatureArg = args[0] as! String + let timestampArg = args[1] as! String + let selfieImageArg = args[2] as! String + let livenessImagesArg = args[3] as! [String] + let userIdArg = args[4] as! String + let partnerParamsArg: [String?: String?]? = nilOrValue(args[5]) + let callbackUrlArg: String? = nilOrValue(args[6]) + let sandboxResultArg: Int64? = isNullish(args[7]) ? nil : (args[7] is Int64? ? args[7] as! Int64? : Int64(args[7] as! Int32)) + api.doSmartSelfieAuthentication(signature: signatureArg, timestamp: timestampArg, selfieImage: selfieImageArg, livenessImages: livenessImagesArg, userId: userIdArg, partnerParams: partnerParamsArg, callbackUrl: callbackUrlArg, sandboxResult: sandboxResultArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + doSmartSelfieAuthenticationChannel.setMessageHandler(nil) + } let getDocumentVerificationJobStatusChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.smileid.SmileIDApi.getDocumentVerificationJobStatus", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getDocumentVerificationJobStatusChannel.setMessageHandler { message, reply in diff --git a/ios/Classes/SmileIDPlugin.swift b/ios/Classes/SmileIDPlugin.swift index 609060fd..064dac59 100644 --- a/ios/Classes/SmileIDPlugin.swift +++ b/ios/Classes/SmileIDPlugin.swift @@ -183,6 +183,94 @@ public class SmileIDPlugin: NSObject, FlutterPlugin, SmileIDApi { .store(in: &subscribers) } + func doSmartSelfieEnrollment( + signature: String, + timestamp: String, + selfieImage: String, + livenessImages: [String], + userId: String, + partnerParams: [String? : String?]?, + callbackUrl: String?, + sandboxResult: Int64?, + allowNewEnroll: Bool?, + completion: @escaping (Result) -> Void + ) { + SmileID.api.doSmartSelfieEnrollment( + signature: signature, + timestamp: timestamp, + selfieImage: MultipartBody( + withImage: getFile(atPath: selfieImage)!, + forKey: URL(fileURLWithPath: selfieImage).lastPathComponent, + forName: URL(fileURLWithPath: selfieImage).lastPathComponent + )!, + livenessImages: livenessImages.map { + MultipartBody( + withImage: getFile(atPath: $0)!, + forKey: URL(fileURLWithPath: $0).lastPathComponent, + forName: URL(fileURLWithPath: $0).lastPathComponent + )! + }, + userId: userId, + partnerParams: convertNullableMapToNonNull(data: partnerParams), + callbackUrl: callbackUrl, + sandboxResult: sandboxResult.map { Int($0) }, + allowNewEnroll: allowNewEnroll + ).sink(receiveCompletion: { status in + switch status { + case .failure(let error): + completion(.failure(error)) + default: + break + } + + }, receiveValue: { response in + completion(.success(response.toResponse())) + }).store(in: &subscribers) + } + + func doSmartSelfieAuthentication( + signature: String, + timestamp: String, + selfieImage: String, + livenessImages: [String], + userId: String, + partnerParams: [String? : String?]?, + callbackUrl: String?, + sandboxResult: Int64?, + completion: @escaping (Result) -> Void + ) { + SmileID.api.doSmartSelfieAuthentication( + signature: signature, + timestamp: timestamp, + userId: userId, + selfieImage: MultipartBody( + withImage: getFile(atPath: selfieImage)!, + forKey: URL(fileURLWithPath: selfieImage).lastPathComponent, + forName: URL(fileURLWithPath: selfieImage).lastPathComponent + )!, + livenessImages: livenessImages.map { + MultipartBody( + withImage: getFile(atPath: $0)!, + forKey: URL(fileURLWithPath: $0).lastPathComponent, + forName: URL(fileURLWithPath: $0).lastPathComponent + )! + }, + partnerParams: convertNullableMapToNonNull(data: partnerParams), + callbackUrl: callbackUrl, + sandboxResult: sandboxResult.map { Int($0) } + ).sink(receiveCompletion: { status in + switch status { + case .failure(let error): + completion(.failure(error)) + default: + break + } + + }, receiveValue: { response in + completion(.success(response.toResponse())) + }).store(in: &subscribers) + } + func getSmartSelfieJobStatus( request: FlutterJobStatusRequest, completion: @escaping (Result) -> Void diff --git a/lib/smile_id_service.dart b/lib/smile_id_service.dart index fa40d4e1..dbdfd4d0 100644 --- a/lib/smile_id_service.dart +++ b/lib/smile_id_service.dart @@ -51,6 +51,56 @@ class SmileIDService { return platformInterface.getSmartSelfieJobStatus(request); } + /// Perform a synchronous SmartSelfie Enrollment. The response will include the final result of + /// the enrollment. + Future doSmartSelfieEnrollment( + String signature, + String timestamp, + String selfieImage, + List livenessImages, + String userId, + Map? partnerParams, + String? callbackUrl, + int? sandboxResult, + bool? allowNewEnroll + ) { + return platformInterface.doSmartSelfieEnrollment( + signature, + timestamp, + selfieImage, + livenessImages, + userId, + partnerParams, + callbackUrl, + sandboxResult, + allowNewEnroll + ); + } + + /// Perform a synchronous SmartSelfie Authentication. The response will include the final result + /// of the authentication. + Future doSmartSelfieAuthentication( + String signature, + String timestamp, + String selfieImage, + List livenessImages, + String userId, + Map? partnerParams, + String? callbackUrl, + int? sandboxResult, + ) { + return platformInterface.doSmartSelfieAuthentication( + signature, + timestamp, + selfieImage, + livenessImages, + userId, + partnerParams, + callbackUrl, + sandboxResult + ); + } + /// Fetches the status of a Job. This can be used to check if a Job is complete, and if so, /// whether it was successful. This should be called when the Job is known to be a /// Document Verification. diff --git a/lib/smileid_messages.g.dart b/lib/smileid_messages.g.dart index cc80fb10..1395bb7f 100644 --- a/lib/smileid_messages.g.dart +++ b/lib/smileid_messages.g.dart @@ -24,6 +24,11 @@ enum FlutterJobType { smartSelfieAuthentication, } +enum FlutterJobTypeV2 { + smartSelfieAuthentication, + smartSelfieEnrollment, +} + enum FlutterImageType { selfieJpgFile, idCardJpgFile, @@ -54,6 +59,13 @@ enum FlutterActionResult { unknown, } +enum FlutterSmartSelfieStatus { + approved, + pending, + rejected, + unknown, +} + /// Custom values specific to partners can be placed in [extras] class FlutterPartnerParams { FlutterPartnerParams({ @@ -946,6 +958,72 @@ class FlutterSmartSelfieJobStatusResponse { } } +class FlutterSmartSelfieResponse { + FlutterSmartSelfieResponse({ + required this.code, + required this.createdAt, + required this.jobId, + required this.jobType, + required this.message, + required this.partnerId, + this.partnerParams, + required this.status, + required this.updatedAt, + required this.userId, + }); + + String code; + + String createdAt; + + String jobId; + + FlutterJobTypeV2 jobType; + + String message; + + String partnerId; + + Map? partnerParams; + + FlutterSmartSelfieStatus status; + + String updatedAt; + + String userId; + + Object encode() { + return [ + code, + createdAt, + jobId, + jobType.index, + message, + partnerId, + partnerParams, + status.index, + updatedAt, + userId, + ]; + } + + static FlutterSmartSelfieResponse decode(Object result) { + result as List; + return FlutterSmartSelfieResponse( + code: result[0]! as String, + createdAt: result[1]! as String, + jobId: result[2]! as String, + jobType: FlutterJobTypeV2.values[result[3]! as int], + message: result[4]! as String, + partnerId: result[5]! as String, + partnerParams: (result[6] as Map?)?.cast(), + status: FlutterSmartSelfieStatus.values[result[7]! as int], + updatedAt: result[8]! as String, + userId: result[9]! as String, + ); + } +} + class FlutterDocumentVerificationJobResult { FlutterDocumentVerificationJobResult({ required this.actions, @@ -1977,21 +2055,24 @@ class _SmileIDApiCodec extends StandardMessageCodec { } else if (value is FlutterSmartSelfieJobStatusResponse) { buffer.putUint8(163); writeValue(buffer, value.encode()); - } else if (value is FlutterSuspectUser) { + } else if (value is FlutterSmartSelfieResponse) { buffer.putUint8(164); writeValue(buffer, value.encode()); - } else if (value is FlutterUploadImageInfo) { + } else if (value is FlutterSuspectUser) { buffer.putUint8(165); writeValue(buffer, value.encode()); - } else if (value is FlutterUploadRequest) { + } else if (value is FlutterUploadImageInfo) { buffer.putUint8(166); writeValue(buffer, value.encode()); - } else if (value is FlutterValidDocument) { + } else if (value is FlutterUploadRequest) { buffer.putUint8(167); writeValue(buffer, value.encode()); - } else if (value is FlutterValidDocumentsResponse) { + } else if (value is FlutterValidDocument) { buffer.putUint8(168); writeValue(buffer, value.encode()); + } else if (value is FlutterValidDocumentsResponse) { + buffer.putUint8(169); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -2073,14 +2154,16 @@ class _SmileIDApiCodec extends StandardMessageCodec { case 163: return FlutterSmartSelfieJobStatusResponse.decode(readValue(buffer)!); case 164: - return FlutterSuspectUser.decode(readValue(buffer)!); + return FlutterSmartSelfieResponse.decode(readValue(buffer)!); case 165: - return FlutterUploadImageInfo.decode(readValue(buffer)!); + return FlutterSuspectUser.decode(readValue(buffer)!); case 166: - return FlutterUploadRequest.decode(readValue(buffer)!); + return FlutterUploadImageInfo.decode(readValue(buffer)!); case 167: - return FlutterValidDocument.decode(readValue(buffer)!); + return FlutterUploadRequest.decode(readValue(buffer)!); case 168: + return FlutterValidDocument.decode(readValue(buffer)!); + case 169: return FlutterValidDocumentsResponse.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -2463,6 +2546,60 @@ class SmileIDApi { } } + Future doSmartSelfieEnrollment(String signature, String timestamp, String selfieImage, List livenessImages, String userId, Map? partnerParams, String? callbackUrl, int? sandboxResult, bool? allowNewEnroll) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.smileid.SmileIDApi.doSmartSelfieEnrollment'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([signature, timestamp, selfieImage, livenessImages, userId, partnerParams, callbackUrl, sandboxResult, allowNewEnroll]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as FlutterSmartSelfieResponse?)!; + } + } + + Future doSmartSelfieAuthentication(String signature, String timestamp, String selfieImage, List livenessImages, String userId, Map? partnerParams, String? callbackUrl, int? sandboxResult) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.smileid.SmileIDApi.doSmartSelfieAuthentication'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([signature, timestamp, selfieImage, livenessImages, userId, partnerParams, callbackUrl, sandboxResult]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as FlutterSmartSelfieResponse?)!; + } + } + Future getDocumentVerificationJobStatus(FlutterJobStatusRequest request) async { const String __pigeon_channelName = 'dev.flutter.pigeon.smileid.SmileIDApi.getDocumentVerificationJobStatus'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( diff --git a/pigeon/messages.dart b/pigeon/messages.dart index e4779104..7a5340c9 100644 --- a/pigeon/messages.dart +++ b/pigeon/messages.dart @@ -3,12 +3,14 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/smileid_messages.g.dart', dartOptions: DartOptions(), - kotlinOut: 'android/src/main/kotlin/com/smileidentity/flutter/generated/SmileIDMessages.g.kt', + kotlinOut: + 'android/src/main/kotlin/com/smileidentity/flutter/generated/SmileIDMessages.g.kt', kotlinOptions: KotlinOptions(errorClassName: "SmileFlutterError"), swiftOut: 'ios/Classes/SmileIDMessages.g.swift', swiftOptions: SwiftOptions(), dartPackageName: 'smileid', )) + enum FlutterJobType { enhancedKyc, documentVerification, @@ -18,6 +20,8 @@ enum FlutterJobType { smartSelfieAuthentication } +enum FlutterJobTypeV2 { smartSelfieAuthentication, smartSelfieEnrollment } + /// Custom values specific to partners can be placed in [extras] class FlutterPartnerParams { final FlutterJobType? jobType; @@ -399,6 +403,34 @@ class FlutterSmartSelfieJobStatusResponse { }); } +enum FlutterSmartSelfieStatus { approved, pending, rejected, unknown } + +class FlutterSmartSelfieResponse { + final String code; + final String createdAt; + final String jobId; + final FlutterJobTypeV2 jobType; + final String message; + final String partnerId; + final Map? partnerParams; + final FlutterSmartSelfieStatus status; + final String updatedAt; + final String userId; + + FlutterSmartSelfieResponse({ + required this.code, + required this.createdAt, + required this.jobId, + required this.jobType, + required this.message, + required this.partnerId, + required this.partnerParams, + required this.status, + required this.updatedAt, + required this.userId, + }); +} + class FlutterDocumentVerificationJobResult { final FlutterActions actions; final String resultCode; @@ -774,7 +806,8 @@ abstract class SmileIDApi { void submitJob(String jobId, bool deleteFilesOnSuccess); @async - FlutterAuthenticationResponse authenticate(FlutterAuthenticationRequest request); + FlutterAuthenticationResponse authenticate( + FlutterAuthenticationRequest request); @async FlutterPrepUploadResponse prepUpload(FlutterPrepUploadRequest request); @@ -786,27 +819,57 @@ abstract class SmileIDApi { FlutterEnhancedKycResponse doEnhancedKyc(FlutterEnhancedKycRequest request); @async - FlutterEnhancedKycAsyncResponse doEnhancedKycAsync(FlutterEnhancedKycRequest request); + FlutterEnhancedKycAsyncResponse doEnhancedKycAsync( + FlutterEnhancedKycRequest request); @async - FlutterSmartSelfieJobStatusResponse getSmartSelfieJobStatus(FlutterJobStatusRequest request); + FlutterSmartSelfieJobStatusResponse getSmartSelfieJobStatus( + FlutterJobStatusRequest request); + + @async + FlutterSmartSelfieResponse doSmartSelfieEnrollment( + String signature, + String timestamp, + String selfieImage, + List livenessImages, + String userId, + Map? partnerParams, + String? callbackUrl, + int? sandboxResult, + bool? allowNewEnroll, + ); + + @async + FlutterSmartSelfieResponse doSmartSelfieAuthentication( + String signature, + String timestamp, + String selfieImage, + List livenessImages, + String userId, + Map? partnerParams, + String? callbackUrl, + int? sandboxResult, + ); @async FlutterDocumentVerificationJobStatusResponse getDocumentVerificationJobStatus( FlutterJobStatusRequest request); @async - FlutterBiometricKycJobStatusResponse getBiometricKycJobStatus(FlutterJobStatusRequest request); + FlutterBiometricKycJobStatusResponse getBiometricKycJobStatus( + FlutterJobStatusRequest request); @async FlutterEnhancedDocumentVerificationJobStatusResponse getEnhancedDocumentVerificationJobStatus( FlutterJobStatusRequest request); @async - FlutterProductsConfigResponse getProductsConfig(FlutterProductsConfigRequest request); + FlutterProductsConfigResponse getProductsConfig( + FlutterProductsConfigRequest request); @async - FlutterValidDocumentsResponse getValidDocuments(FlutterProductsConfigRequest request); + FlutterValidDocumentsResponse getValidDocuments( + FlutterProductsConfigRequest request); @async FlutterServicesResponse getServices();