diff --git a/CHANGELOG.md b/CHANGELOG.md index 49cc9725f..54dfd2be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,16 @@ + ## 10.0.0-beta12 (unreleased) ### Added +- Consent Screen SwiftUI View ### Changed ### Fixed ### Removed +- Biometric KYC no longer bundles the Consent Screen +- Biometric KYC no longer bundles an ID Type selector or input ## 10.0.0-beta11 diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 7731390cb..a25a141bd 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,6 +1,6 @@ PODS: - netfox (1.21.0) - - SmileID (10.0.0-beta10): + - SmileID (10.0.0-beta11): - Zip (~> 2.1.0) - SwiftLint (0.52.4) - Zip (2.1.2) @@ -22,10 +22,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: netfox: 9d5cc727fe7576c4c7688a2504618a156b7d44b7 - SmileID: 5975f6130f357bfa08e9b1f731f9ce3e2d91c216 + SmileID: fafe73ce2afa5b50d9c3b32e870b40562ff1827b SwiftLint: 1cc5cd61ba9bacb2194e340aeb47a2a37fda00b3 Zip: b3fef584b147b6e582b2256a9815c897d60ddc67 PODFILE CHECKSUM: b024c66547f30afaee4b2f86d064c8edcba8cbef -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/Example/SmileID.xcodeproj/project.pbxproj b/Example/SmileID.xcodeproj/project.pbxproj index fe2f4d067..cc31e521a 100644 --- a/Example/SmileID.xcodeproj/project.pbxproj +++ b/Example/SmileID.xcodeproj/project.pbxproj @@ -23,14 +23,18 @@ 1ED53F732A2F28590020BEFB /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F692A2F28590020BEFB /* HomeView.swift */; }; 1EFAB3172A375265008E3C13 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 585BE4882AC7748E0091DDD8 /* RestartableTimerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585BE4872AC7748E0091DDD8 /* RestartableTimerTest.swift */; }; + 58C5F1D82B05925800A6080C /* BiometricKycWithIdInputScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C5F1D72B05925800A6080C /* BiometricKycWithIdInputScreen.swift */; }; 58C7118C2A69DE920062BBFB /* EnhancedKycTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7118B2A69DE920062BBFB /* EnhancedKycTest.swift */; }; 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 6AC9802B9D1A630961B5454B /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98436935FFEA40E632182 /* CodeScanner.swift */; }; 6AC983F056A8F9088D6CF3F7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC982147640002B81F72DEC /* SettingsView.swift */; }; 6AC984526F49F4E8F52C7494 /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98BA00298258573CBCBD4 /* ScannerViewController.swift */; }; + 6AC984F8CA1753050C98F14B /* IdInfoInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC987C6B5794E2B1241AA51 /* IdInfoInputViewModel.swift */; }; 6AC9870BB28E40FCACC75947 /* DocumentVerificationIdTypeSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC9868BCF06ECE5F65DF248 /* DocumentVerificationIdTypeSelector.swift */; }; 6AC98856053013D0E8ABB188 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC982F34F80CAE1AA5569AB /* OnboardingScreen.swift */; }; + 6AC9886EEE4DE8AE1A31896A /* IdInfoInputScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC980D3CB9C357AD1B13D80 /* IdInfoInputScreen.swift */; }; + 6AC98976E04EBF5C45A2E857 /* BiometricKycWithIdInputScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC988EEBD0EAB45DC9AC13B /* BiometricKycWithIdInputScreenViewModel.swift */; }; 6AC98990097662789B0107EB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98BC49871655D87C7DEE3 /* SettingsViewModel.swift */; }; 6AC98B6FFA753C5463F7216F /* SmileConfigEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC984E484EEF69069C705C7 /* SmileConfigEntryView.swift */; }; 6AC98C0E9305B4B3EB66ED35 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC980584C522B17A099E098 /* Util.swift */; }; @@ -78,6 +82,7 @@ 262BF9A8643DF9220FD233E3 /* Pods-SmileID_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SmileID_Example.release.xcconfig"; path = "Target Support Files/Pods-SmileID_Example/Pods-SmileID_Example.release.xcconfig"; sourceTree = ""; }; 287986BB9E93D632523CC13A /* Pods_SmileID_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SmileID_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 585BE4872AC7748E0091DDD8 /* RestartableTimerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartableTimerTest.swift; sourceTree = ""; }; + 58C5F1D72B05925800A6080C /* BiometricKycWithIdInputScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricKycWithIdInputScreen.swift; sourceTree = ""; }; 58C7118B2A69DE920062BBFB /* EnhancedKycTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedKycTest.swift; sourceTree = ""; }; 607FACD01AFB9204008FA782 /* Smile ID.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Smile ID.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -89,11 +94,14 @@ 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67420F8D15457A4FC46AFB84 /* Pods-SmileID_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SmileID_Example.debug.xcconfig"; path = "Target Support Files/Pods-SmileID_Example/Pods-SmileID_Example.debug.xcconfig"; sourceTree = ""; }; 6AC980584C522B17A099E098 /* Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; + 6AC980D3CB9C357AD1B13D80 /* IdInfoInputScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdInfoInputScreen.swift; sourceTree = ""; }; 6AC982147640002B81F72DEC /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 6AC982F34F80CAE1AA5569AB /* OnboardingScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = ""; }; 6AC98436935FFEA40E632182 /* CodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = ""; }; 6AC984E484EEF69069C705C7 /* SmileConfigEntryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmileConfigEntryView.swift; sourceTree = ""; }; 6AC9868BCF06ECE5F65DF248 /* DocumentVerificationIdTypeSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentVerificationIdTypeSelector.swift; sourceTree = ""; }; + 6AC987C6B5794E2B1241AA51 /* IdInfoInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdInfoInputViewModel.swift; sourceTree = ""; }; + 6AC988EEBD0EAB45DC9AC13B /* BiometricKycWithIdInputScreenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometricKycWithIdInputScreenViewModel.swift; sourceTree = ""; }; 6AC9893915EBA33F6984A6D9 /* DocumentSelectorViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentSelectorViewModel.swift; sourceTree = ""; }; 6AC98BA00298258573CBCBD4 /* ScannerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = ""; }; 6AC98BC49871655D87C7DEE3 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -136,6 +144,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 58C5F1D62B05922100A6080C /* BiometricKYC */ = { + isa = PBXGroup; + children = ( + 58C5F1D72B05925800A6080C /* BiometricKycWithIdInputScreen.swift */, + 6AC988EEBD0EAB45DC9AC13B /* BiometricKycWithIdInputScreenViewModel.swift */, + 6AC980D3CB9C357AD1B13D80 /* IdInfoInputScreen.swift */, + 6AC987C6B5794E2B1241AA51 /* IdInfoInputViewModel.swift */, + ); + path = BiometricKYC; + sourceTree = ""; + }; 607FACC71AFB9204008FA782 = { isa = PBXGroup; children = ( @@ -160,7 +179,8 @@ 607FACD21AFB9204008FA782 /* Example */ = { isa = PBXGroup; children = ( - 91D9FBC52AB49C3400A8D36B /* CountrySelector */, + 58C5F1D62B05922100A6080C /* BiometricKYC */, + 91CB21A42AC10C61005AEBF5 /* NavigationBar.swift */, 1ECAE3862A2F69BC00653FCA /* ToastView.swift */, 1ED53F672A2F28590020BEFB /* EnterUserIDView.swift */, 1ED53F692A2F28590020BEFB /* HomeView.swift */, @@ -312,14 +332,6 @@ path = ../../Tests/SmartSelfie; sourceTree = ""; }; - 91D9FBC52AB49C3400A8D36B /* CountrySelector */ = { - isa = PBXGroup; - children = ( - 91CB21A42AC10C61005AEBF5 /* NavigationBar.swift */, - ); - name = CountrySelector; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -532,6 +544,7 @@ 1ED53F6B2A2F28590020BEFB /* ProductCell.swift in Sources */, 1E60ED382A29C306002695FF /* Constants.swift in Sources */, 1ED53F712A2F28590020BEFB /* EnterUserIDView.swift in Sources */, + 58C5F1D82B05925800A6080C /* BiometricKycWithIdInputScreen.swift in Sources */, 1ED53F732A2F28590020BEFB /* HomeView.swift in Sources */, 1E60ED3B2A29C306002695FF /* CopyableLabel.swift in Sources */, 6AC98C0E9305B4B3EB66ED35 /* Util.swift in Sources */, @@ -543,6 +556,9 @@ 6AC98856053013D0E8ABB188 /* OnboardingScreen.swift in Sources */, 6AC9802B9D1A630961B5454B /* CodeScanner.swift in Sources */, 6AC984526F49F4E8F52C7494 /* ScannerViewController.swift in Sources */, + 6AC98976E04EBF5C45A2E857 /* BiometricKycWithIdInputScreenViewModel.swift in Sources */, + 6AC9886EEE4DE8AE1A31896A /* IdInfoInputScreen.swift in Sources */, + 6AC984F8CA1753050C98F14B /* IdInfoInputViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/SmileID/BiometricKYC/BiometricKycWithIdInputScreen.swift b/Example/SmileID/BiometricKYC/BiometricKycWithIdInputScreen.swift new file mode 100644 index 000000000..34a7fe2d9 --- /dev/null +++ b/Example/SmileID/BiometricKYC/BiometricKycWithIdInputScreen.swift @@ -0,0 +1,73 @@ +import Foundation +import SmileID +import SwiftUI + +struct BiometricKycWithIdInputScreen: View { + let delegate: BiometricKycResultDelegate + + @State private var selectedCountry: CountryInfo? + @ObservedObject private var viewModel = BiometricKycWithIdInputScreenViewModel() + + var body: some View { + switch viewModel.step { + case .loading(let messageKey): + VStack { + ActivityIndicator(isAnimating: true).padding() + Text(SmileIDResourcesHelper.localizedString(for: messageKey)) + .font(SmileID.theme.body) + .foregroundColor(SmileID.theme.onLight) + } + .frame(maxWidth: .infinity) + case .idTypeSelection(let countryList): + SearchableDropdownSelector( + items: countryList, + selectedItem: selectedCountry, + itemDisplayName: { $0.name }, + onItemSelected: { selectedCountry = $0 } + ) + if let selectedCountry = selectedCountry { + RadioGroupSelector( + title: "Select ID Type", + items: selectedCountry.availableIdTypes, + itemDisplayName: { $0.label }, + onItemSelected: { idType in + viewModel.onIdTypeSelected( + country: selectedCountry.countryCode, + idType: idType.idTypeKey, + requiredFields: idType.requiredFields ?? [] + ) + } + ) + } + case .consent(let country, let idType, let requiredFields): + SmileID.consentScreen( + partnerIcon: UIImage(named: "SmileLogo")!, + partnerName: "Smile ID", + productName: "ID", + partnerPrivacyPolicy: URL(string: "https://usesmileid.com")!, + showAttribution: true, + onConsentGranted: { + viewModel.onConsentGranted( + country: country, + idType: idType, + requiredFields: requiredFields) + }, + onConsentDenied: { delegate.didError(error: SmileIDError.consentDenied) } + ) + case .idInput(let country, let idType, let requiredFields): + IdInfoInputScreen( + selectedCountry: country, + selectedIdType: idType, + header: "Enter ID Information", + requiredFields: requiredFields, + onResult: viewModel.onIdFieldsEntered + ).frame(maxWidth: .infinity) + case .sdk(let idInfo): + SmileID.biometricKycScreen( + idInfo: idInfo, + allowAgentMode: true, + delegate: delegate + ) + } + } +} diff --git a/Example/SmileID/BiometricKYC/BiometricKycWithIdInputScreenViewModel.swift b/Example/SmileID/BiometricKYC/BiometricKycWithIdInputScreenViewModel.swift new file mode 100644 index 000000000..cf97a5a6e --- /dev/null +++ b/Example/SmileID/BiometricKYC/BiometricKycWithIdInputScreenViewModel.swift @@ -0,0 +1,120 @@ +import Foundation +import SmileID + +enum BiometricKycWithIdInputScreenStep { + case loading(String) + case idTypeSelection([CountryInfo]) + case consent(country: String, idType: String, requiredFields: [RequiredField]) + case idInput(country: String, idType: String, requiredFields: [RequiredField]) + case sdk(IdInfo) +} + +class BiometricKycWithIdInputScreenViewModel: ObservableObject { + private let userId = generateUserId() + private let jobId = generateJobId() + + @Published @MainActor var step = BiometricKycWithIdInputScreenStep.loading("Loading ID Types…") + + init() { + loadIdTypes() + } + + private func loadIdTypes() { + let authRequest = AuthenticationRequest( + jobType: .biometricKyc, + enrollment: false, + jobId: jobId, + userId: userId + ) + DispatchQueue.main.async { + self.step = .loading("Loading ID Types…") + } + Task { + do { + let authResponse = try await SmileID.api.authenticate(request: authRequest).async() + let productsConfigRequest = ProductsConfigRequest( + timestamp: authResponse.timestamp, + signature: authResponse.signature + ) + let productsConfigResponse = try await SmileID.api.getProductsConfig( + request: productsConfigRequest + ).async() + let supportedCountries = productsConfigResponse.idSelection.biometricKyc + let servicesResponse = try await SmileID.api.getServices().async() + let servicesCountryInfo = servicesResponse.hostedWeb.biometricKyc + // sort by country name + let countryList = servicesCountryInfo + .filter { supportedCountries.keys.contains($0.countryCode) } + .sorted { $0.name < $1.name } + DispatchQueue.main.async { self.step = .idTypeSelection(countryList) } + } catch { + print("Error loading id types: \(error)") + DispatchQueue.main.async { + self.step = .loading("Error loading ID Types. Please try again.") + } + } + } + } + + private func loadConsent( + country: String, + idType: String, + requiredFields: [RequiredField] + ) { + let authRequest = AuthenticationRequest( + jobType: .biometricKyc, + enrollment: false, + jobId: jobId, + userId: userId, + country: country, + idType: idType + ) + DispatchQueue.main.async { + self.step = .loading("Loading Consent…") + } + Task { + do { + let authResponse = try await SmileID.api.authenticate(request: authRequest).async() + if authResponse.consentInfo?.consentRequired == true { + DispatchQueue.main.async { + self.step = .consent( + country: country, + idType: idType, + requiredFields: requiredFields + ) + } + } else { + // We don't need consent. Proceed forward as if consent has already been granted + onConsentGranted( + country: country, + idType: idType, + requiredFields: requiredFields + ) + } + } catch { + print("Error loading consent: \(error)") + DispatchQueue.main.async { + self.step = .loading("Error loading consent. Please try again.") + } + } + } + } + + func onIdTypeSelected(country: String, idType: String, requiredFields: [RequiredField]) { + loadConsent(country: country, idType: idType, requiredFields: requiredFields) + } + + func onConsentGranted(country: String, idType: String, requiredFields: [RequiredField]) { + DispatchQueue.main.async { + self.step = .idInput( + country: country, + idType: idType, + requiredFields: requiredFields + ) + } + } + + func onIdFieldsEntered(idInfo: IdInfo) { + DispatchQueue.main.async { self.step = .sdk(idInfo) } + } +} diff --git a/Sources/SmileID/Classes/BiometricKYC/IdInfoInputScreen.swift b/Example/SmileID/BiometricKYC/IdInfoInputScreen.swift similarity index 99% rename from Sources/SmileID/Classes/BiometricKYC/IdInfoInputScreen.swift rename to Example/SmileID/BiometricKYC/IdInfoInputScreen.swift index d92e5febc..983e0a7de 100644 --- a/Sources/SmileID/Classes/BiometricKYC/IdInfoInputScreen.swift +++ b/Example/SmileID/BiometricKYC/IdInfoInputScreen.swift @@ -1,3 +1,4 @@ +import SmileID import SwiftUI /// Allows user to enter ID info. Requires that the user has already selected a country and ID type. diff --git a/Sources/SmileID/Classes/BiometricKYC/IdInfoInputViewModel.swift b/Example/SmileID/BiometricKYC/IdInfoInputViewModel.swift similarity index 99% rename from Sources/SmileID/Classes/BiometricKYC/IdInfoInputViewModel.swift rename to Example/SmileID/BiometricKYC/IdInfoInputViewModel.swift index d756518d4..1fae823a0 100644 --- a/Sources/SmileID/Classes/BiometricKYC/IdInfoInputViewModel.swift +++ b/Example/SmileID/BiometricKYC/IdInfoInputViewModel.swift @@ -1,4 +1,5 @@ import Combine +import SmileID import SwiftUI import UIKit diff --git a/Example/SmileID/HomeView.swift b/Example/SmileID/HomeView.swift index 2ac4154e6..4002384d4 100644 --- a/Example/SmileID/HomeView.swift +++ b/Example/SmileID/HomeView.swift @@ -63,13 +63,7 @@ struct HomeView: View { image: "biometric", name: "Biometric KYC", content: { - SmileID.biometricKycScreen( - partnerIcon: UIImage(named: "SmileLogo")!, - partnerName: "Smile ID", - productName: "ID", - partnerPrivacyPolicy: URL(string: "https://usesmileid.com")!, - delegate: viewModel - ) + BiometricKycWithIdInputScreen(delegate: viewModel) } ) ].map { AnyView($0) } diff --git a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycScreen.swift b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycScreen.swift index 0d1d2ee8b..e280a2c0d 100644 --- a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycScreen.swift +++ b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycScreen.swift @@ -4,10 +4,6 @@ import SwiftUI struct OrchestratedBiometricKycScreen: View { let userId: String let jobId: String - let partnerIcon: UIImage - let partnerName: String - let productName: String - let partnerPrivacyPolicy: URL let showInstructions: Bool let showAttribution: Bool let allowAgentMode: Bool @@ -15,16 +11,10 @@ struct OrchestratedBiometricKycScreen: View { let delegate: BiometricKycResultDelegate @ObservedObject private var viewModel: OrchestratedBiometricKycViewModel - @State private var selectedCountry: CountryInfo? - init( - idInfo: IdInfo?, + idInfo: IdInfo, userId: String, jobId: String, - partnerIcon: UIImage, - partnerName: String, - productName: String, - partnerPrivacyPolicy: URL, showInstructions: Bool, showAttribution: Bool, allowAgentMode: Bool, @@ -33,10 +23,6 @@ struct OrchestratedBiometricKycScreen: View { ) { self.userId = userId self.jobId = jobId - self.partnerIcon = partnerIcon - self.partnerName = partnerName - self.productName = productName - self.partnerPrivacyPolicy = partnerPrivacyPolicy self.showInstructions = showInstructions self.showAttribution = showAttribution self.allowAgentMode = allowAgentMode @@ -48,60 +34,6 @@ struct OrchestratedBiometricKycScreen: View { var body: some View { switch viewModel.step { - case .loading(let messageKey): - VStack { - ActivityIndicator(isAnimating: true).padding() - Text(SmileIDResourcesHelper.localizedString(for: messageKey)) - .font(SmileID.theme.body) - .foregroundColor(SmileID.theme.onLight) - } - .frame(maxWidth: .infinity) - case .idTypeSelection(let countryList): - SearchableDropdownSelector( - items: countryList, - selectedItem: selectedCountry, - itemDisplayName: { $0.name }, - onItemSelected: { selectedCountry = $0 } - ) - if let selectedCountry = selectedCountry { - RadioGroupSelector( - title: SmileIDResourcesHelper.localizedString(for: "BiometricKYC.SelectIdType"), - items: selectedCountry.availableIdTypes, - itemDisplayName: { $0.label }, - onItemSelected: { idType in - viewModel.onIdTypeSelected( - country: selectedCountry.countryCode, - idType: idType.idTypeKey, - requiredFields: idType.requiredFields ?? [] - ) - } - ) - } - case .consent(let country, let idType, let requiredFields): - OrchestratedConsentScreen( - partnerIcon: partnerIcon, - partnerName: partnerName, - productName: productName, - partnerPrivacyPolicy: partnerPrivacyPolicy, - showAttribution: showAttribution, - onConsentGranted: { - viewModel.onConsentGranted( - country: country, - idType: idType, - requiredFields: requiredFields) - }, - onConsentDenied: { delegate.didError(error: SmileIDError.consentDenied) } - ) - case .idInput(let country, let idType, let requiredFields): - IdInfoInputScreen( - selectedCountry: country, - selectedIdType: idType, - header: SmileIDResourcesHelper.localizedString( - for: "BiometricKYC.EnterIdInfoTitle" - ), - requiredFields: requiredFields, - onResult: viewModel.onIdFieldsEntered - ).frame(maxWidth: .infinity) case .selfie: SelfieCaptureView( viewModel: SelfieCaptureViewModel( diff --git a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift index d13665286..9c5df37c7 100644 --- a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift +++ b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift @@ -2,10 +2,6 @@ import Combine import Foundation internal enum BiometricKycStep { - case loading(messageKey: String) - case idTypeSelection([CountryInfo]) - case consent(country: String, idType: String, requiredFields: [RequiredField]) - case idInput(country: String, idType: String, requiredFields: [RequiredField]) case selfie case processing(ProcessingState) } @@ -15,7 +11,7 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject, SelfieImageC private let userId: String private let jobId: String private var extraPartnerParams: [String: String] - private var idInfo: IdInfo? + private var idInfo: IdInfo // MARK: - Other Properties private var error: Error? @@ -23,128 +19,13 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject, SelfieImageC private var jobStatusResponse: BiometricKycJobStatusResponse? // MARK: - UI Properties - @Published @MainActor private (set) var step: BiometricKycStep = .loading( - messageKey: "BiometricKYC.Loading.IdTypes" - ) + @Published @MainActor private (set) var step: BiometricKycStep = .selfie - init(userId: String, jobId: String, idInfo: IdInfo?, extraPartnerParams: [String: String] = [:]) { + init(userId: String, jobId: String, idInfo: IdInfo, extraPartnerParams: [String: String] = [:]) { self.userId = userId self.jobId = jobId self.idInfo = idInfo self.extraPartnerParams = extraPartnerParams - if let idInfo = idInfo { - guard let idType = idInfo.idType else { - fatalError("You are expected to pass in the idType if you pass in idInfo") - } - // On this code path, we don't need to load services, ever, at all - loadConsent(country: idInfo.country, idType: idType, requiredFields: []) - } else { - loadIdTypes() - } - } - - private func loadIdTypes() { - let authRequest = AuthenticationRequest( - jobType: .biometricKyc, - enrollment: false, - jobId: jobId, - userId: userId - ) - DispatchQueue.main.async { - self.step = .loading(messageKey: "BiometricKYC.Loading.IdTypes") - } - Task { - do { - let authResponse = try await SmileID.api.authenticate(request: authRequest).async() - let productsConfigRequest = ProductsConfigRequest( - timestamp: authResponse.timestamp, - signature: authResponse.signature - ) - let productsConfigResponse = try await SmileID.api.getProductsConfig( - request: productsConfigRequest - ).async() - let supportedCountries = productsConfigResponse.idSelection.biometricKyc - let servicesResponse = try await SmileID.api.getServices().async() - let servicesCountryInfo = servicesResponse.hostedWeb.biometricKyc - // sort by country name - let countryList = servicesCountryInfo - .filter { supportedCountries.keys.contains($0.countryCode) } - .sorted { $0.name < $1.name } - DispatchQueue.main.async { self.step = .idTypeSelection(countryList) } - } catch { - print("Error loading id types: \(error)") - self.error = error - DispatchQueue.main.async { self.step = .processing(.error) } - } - } - } - - private func loadConsent( - country: String, - idType: String, - requiredFields: [RequiredField] - ) { - let authRequest = AuthenticationRequest( - jobType: .biometricKyc, - enrollment: false, - jobId: jobId, - userId: userId, - country: country, - idType: idType - ) - DispatchQueue.main.async { - self.step = .loading(messageKey: "BiometricKYC.Loading.Consent") - } - Task { - do { - let authResponse = try await SmileID.api.authenticate(request: authRequest).async() - if authResponse.consentInfo?.consentRequired == true { - DispatchQueue.main.async { - self.step = .consent( - country: country, - idType: idType, - requiredFields: requiredFields - ) - } - } else { - // We don't need consent. Proceed forward as if consent has already been granted - onConsentGranted( - country: country, - idType: idType, - requiredFields: requiredFields - ) - } - } catch { - print("Error loading consent: \(error)") - self.error = error - DispatchQueue.main.async { self.step = .processing(.error) } - } - } - } - - func onIdTypeSelected(country: String, idType: String, requiredFields: [RequiredField]) { - loadConsent(country: country, idType: idType, requiredFields: requiredFields) - } - - func onConsentGranted(country: String, idType: String, requiredFields: [RequiredField]) { - // If idInfo is already set, it was passed in, so we skip straight to selfie capture -- the - // partner is required to pass in all required inputs - if idInfo != nil { - DispatchQueue.main.async { self.step = .selfie } - } else { - DispatchQueue.main.async { - self.step = .idInput( - country: country, - idType: idType, - requiredFields: requiredFields - ) - } - } - } - - func onIdFieldsEntered(idInfo: IdInfo) { - self.idInfo = idInfo - DispatchQueue.main.async { self.step = .selfie } } func didCapture(selfie: Data, livenessImages: [Data]) { @@ -161,9 +42,7 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject, SelfieImageC } func onRetry() { - if idInfo == nil { - loadIdTypes() - } else if selfieCaptureResultStore == nil { + if selfieCaptureResultStore == nil { DispatchQueue.main.async { self.step = .selfie } } else { submitJob(selfieCaptureResultStore: selfieCaptureResultStore!) @@ -187,12 +66,6 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject, SelfieImageC func submitJob(selfieCaptureResultStore: SelfieCaptureResultStore) { DispatchQueue.main.async { self.step = .processing(.inProgress) } - guard let idInfo = idInfo else { - print("idInfo is nil") - error = SmileIDError.unknown("idInfo is nil") - DispatchQueue.main.async { self.step = .processing(.error) } - return - } Task { do { let livenessImages = selfieCaptureResultStore.livenessImages @@ -210,7 +83,9 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject, SelfieImageC jobType: .biometricKyc, enrollment: false, jobId: jobId, - userId: userId + userId: userId, + country: idInfo.country, + idType: idInfo.idType ) let authResponse = try await SmileID.api.authenticate(request: authRequest).async() let prepUploadRequest = PrepUploadRequest( diff --git a/Sources/SmileID/Classes/Networking/Models/Services.swift b/Sources/SmileID/Classes/Networking/Models/Services.swift index df221154f..48c01f8b6 100644 --- a/Sources/SmileID/Classes/Networking/Models/Services.swift +++ b/Sources/SmileID/Classes/Networking/Models/Services.swift @@ -150,7 +150,7 @@ public enum RequiredField: String, Codable { .jobId ] - static func sorter(this: RequiredField, that: RequiredField) -> Bool { + public static func sorter(this: RequiredField, that: RequiredField) -> Bool { let thisIndex = sortedCases.firstIndex(of: this) ?? 0 let thatIndex = sortedCases.firstIndex(of: that) ?? 0 return thisIndex < thatIndex diff --git a/Sources/SmileID/Classes/Networking/NetworkUtil.swift b/Sources/SmileID/Classes/Networking/NetworkUtil.swift index 01a55e13d..f856e3305 100644 --- a/Sources/SmileID/Classes/Networking/NetworkUtil.swift +++ b/Sources/SmileID/Classes/Networking/NetworkUtil.swift @@ -58,7 +58,7 @@ enum CryptoAlgorithm { } } -internal extension AnyPublisher { +public extension AnyPublisher { func async() async throws -> Output { try await withCheckedThrowingContinuation { continuation in var cancellable: AnyCancellable? diff --git a/Sources/SmileID/Classes/SmileID.swift b/Sources/SmileID/Classes/SmileID.swift index 923a6f368..1a680e96a 100644 --- a/Sources/SmileID/Classes/SmileID.swift +++ b/Sources/SmileID/Classes/SmileID.swift @@ -198,6 +198,9 @@ public class SmileID { /// - captureBothSides: Whether to capture both sides of the ID or not. Otherwise, only the /// front side will be captured. If this is true, an option to skip back side will still be /// shown + /// - allowAgentMode: Whether to allow Agent Mode or not. If allowed, a switch will be + /// displayed allowing toggling between the back camera and front camera. If not allowed, only + /// the front camera will be used. /// - allowGalleryUpload: Whether to allow the user to upload images from their gallery or not /// - showInstructions: Whether to deactivate capture screen's instructions for Document /// Verification (NB! If instructions are disabled, gallery upload won't be possible) @@ -294,17 +297,31 @@ public class SmileID { ).environmentObject(router) } + public class func consentScreen( + partnerIcon: UIImage, + partnerName: String, + productName: String, + partnerPrivacyPolicy: URL, + showAttribution: Bool = true, + onConsentGranted: @escaping () -> Void, + onConsentDenied: @escaping () -> Void + ) -> some View { + OrchestratedConsentScreen( + partnerIcon: partnerIcon, + partnerName: partnerName, + productName: productName, + partnerPrivacyPolicy: partnerPrivacyPolicy, + showAttribution: showAttribution, + onConsentGranted: onConsentGranted, + 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 /// - Parameters: - /// - partnerIcon: Your own icon to display on the Biometric KYC screen (i.e. company logo) - /// - partnerName: Your own name to display on the Biometric KYC screen (i.e. company name) - /// - productName: The type of information you are trying to access (i.e. ID type) - /// - partnerPrivacyPolicy: A link to your own privacy policy to display - /// - idInfo: The ID information to look up in the ID Authority. If nil (default), an ID type - /// selector and input fields will be displayed. If provided, it is assumed that ALL required - /// information has already been provided + /// - idInfo: The ID information to look up in the ID Authority /// - userId: The user ID to associate with the Biometric KYC. Most often, this will correspond /// to a unique User ID within your own system. If not provided, a random user ID is generated /// - jobId: The job ID to associate with the Biometric KYC. Most often, this will correspond @@ -317,11 +334,7 @@ public class SmileID { /// - extraPartnerParams: Custom values specific to partners /// - delegate: Callback to be invoked when the Biometric KYC is complete. public class func biometricKycScreen( - partnerIcon: UIImage, - partnerName: String, - productName: String, - partnerPrivacyPolicy: URL, - idInfo: IdInfo? = nil, + idInfo: IdInfo, userId: String = generateUserId(), jobId: String = generateJobId(), allowAgentMode: Bool = false, @@ -334,10 +347,6 @@ public class SmileID { idInfo: idInfo, userId: userId, jobId: jobId, - partnerIcon: partnerIcon, - partnerName: partnerName, - productName: productName, - partnerPrivacyPolicy: partnerPrivacyPolicy, showInstructions: showInstructions, showAttribution: showAttribution, allowAgentMode: allowAgentMode, diff --git a/Sources/SmileID/Resources/Localization/en.lproj/Localizable.strings b/Sources/SmileID/Resources/Localization/en.lproj/Localizable.strings index a49a3c41b..77769124f 100644 --- a/Sources/SmileID/Resources/Localization/en.lproj/Localizable.strings +++ b/Sources/SmileID/Resources/Localization/en.lproj/Localizable.strings @@ -85,10 +85,6 @@ "IdInfo.BankCode" = "Bank Code"; "IdInfo.Citizenship" = "Citizenship"; -"BiometricKYC.Loading.IdTypes" = "Loading ID Types…"; -"BiometricKYC.SelectIdType" = "Select ID Type"; -"BiometricKYC.Loading.Consent" = "Loading…"; -"BiometricKYC.EnterIdInfoTitle" = "Enter ID Information"; "BiometricKYC.Processing.Title" = "Processing your Selfie and ID"; "BiometricKYC.Processing.Subtitle" = "Just a few more seconds"; "BiometricKYC.Success.Title" = "Submission Complete";