From b41acaba5004dd48c95c926f31a6e62ad798724b Mon Sep 17 00:00:00 2001 From: JNdhlovu Date: Wed, 25 Sep 2024 17:59:11 +0200 Subject: [PATCH] Feature/smilescreens (#236) * feat: optional confirmation on docv * feat: confirm optional * modify access for needed utis methos in wrappers * feat: changelog * chore: undo signing * feat: lint warnings * chore: lint * chore: lint * chore: bump version for release * chore: bump version for release * chore: bump version for release * chore: bump version for release --- CHANGELOG.md | 8 +- Example/Podfile.lock | 4 +- SmileID.podspec | 4 +- .../View/DocumentCaptureScreen.swift | 22 ++- .../Classes/Helpers/LocalStorage.swift | 2 +- .../Networking/Models/v2/Metadata.swift | 125 +++++++++--------- Sources/SmileID/Classes/SmileID.swift | 62 ++++----- Sources/SmileID/Classes/Util.swift | 36 ++--- 8 files changed, 136 insertions(+), 127 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7db50d9d..7d41560e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Release Notes -## Unreleased +## 10.2.11 ### Added * Add metadata support +* Modified access for util methods for use in wrappers ## 10.2.10 @@ -20,6 +21,11 @@ ### Added * Document capture cleanup and optionally showing confirmation and returning the captured image if false +## 10.2.9 + +### Added +* Document capture cleanup and optionally showing confirmation and returning the captured image if false + ## 10.2.8 ### Changed diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 4b638dab..c66eab95 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -12,7 +12,7 @@ PODS: - Sentry (8.36.0): - Sentry/Core (= 8.36.0) - Sentry/Core (8.36.0) - - SmileID (10.2.10): + - SmileID (10.2.11): - FingerprintJS - lottie-ios (~> 4.4.2) - ZIPFoundation (~> 0.9) @@ -51,7 +51,7 @@ SPEC CHECKSUMS: lottie-ios: fcb5e73e17ba4c983140b7d21095c834b3087418 netfox: 9d5cc727fe7576c4c7688a2504618a156b7d44b7 Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57 - SmileID: 41b6c7ea0ec44116c43e94a1221acde297bd3b08 + SmileID: 6222ec839420b69917ae23ea11876d78d2529620 SwiftLint: 3fe909719babe5537c552ee8181c0031392be933 ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c diff --git a/SmileID.podspec b/SmileID.podspec index 0547742c..4f5f7f6a 100644 --- a/SmileID.podspec +++ b/SmileID.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'SmileID' - s.version = '10.2.10' + s.version = '10.2.11' s.summary = 'The Official Smile Identity iOS SDK.' s.homepage = 'https://docs.usesmileid.com/integration-options/mobile/ios-v10-beta' s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'Japhet' => 'japhet@usesmileid.com', 'Juma Allan' => 'juma@usesmileid.com', 'Vansh Gandhi' => 'vansh@usesmileid.com'} - s.source = { :git => "https://github.com/smileidentity/ios.git", :tag => "v10.2.10" } + s.source = { :git => "https://github.com/smileidentity/ios.git", :tag => "v10.2.11" } s.ios.deployment_target = '13.0' s.dependency 'ZIPFoundation', '~> 0.9' s.dependency 'FingerprintJS' diff --git a/Sources/SmileID/Classes/DocumentVerification/View/DocumentCaptureScreen.swift b/Sources/SmileID/Classes/DocumentVerification/View/DocumentCaptureScreen.swift index e51ce41e..53682126 100644 --- a/Sources/SmileID/Classes/DocumentVerification/View/DocumentCaptureScreen.swift +++ b/Sources/SmileID/Classes/DocumentVerification/View/DocumentCaptureScreen.swift @@ -21,10 +21,10 @@ public struct DocumentCaptureScreen: View { let onConfirm: (Data) -> Void let onError: (Error) -> Void let onSkip: () -> Void - + @EnvironmentObject private var localMetadata: LocalMetadata @ObservedObject private var viewModel: DocumentCaptureViewModel - + public init( side: DocumentCaptureSide, showInstructions: Bool, @@ -55,14 +55,14 @@ public struct DocumentCaptureScreen: View { self.onConfirm = onConfirm self.onError = onError self.onSkip = onSkip - + viewModel = DocumentCaptureViewModel( knownAspectRatio: knownIdAspectRatio, side: side, localMetadata: LocalMetadata() ) } - + public var body: some View { ZStack { if let captureError = viewModel.captureError { @@ -78,7 +78,7 @@ public struct DocumentCaptureScreen: View { viewModel.updateLocalMetadata(localMetadata) } } - + private var instructionsView: some View { DocumentCaptureInstructionsScreen( heroImage: instructionsHeroImage, @@ -95,11 +95,11 @@ public struct DocumentCaptureScreen: View { ImagePicker(onImageSelected: viewModel.onPhotoSelectedFromGallery) } } - + private func errorView(error: Error) -> some View { Color.clear.onAppear { onError(error) } } - + private func confirmationView(imageToConfirm: Data) -> some View { Group { if showConfirmation { @@ -124,7 +124,7 @@ public struct DocumentCaptureScreen: View { } } } - + private var captureView: some View { CaptureScreenContent( title: captureTitleText, @@ -142,9 +142,7 @@ public struct DocumentCaptureScreen: View { message: Text(alert.message ?? ""), primaryButton: .default( Text(SmileIDResourcesHelper.localizedString(for: "Camera.Unauthorized.PrimaryAction")), - action: { - viewModel.openSettings() - } + action: { viewModel.openSettings() } ), secondaryButton: .cancel() ) @@ -161,7 +159,7 @@ struct CaptureScreenContent: View { let showManualCaptureButton: Bool let cameraManager: CameraManager let onCaptureClick: () -> Void - + var body: some View { VStack(alignment: .center, spacing: 16) { ZStack { diff --git a/Sources/SmileID/Classes/Helpers/LocalStorage.swift b/Sources/SmileID/Classes/Helpers/LocalStorage.swift index c040b772..811716fa 100644 --- a/Sources/SmileID/Classes/Helpers/LocalStorage.swift +++ b/Sources/SmileID/Classes/Helpers/LocalStorage.swift @@ -62,7 +62,7 @@ public class LocalStorage { try createSmileFile(to: jobId, name: filename(for: FileType.liveness.name), file: data) } - static func createDocumentFile( + public static func createDocumentFile( jobId: String, fileType: FileType, document data: Data diff --git a/Sources/SmileID/Classes/Networking/Models/v2/Metadata.swift b/Sources/SmileID/Classes/Networking/Models/v2/Metadata.swift index 46cdddc9..06ff2d7c 100644 --- a/Sources/SmileID/Classes/Networking/Models/v2/Metadata.swift +++ b/Sources/SmileID/Classes/Networking/Models/v2/Metadata.swift @@ -3,11 +3,10 @@ import UIKit public struct Metadata: Codable { public var items: [Metadatum] - public init(items: [Metadatum]) { self.items = items } - + public static func `default`() -> Metadata { Metadata(items: [ .sdk, @@ -15,11 +14,11 @@ public struct Metadata: Codable { .clientIP, .fingerprint, .deviceModel, - .deviceOS + .deviceOS, ]) } - - public mutating func removeAllOfType(_ type: T.Type) { + + public mutating func removeAllOfType(_: T.Type) { items.removeAll { $0 is T } } } @@ -33,116 +32,116 @@ extension Array where Element == Metadatum { public class Metadatum: Codable { public let name: String public let value: String - + public init(name: String, value: String) { self.name = name self.value = value } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) value = try container.decode(String.self, forKey: .value) } - + private enum CodingKeys: String, CodingKey { case name, value } - + public static let sdk = Metadatum(name: "sdk", value: "iOS") public static let sdkVersion = Metadatum(name: "sdk_version", value: SmileID.version) public static let clientIP = Metadatum(name: "client_ip", value: getIPAddress(useIPv4: true)) public static let fingerprint = Metadatum(name: "fingerprint", value: SmileID.deviceId) public static let deviceModel = Metadatum(name: "device_model", value: UIDevice.current.modelName) public static let deviceOS = Metadatum(name: "device_os", value: UIDevice.current.systemVersion) - + public class SelfieImageOrigin: Metadatum { public init(cameraFacing: CameraFacingValue) { super.init(name: "selfie_image_origin", value: cameraFacing.rawValue) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } - + public class SelfieCaptureDuration: Metadatum { public init(duration: TimeInterval) { super.init(name: "selfie_capture_duration_ms", value: String(Int(duration * 1000))) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } - + public class DocumentFrontImageOrigin: Metadatum { public init(origin: DocumentImageOriginValue) { super.init(name: "document_front_image_origin", value: origin.rawValue) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } - + public class DocumentBackImageOrigin: Metadatum { public init(origin: DocumentImageOriginValue) { super.init(name: "document_back_image_origin", value: origin.rawValue) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } - + public class DocumentFrontCaptureRetries: Metadatum { public init(retries: Int) { super.init(name: "document_front_capture_retries", value: String(retries)) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } - + public class DocumentBackCaptureRetries: Metadatum { public init(retries: Int) { super.init(name: "document_back_capture_retries", value: String(retries)) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } - + public class DocumentFrontCaptureDuration: Metadatum { public init(duration: TimeInterval) { super.init(name: "document_front_capture_duration_ms", value: String(Int(duration * 1000))) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } - + public class DocumentBackCaptureDuration: Metadatum { public init(duration: TimeInterval) { super.init(name: "document_back_capture_duration_ms", value: String(Int(duration * 1000))) } - - required public init(from decoder: Decoder) throws { + + public required init(from decoder: Decoder) throws { try super.init(from: decoder) } } } + public enum DocumentImageOriginValue: String { - case gallery = "gallery" + case gallery case cameraAutoCapture = "camera_auto_capture" case cameraManualCapture = "camera_manual_capture" - public var value: String { - return self.rawValue + return rawValue } } @@ -152,33 +151,36 @@ public enum CameraFacingValue: String, Codable { } func getIPAddress(useIPv4: Bool) -> String { - var address: String = "" + var address = "" var ifaddr: UnsafeMutablePointer? - + guard getifaddrs(&ifaddr) == 0 else { return "" } - + var ptr = ifaddr while ptr != nil { defer { ptr = ptr?.pointee.ifa_next } - + guard let interface = ptr?.pointee else { return "" } - + let addrFamily = interface.ifa_addr.pointee.sa_family if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) { let name = String(cString: interface.ifa_name) - if name == "en0" || name == "en1" || name == "pdp_ip0" || name == "pdp_ip1" || name == "pdp_ip2" || name == "pdp_ip3" { + if name == "en0" || name == "en1" || name == "pdp_ip0" + || name == "pdp_ip1" || name == "pdp_ip2" || name == "pdp_ip3" + { var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) address = String(cString: hostname) - + if (useIPv4 && addrFamily == UInt8(AF_INET)) || - (!useIPv4 && addrFamily == UInt8(AF_INET6)) { + (!useIPv4 && addrFamily == UInt8(AF_INET6)) + { if !useIPv4 { if let percentIndex = address.firstIndex(of: "%") { address = String(address[.. String { } } } - + freeifaddrs(ifaddr) return address } public class LocalMetadata: ObservableObject { - @Published var metadata: Metadata = Metadata.default() - + @Published var metadata: Metadata = .default() + public init() {} + func addMetadata(_ newMetadata: Metadatum) { metadata.items.append(newMetadata) objectWillChange.send() @@ -208,25 +211,27 @@ public class LocalMetadata: ObservableObject { extension UIDevice { var modelName: String { #if targetEnvironment(simulator) - let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]! + let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]! #else - var systemInfo = utsname() - uname(&systemInfo) - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) - } + var systemInfo = utsname() + uname(&systemInfo) + let machineMirror = Mirror(reflecting: systemInfo.machine) + let identifier = machineMirror.children.reduce("") { identifier, element in + guard let value = element.value as? Int8, value != 0 else { return identifier } + return identifier + String(UnicodeScalar(UInt8(value))) + } #endif return DeviceModel.all.first { $0.identifier == identifier }?.model ?? identifier - } - + } + struct DeviceModel: Decodable { let identifier: String let model: String static var all: [DeviceModel] { - let currentDevice = UIDevice.current.name - guard let devicesUrl = SmileIDResourcesHelper.bundle.url(forResource: "devicemodels", withExtension: "json") else { return [] } + _ = UIDevice.current.name + guard let devicesUrl = SmileIDResourcesHelper.bundle.url( + forResource: "devicemodels", withExtension: "json" + ) else { return [] } do { let data = try Data(contentsOf: devicesUrl) let devices = try JSONDecoder().decode([DeviceModel].self, from: data) diff --git a/Sources/SmileID/Classes/SmileID.swift b/Sources/SmileID/Classes/SmileID.swift index d2f6502d..ab0efb89 100644 --- a/Sources/SmileID/Classes/SmileID.swift +++ b/Sources/SmileID/Classes/SmileID.swift @@ -1,17 +1,17 @@ +import FingerprintJS import Foundation import SwiftUI import UIKit -import FingerprintJS public class SmileID { /// The default value for `timeoutIntervalForRequest` for URLSession default configuration. public static let defaultRequestTimeout: TimeInterval = 60 - public static let version = "10.2.10" + public static let version = "10.2.11" @Injected var injectedApi: SmileIDServiceable public static var configuration: Config { config } - + public static var api: SmileIDServiceable { SmileID.instance.injectedApi } - + static let instance: SmileID = { let container = DependencyContainer.shared container.register(SmileIDServiceable.self) { SmileIDService() } @@ -25,7 +25,7 @@ public class SmileID { let instance = SmileID() return instance }() - + /// A private static constant that initializes a `URLSession` with a default configuration. /// This `URLSession` is used for creating `URLSessionDataTask`s in the networking layer. /// The session configuration sets the timeout interval for requests to the value specified by `SmileID.requestTimeout`. @@ -37,9 +37,9 @@ public class SmileID { let session = URLSession(configuration: configuration) return session }() - + private init() {} - + public private(set) static var config: Config! public private(set) static var useSandbox = false public private(set) static var allowOfflineMode = false @@ -50,7 +50,7 @@ public class SmileID { private(set) static var localizableStrings: SmileIDLocalizableStrings? /// The timeout interval for requests. This value is initialized to the `defaultRequestTimeout`. private(set) static var requestTimeout: TimeInterval = SmileID.defaultRequestTimeout - + /// This method initializes SmileID. Invoke this method once in your application lifecycle /// before calling any other SmileID methods. /// - Parameters: @@ -71,7 +71,7 @@ public class SmileID { requestTimeout: requestTimeout ) } - + /// This method initializes SmileID. Invoke this method once in your application lifecylce /// before calling any other SmileID methods. /// - Parameters: @@ -103,7 +103,7 @@ public class SmileID { } } } - + /// Sets the state of offline mode for the SDK. /// This function enables or disables the SDK's ability to operate in offline mode, /// where it can continue functioning without an active internet connection. When offline mode @@ -117,17 +117,17 @@ public class SmileID { public class func setAllowOfflineMode(allowOfflineMode: Bool) { SmileID.allowOfflineMode = allowOfflineMode } - + /// Retrieves a list of unsubmitted job IDs. public class func getUnsubmittedJobs() -> [String] { LocalStorage.getUnsubmittedJobs() } - + /// Retrieves a list of submitted job IDs. public class func getSubmittedJobs() -> [String] { LocalStorage.getSubmittedJobs() } - + /// Initiates the cleanup process for a single job by its ID. /// This is a convenience method that wraps the cleanup process, allowing for a single job ID /// to be specified for cleanup. @@ -136,7 +136,7 @@ public class SmileID { public class func cleanup(jobId: String) throws { try cleanup(jobIds: [jobId]) } - + /// Initiates the cleanup process for multiple jobs by their IDs. /// If no IDs are provided, a default cleanup process is initiated that may target /// specific jobs based on the implementation in com.smileidentity.util.cleanup. @@ -150,7 +150,7 @@ public class SmileID { try LocalStorage.deleteAll() } } - + /// Submits a previously captured job to SmileID for processing. /// /// - Parameters: @@ -192,12 +192,12 @@ public class SmileID { ) } catch let error as SmileIDError { switch error { - case .api("2215", _): - prepUploadResponse = try await SmileID.api.prepUpload( - request: prepUploadRequest.copy(retry: "true") - ) - default: - throw error + case .api("2215", _): + prepUploadResponse = try await SmileID.api.prepUpload( + request: prepUploadRequest.copy(retry: "true") + ) + default: + throw error } } let allFiles: [URL] @@ -238,28 +238,28 @@ public class SmileID { } } } - + /// Set the callback URL for all submitted jobs. If no value is set, the default callback URL /// from the partner portal will be used. /// - Parameter url: A valid URL pointing to your server public class func setCallbackUrl(url: URL?) { SmileID.callbackUrl = url?.absoluteString ?? "" } - + /// Apply theme /// - Parameter theme: A `SmileIdTheme` used to override the colors and fonts used within the /// SDK. If no value is set, the default theme will be used. public class func apply(_ theme: SmileIdTheme) { self.theme = theme } - + /// Apply localizable strings /// - Parameter localizableStrings: A `SmileIDLocalizableStrings` used to override all copy /// used within the SDK. if no value is set, the default copy will be used. public class func apply(_ localizableStrings: SmileIDLocalizableStrings) { self.localizableStrings = localizableStrings } - + /// Load the Config object from a json file /// - Parameter resourceName: The name of the json file. Defaults to `smile_config` /// - Returns: A `Config` object @@ -270,7 +270,7 @@ public class SmileID { return try! decoder.decode(Config.self, from: Data(contentsOf: configUrl)) // swiftlint:enable force_try } - + /// Perform a SmartSelfie™ Enrollment /// /// Docs: https://docs.usesmileid.com/products/for-individuals-kyc/biometric-authentication @@ -314,7 +314,7 @@ public class SmileID { onResult: delegate ) } - + /// Perform a SmartSelfie™ Authentication /// /// Docs: https://docs.usesmileid.com/products/for-individuals-kyc/biometric-authentication @@ -358,7 +358,7 @@ public class SmileID { onResult: delegate ) } - + /// Perform a Document Verification /// - Parameters: /// - userId: The user ID to associate with the Document Verification. Most often, this will @@ -419,7 +419,7 @@ public class SmileID { onResult: delegate ) } - + /// Perform an Enhanced Document Verification /// - Parameters: /// - userId: The user ID to associate with the Document Verification. Most often, this will @@ -480,7 +480,7 @@ public class SmileID { onResult: delegate ) } - + public class func consentScreen( partnerIcon: UIImage, partnerName: String, @@ -500,7 +500,7 @@ public class SmileID { onConsentDenied: onConsentDenied ) } - + /// Perform a Biometric KYC: Verify the ID information of your user and confirm that the ID /// actually belongs to the user. This is achieved by comparing the user's SmartSelfie™ to the /// user's photo in an ID authority database diff --git a/Sources/SmileID/Classes/Util.swift b/Sources/SmileID/Classes/Util.swift index 553bf879..d2d612c3 100644 --- a/Sources/SmileID/Classes/Util.swift +++ b/Sources/SmileID/Classes/Util.swift @@ -17,7 +17,7 @@ public extension View { /// Cuts out the given shape from the view. This is used instead of a ZStack with a shape and a /// blendMode of .destinationOut because that causes issues on iOS 14 devices func cutout(_ shape: S) -> some View { - self.clipShape( + clipShape( StackedShape(bottom: Rectangle(), top: shape), style: FillStyle(eoFill: true) ) @@ -27,7 +27,7 @@ public extension View { private struct StackedShape: Shape { var bottom: Bottom var top: Top - + func path(in rect: CGRect) -> Path { Path { path in path.addPath(bottom.path(in: rect)) @@ -38,12 +38,12 @@ private struct StackedShape: Shape { extension String: Error {} -enum FileType: String { +public enum FileType: String { case selfie = "si_selfie" case liveness = "si_liveness" case documentFront = "si_document_front" case documentBack = "si_document_back" - + var name: String { return rawValue } @@ -51,21 +51,21 @@ enum FileType: String { extension String { func nilIfEmpty() -> String? { - return self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self + return trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self } } func toErrorMessage(error: SmileIDError) -> (String, String?) { switch error { - case .api(let code, let message): - let errorMessage = "Si.Error.Message.\(code)" - return (errorMessage, message) - case let .request(error): - return (error.localizedDescription, nil) - case .httpError(_, let message): - return ("", message) - default: - return ("Confirmation.FailureReason", nil) + case let .api(code, message): + let errorMessage = "Si.Error.Message.\(code)" + return (errorMessage, message) + case let .request(error): + return (error.localizedDescription, nil) + case let .httpError(_, message): + return ("", message) + default: + return ("Confirmation.FailureReason", nil) } } @@ -83,11 +83,11 @@ func getRelativePath(from absoluteURL: URL?) -> URL? { guard let absoluteURL = absoluteURL else { return nil } - + let relativeComponents = absoluteURL.pathComponents .drop(while: { $0 != "SmileID" }) .dropFirst() - + if relativeComponents.isEmpty { return absoluteURL } else { @@ -97,11 +97,11 @@ func getRelativePath(from absoluteURL: URL?) -> URL? { struct MonotonicTime { private let startTime: UInt64 - + init() { startTime = mach_absolute_time() } - + func elapsedTime() -> TimeInterval { let endTime = mach_absolute_time() let elapsed = endTime - startTime