diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index ac6e16f664..61d70eaaed 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -88,6 +88,13 @@ 00F621C527F1AF5A00C04097 /* AtomeComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F621C427F1AF5A00C04097 /* AtomeComponentTests.swift */; }; 00FFD8F02A865BFB008D4A5A /* ApplePaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00FFD8EF2A865BFB008D4A5A /* ApplePaySettingsView.swift */; }; 00FFD8F12A865BFB008D4A5A /* ApplePaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00FFD8EF2A865BFB008D4A5A /* ApplePaySettingsView.swift */; }; + 210CC97F2A5FC23400F8F672 /* UITextViewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210CC97E2A5FC23400F8F672 /* UITextViewHelpers.swift */; }; + 2159173A2A0D161B0004081E /* ThreeDS2PlusDAScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215917392A0D161B0004081E /* ThreeDS2PlusDAScreenPresenter.swift */; }; + 2159173C2A0D2B6E0004081E /* ThreeDS2DAScreenPresenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2159173B2A0D2B6E0004081E /* ThreeDS2DAScreenPresenterMock.swift */; }; + 21B3A71329CA70FF00F48386 /* DelegatedAuthenticationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B3A71229CA70FF00F48386 /* DelegatedAuthenticationView.swift */; }; + 21B3A71529CA720C00F48386 /* DARegistrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B3A71429CA720C00F48386 /* DARegistrationViewController.swift */; }; + 21B3A71729CA721F00F48386 /* DAApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B3A71629CA721F00F48386 /* DAApprovalViewController.swift */; }; + 21B3A71929CA780200F48386 /* DelegatedAuthenticationComponentStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B3A71829CA780200F48386 /* DelegatedAuthenticationComponentStyle.swift */; }; 5A1315C926296B100092366D /* ProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1315C826296B100092366D /* ProgressViewStyle.swift */; }; 5A15D589264BB0BD00A8E3C7 /* BoletoComponentExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A15D588264BB0BD00A8E3C7 /* BoletoComponentExtensions.swift */; }; 5A15D5A1264BE1E500A8E3C7 /* PrefilledShopperInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A15D5A0264BE1E500A8E3C7 /* PrefilledShopperInformation.swift */; }; @@ -1367,6 +1374,13 @@ 00F621C127EB1E3100C04097 /* AtomePaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomePaymentMethod.swift; sourceTree = ""; }; 00F621C427F1AF5A00C04097 /* AtomeComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomeComponentTests.swift; sourceTree = ""; }; 00FFD8EF2A865BFB008D4A5A /* ApplePaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePaySettingsView.swift; sourceTree = ""; }; + 210CC97E2A5FC23400F8F672 /* UITextViewHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextViewHelpers.swift; sourceTree = ""; }; + 215917392A0D161B0004081E /* ThreeDS2PlusDAScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreeDS2PlusDAScreenPresenter.swift; sourceTree = ""; }; + 2159173B2A0D2B6E0004081E /* ThreeDS2DAScreenPresenterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreeDS2DAScreenPresenterMock.swift; sourceTree = ""; }; + 21B3A71229CA70FF00F48386 /* DelegatedAuthenticationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatedAuthenticationView.swift; sourceTree = ""; }; + 21B3A71429CA720C00F48386 /* DARegistrationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DARegistrationViewController.swift; sourceTree = ""; }; + 21B3A71629CA721F00F48386 /* DAApprovalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAApprovalViewController.swift; sourceTree = ""; }; + 21B3A71829CA780200F48386 /* DelegatedAuthenticationComponentStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatedAuthenticationComponentStyle.swift; sourceTree = ""; }; 5A1315C826296B100092366D /* ProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressViewStyle.swift; sourceTree = ""; }; 5A15D588264BB0BD00A8E3C7 /* BoletoComponentExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoletoComponentExtensions.swift; sourceTree = ""; }; 5A15D5A0264BE1E500A8E3C7 /* PrefilledShopperInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefilledShopperInformation.swift; sourceTree = ""; }; @@ -2605,6 +2619,23 @@ path = Atome; sourceTree = ""; }; + 210CC97D2A5FC11900F8F672 /* Recovered References */ = { + isa = PBXGroup; + children = ( + ); + name = "Recovered References"; + sourceTree = ""; + }; + 21B3A70F29CA709D00F48386 /* DelegatedAuthentication */ = { + isa = PBXGroup; + children = ( + 21B3A71429CA720C00F48386 /* DARegistrationViewController.swift */, + 21B3A71629CA721F00F48386 /* DAApprovalViewController.swift */, + 21B3A71229CA70FF00F48386 /* DelegatedAuthenticationView.swift */, + ); + path = DelegatedAuthentication; + sourceTree = ""; + }; 5A22C26C262D67C000F12D97 /* QRCode */ = { isa = PBXGroup; children = ( @@ -3466,6 +3497,7 @@ F94D65DA2B0364AF0095D61E /* AdyenDelegatedAuthentication */, E2C0E03422097917008616F6 /* Products */, E2D12C01221ECBB000EF682F /* Frameworks */, + 210CC97D2A5FC11900F8F672 /* Recovered References */, ); sourceTree = ""; }; @@ -3626,6 +3658,7 @@ 5AD40E74262F04440090E01C /* UIProgressViewHelpers.swift */, 5AD40EEE26303C830090E01C /* UIButtonHelpers.swift */, 5AD40EFA26303D490090E01C /* UILabelHelpers.swift */, + 210CC97E2A5FC23400F8F672 /* UITextViewHelpers.swift */, A0D48FB727109B0200C0B82C /* ArrayHelpers.swift */, E7E0E2372762005B001DF0C9 /* NSConstraintHelper.swift */, 0035016B2976B14A00632D8C /* UIImageViewHelpers.swift */, @@ -4433,6 +4466,7 @@ F9175FF0259499C900D653BE /* View Controllers */ = { isa = PBXGroup; children = ( + 21B3A70F29CA709D00F48386 /* DelegatedAuthentication */, A02AF3EB275A3C6F00E1636C /* Document */, 5A64570D2625CB02001824F0 /* QR Code */, F97C851425C1928500D7F85C /* Voucher */, @@ -4475,6 +4509,7 @@ 5A64573E2626D2E2001824F0 /* QRCodeComponentStyle.swift */, A03EE7302761297800470561 /* DocumentComponentStyle.swift */, 5A2D1950267368580082BCE9 /* ActionComponentStyle.swift */, + 21B3A71829CA780200F48386 /* DelegatedAuthenticationComponentStyle.swift */, ); path = "UI Style"; sourceTree = ""; @@ -4670,6 +4705,7 @@ isa = PBXGroup; children = ( F94F0DEA28AA3FB400C0923D /* ThreeDS2PlusDACoreActionHandler.swift */, + 215917392A0D161B0004081E /* ThreeDS2PlusDAScreenPresenter.swift */, ); path = "3DS2+Delegated Authentication"; sourceTree = ""; @@ -4714,6 +4750,7 @@ F94F0DFE28AD2BCE00C0923D /* AuthenticationServiceMock.swift */, F957AA692552D98E0099AD73 /* AnyThreeDS2FingerprintSubmitterMock.swift */, F9B9F624295485A2008C2E49 /* ThreeDSResultExtension.swift */, + 2159173B2A0D2B6E0004081E /* ThreeDS2DAScreenPresenterMock.swift */, ); path = "3DS2 Component"; sourceTree = ""; @@ -6635,6 +6672,7 @@ 81C4006B2A40A526007EC51C /* FormAddressPickerItem.swift in Sources */, 81129AE62A4EEF8600E63EBE /* SearchViewController+InterfaceState.swift in Sources */, E2C0E0C1220D7E5E008616F6 /* SubmitButton.swift in Sources */, + 210CC97F2A5FC23400F8F672 /* UITextViewHelpers.swift in Sources */, E216D3C9221AFB830013CBCF /* IBANValidator.swift in Sources */, E76EC6972417F36A009C6E2F /* FormTextInputItemView.swift in Sources */, E2C0E0B6220B08ED008616F6 /* FormView.swift in Sources */, @@ -7107,6 +7145,8 @@ F97C84CD25C1761900D7F85C /* VoucherAction.swift in Sources */, 5A4633AA265FBAEB005FE0D8 /* VoucherView.swift in Sources */, 5A64573F2626D2E2001824F0 /* QRCodeComponentStyle.swift in Sources */, + 21B3A71529CA720C00F48386 /* DARegistrationViewController.swift in Sources */, + 21B3A71729CA721F00F48386 /* DAApprovalViewController.swift in Sources */, E73C54E425EBD2DC00B57758 /* BrowserComponent.swift in Sources */, F9B8AF5927DA48C900DC0894 /* ActionHandlingComponent.swift in Sources */, A03EE735276133A500470561 /* ShareableComponent.swift in Sources */, @@ -7122,9 +7162,11 @@ F95A78B925C4302A0032CF7E /* ShareableVoucherView.swift in Sources */, F9175FBA2594996000D653BE /* Action.swift in Sources */, F95A78CD25C43C4F0032CF7E /* VoucherSeparatorView.swift in Sources */, + 2159173C2A0D2B6E0004081E /* ThreeDS2DAScreenPresenterMock.swift in Sources */, E745256925C1C9E1006A941F /* AdyenActionComponent.swift in Sources */, F917616F25A30B6A00D653BE /* ThreeDSActionHandlerResult.swift in Sources */, F9175FB92594996000D653BE /* AwaitAction.swift in Sources */, + 21B3A71929CA780200F48386 /* DelegatedAuthenticationComponentStyle.swift in Sources */, F97C850125C17CCD00D7F85C /* VoucherShareableViewProvider.swift in Sources */, F9175FB42594996000D653BE /* ThreeDS2FingerprintAction.swift in Sources */, 5A22C2AD262EF99C00F12D97 /* ExpirationTimer.swift in Sources */, @@ -7152,9 +7194,11 @@ F9237D3B28CB467D004F9929 /* ThreeDS2CompactActionHandler+Initializers.swift in Sources */, A0113D9B2763887800AD395C /* ActionNavigationBar.swift in Sources */, F91760632594A0E300D653BE /* ActionsBundleExtension.swift in Sources */, + 21B3A71329CA70FF00F48386 /* DelegatedAuthenticationView.swift in Sources */, F917614A25A30B5E00D653BE /* ThreeDS2FingerprintSubmitter.swift in Sources */, 5A988B7B2653F1750007F4C0 /* BoletoVoucherAction.swift in Sources */, F917613525A30B5700D653BE /* ThreeDS2Details.swift in Sources */, + 2159173A2A0D161B0004081E /* ThreeDS2PlusDAScreenPresenter.swift in Sources */, A03EE72F2760A2E300470561 /* DocumentActionViewModel.swift in Sources */, F94F0DEB28AA3FB400C0923D /* ThreeDS2PlusDACoreActionHandler.swift in Sources */, F9B018032608F648001F23FC /* EContextStoresVoucherAction.swift in Sources */, diff --git a/Adyen/Helpers/UITextViewHelpers.swift b/Adyen/Helpers/UITextViewHelpers.swift new file mode 100644 index 0000000000..b77ab861cf --- /dev/null +++ b/Adyen/Helpers/UITextViewHelpers.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) 2023 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +import AdyenNetworking +import UIKit + +@_spi(AdyenInternal) +extension UITextView { + + /// Initializes UITextView with given `TextStyle` + /// Sets `translatesAutoresizingMaskIntoConstraints` to `false` + /// - Parameter style: `TextStyle` to be applied + public convenience init(style: TextStyle) { + self.init() + translatesAutoresizingMaskIntoConstraints = false + adyen.apply(style) + } +} + +public extension AdyenScope where Base: UITextView { + + /// Applies given `TextStyle` to the UITextView + /// Sets `translatesAutoresizingMaskIntoConstraints` to `false` + /// - Parameter style: `TextStyle` to be applied + internal func apply(_ style: TextStyle) { + base.font = style.font + base.textColor = style.color + base.textAlignment = style.textAlignment + base.backgroundColor = style.backgroundColor + round(using: style.cornerRounding) + + base.adjustsFontForContentSizeCategory = true + } +} diff --git a/Adyen/Utilities/PublicKeyProvider/PublicKeyProvider.swift b/Adyen/Utilities/PublicKeyProvider/PublicKeyProvider.swift index 8ffdd61a80..9893815ec8 100644 --- a/Adyen/Utilities/PublicKeyProvider/PublicKeyProvider.swift +++ b/Adyen/Utilities/PublicKeyProvider/PublicKeyProvider.swift @@ -75,7 +75,19 @@ public final class PublicKeyProvider: AnyPublicKeyProvider { cachedPublicKey = response.cardPublicKey completion(.success(response.cardPublicKey)) case let .failure(error): + if error is DecodingError { + // Disclaimer: This error check is not 100% reliable. Need to improve the endpoint. + return completion(.failure(Error.invalidClientKey)) + } completion(.failure(error)) } } + + public enum Error: Swift.Error, LocalizedError { + case invalidClientKey + + public var errorDescription: String? { + return "Client key not found on the selected environment." + } + } } diff --git a/AdyenActions/AdyenActionComponent.swift b/AdyenActions/AdyenActionComponent.swift index 33c37f421e..84d52c0643 100644 --- a/AdyenActions/AdyenActionComponent.swift +++ b/AdyenActions/AdyenActionComponent.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2024 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -137,7 +137,7 @@ public final class AdyenActionComponent: ActionComponent, ActionHandlingComponen private func handle(_ action: ThreeDS2Action) { let component = createThreeDS2Component() currentActionComponent = component - + component.handle(action) } @@ -153,10 +153,11 @@ public final class AdyenActionComponent: ActionComponent, ActionHandlingComponen appearanceConfiguration: configuration.threeDS.appearanceConfiguration, requestorAppURL: configuration.threeDS.requestorAppURL, delegateAuthentication: configuration.threeDS.delegateAuthentication) - let component = ThreeDS2Component(context: context, configuration: threeDS2Configuration) + let component = ThreeDS2Component(context: context, + configuration: threeDS2Configuration) + component.presentationDelegate = presentationDelegate component._isDropIn = _isDropIn component.delegate = delegate - component.presentationDelegate = presentationDelegate return component } diff --git a/AdyenActions/Assets/AdyenActionsAssets.xcassets/biometric.imageset/Biometric.pdf b/AdyenActions/Assets/AdyenActionsAssets.xcassets/biometric.imageset/Biometric.pdf new file mode 100644 index 0000000000..ef6db02e36 Binary files /dev/null and b/AdyenActions/Assets/AdyenActionsAssets.xcassets/biometric.imageset/Biometric.pdf differ diff --git a/AdyenActions/Assets/AdyenActionsAssets.xcassets/biometric.imageset/Contents.json b/AdyenActions/Assets/AdyenActionsAssets.xcassets/biometric.imageset/Contents.json new file mode 100644 index 0000000000..d2b36654de --- /dev/null +++ b/AdyenActions/Assets/AdyenActionsAssets.xcassets/biometric.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Biometric.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/AdyenActions/Components/3DS2/Action handlers/3DS2 Without Delegated Authentication/ThreeDS2CoreActionHandler.swift b/AdyenActions/Components/3DS2/Action handlers/3DS2 Without Delegated Authentication/ThreeDS2CoreActionHandler.swift index 59d2525d65..b2cda1f202 100644 --- a/AdyenActions/Components/3DS2/Action handlers/3DS2 Without Delegated Authentication/ThreeDS2CoreActionHandler.swift +++ b/AdyenActions/Components/3DS2/Action handlers/3DS2 Without Delegated Authentication/ThreeDS2CoreActionHandler.swift @@ -101,7 +101,8 @@ internal class ThreeDS2CoreActionHandler: AnyThreeDS2CoreActionHandler { case let .success(transaction): let encodedFingerprint = try AdyenCoder.encodeBase64(ThreeDS2Component.Fingerprint( authenticationRequestParameters: transaction.authenticationParameters, - delegatedAuthenticationSDKOutput: nil + delegatedAuthenticationSDKOutput: nil, + deleteDelegatedAuthenticationCredential: nil )) self.transaction = transaction completionHandler(.success(encodedFingerprint)) diff --git a/AdyenActions/Components/3DS2/Action handlers/3DS2+Delegated Authentication/ThreeDS2PlusDACoreActionHandler.swift b/AdyenActions/Components/3DS2/Action handlers/3DS2+Delegated Authentication/ThreeDS2PlusDACoreActionHandler.swift index 91d08b1b67..9dae5dec8a 100644 --- a/AdyenActions/Components/3DS2/Action handlers/3DS2+Delegated Authentication/ThreeDS2PlusDACoreActionHandler.swift +++ b/AdyenActions/Components/3DS2/Action handlers/3DS2+Delegated Authentication/ThreeDS2PlusDACoreActionHandler.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2024 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -33,53 +33,85 @@ } } + private struct DelegatedAuthenticationPayload { + let delegatedAuthenticationOutput: String + let delete: Bool? + } + /// Handles the 3D Secure 2 fingerprint and challenge actions separately + Delegated Authentication. @available(iOS 14.0, *) internal class ThreeDS2PlusDACoreActionHandler: ThreeDS2CoreActionHandler { - + internal var delegatedAuthenticationState: DelegatedAuthenticationState = .init() - + internal struct DelegatedAuthenticationState { internal var isDeviceRegistrationFlow: Bool = false } - + private let delegatedAuthenticationService: AuthenticationServiceProtocol - + private let deviceSupportCheckerService: AdyenAuthentication.DeviceSupportCheckerProtocol + private let presenter: ThreeDS2PlusDAScreenPresenterProtocol + + /// Errors during the Delegated authentication flow + private enum ThreeDS2PlusDACoreActionError: Error { + /// When the backend doesn't support delegated authentication, so the threeDSToken doesn't contain the `sdkInput` parameter + case sdkInputNotAvailableForApproval + /// When the device is not registered for delegated authentication. + case deviceIsNotRegistered + /// When the `sdkInput` parameter is not available in the threeDStoken, this occurs during the registration flow. + case sdkInputNotAvailableForRegistration + /// When the user doesn't provide consent to use delegated authentication for the transaction during an approval flow. + case noConsentForApproval + } + /// Initializes the 3D Secure 2 action handler. /// /// - Parameter context: The context object for this component. /// - Parameter appearanceConfiguration: The appearance configuration. /// - Parameter delegatedAuthenticationConfiguration: The delegated authentication configuration. + /// - Parameter presentationDelegate: The presentation delegate internal convenience init( context: AdyenContext, appearanceConfiguration: ADYAppearanceConfiguration, - delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication + delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication, + presentationDelegate: PresentationDelegate? ) { self.init( context: context, + presenter: ThreeDS2PlusDAScreenPresenter(presentationDelegate: presentationDelegate, + style: .init(), + localizedParameters: delegatedAuthenticationConfiguration.localizationParameters), appearanceConfiguration: appearanceConfiguration, + style: delegatedAuthenticationConfiguration.delegatedAuthenticationComponentStyle, delegatedAuthenticationService: AuthenticationService( configuration: delegatedAuthenticationConfiguration.authenticationServiceConfiguration() ) ) } - + /// Initializes the 3D Secure 2 action handler. /// /// - Parameter context: The context object for this component. /// - Parameter service: The 3DS2 Service. /// - Parameter appearanceConfiguration: The appearance configuration. + /// - Parameter style: The delegate authentication component style. /// - Parameter delegatedAuthenticationService: The Delegated Authentication service. + /// - Parameter presentationDelegate: Presentation delegate internal init(context: AdyenContext, service: AnyADYService = ADYServiceAdapter(), + presenter: ThreeDS2PlusDAScreenPresenterProtocol, appearanceConfiguration: ADYAppearanceConfiguration = .init(), - delegatedAuthenticationService: AuthenticationServiceProtocol) { + style: DelegatedAuthenticationComponentStyle = .init(), + delegatedAuthenticationService: AuthenticationServiceProtocol, + deviceSupportCheckerService: AdyenAuthentication.DeviceSupportCheckerProtocol = DeviceSupportChecker()) { self.delegatedAuthenticationService = delegatedAuthenticationService + self.deviceSupportCheckerService = deviceSupportCheckerService + self.presenter = presenter super.init(context: context, service: service, appearanceConfiguration: appearanceConfiguration) } - + // MARK: - Fingerprint - + /// Handles the 3D Secure 2 fingerprint action. /// /// - Parameter fingerprintAction: The fingerprint action as received from the Checkout API. @@ -99,47 +131,37 @@ } } } - - private func addSDKOutputIfNeeded(toFingerprintResult fingerprintResult: String, _ fingerprintAction: ThreeDS2FingerprintAction, completionHandler: @escaping (Result) -> Void) { + + private func addSDKOutputIfNeeded(toFingerprintResult fingerprintResult: String, + _ fingerprintAction: ThreeDS2FingerprintAction, + completionHandler: @escaping (Result) -> Void) { do { let token = try AdyenCoder.decodeBase64(fingerprintAction.fingerprintToken) as ThreeDS2Component.FingerprintToken let fingerprintResult: ThreeDS2Component.Fingerprint = try AdyenCoder.decodeBase64(fingerprintResult) performDelegatedAuthentication(token) { [weak self] result in guard let self else { return } self.delegatedAuthenticationState.isDeviceRegistrationFlow = result.successResult == nil - guard let fingerprintResult = self.createFingerPrintResult(authenticationSDKOutput: result.successResult, - fingerprintResult: fingerprintResult, - completionHandler: completionHandler) else { return } + guard let fingerprintResult = self.createFingerPrintResult( + authenticationSDKOutput: result.successResult?.delegatedAuthenticationOutput, + fingerprintResult: fingerprintResult, + deleteDelegatedAuthenticationCredential: result.successResult?.delete, + completionHandler: completionHandler + ) else { return } completionHandler(.success(fingerprintResult)) } } catch { didFail(with: error, completionHandler: completionHandler) } - } - - internal func performDelegatedAuthentication(_ fingerprintToken: ThreeDS2Component.FingerprintToken, - completion: @escaping (Result) -> Void) { - guard let delegatedAuthenticationInput = fingerprintToken.delegatedAuthenticationSDKInput else { - completion(.failure(DelegateAuthenticationError.authenticationFailed(cause: nil))) - return - } - delegatedAuthenticationService.authenticate(withAuthenticationInput: delegatedAuthenticationInput) { result in - switch result { - case let .success(sdkOutput): - completion(.success(sdkOutput)) - case let .failure(error): - completion(.failure(DelegateAuthenticationError.authenticationFailed(cause: error))) - } - } - } - + private func createFingerPrintResult(authenticationSDKOutput: String?, fingerprintResult: ThreeDS2Component.Fingerprint, + deleteDelegatedAuthenticationCredential: Bool?, completionHandler: @escaping (Result) -> Void) -> String? { do { let fingerprintResult = fingerprintResult.withDelegatedAuthenticationSDKOutput( - delegatedAuthenticationSDKOutput: authenticationSDKOutput + delegatedAuthenticationSDKOutput: authenticationSDKOutput, + deleteDelegatedAuthenticationCredential: deleteDelegatedAuthenticationCredential ) let encodedFingerprintResult = try AdyenCoder.encodeBase64(fingerprintResult) return encodedFingerprintResult @@ -148,9 +170,123 @@ } return nil } - + + // MARK: - Delegated Authentication + + /// This method checks; + /// 1. if DA has been registered on the device + /// 2. shows an approval screen if it has been registered + /// else calls the completion with a failure. + private func performDelegatedAuthentication( + _ fingerprintToken: ThreeDS2Component.FingerprintToken, + completion: @escaping (Result + ) -> Void + ) { + guard let delegatedAuthenticationInput = fingerprintToken.delegatedAuthenticationSDKInput else { + completion(.failure(.authenticationFailed(cause: ThreeDS2PlusDACoreActionError.sdkInputNotAvailableForApproval))) + return + } + + isDeviceRegisteredForDelegatedAuthentication( + delegatedAuthenticationInput: delegatedAuthenticationInput, + registeredHandler: { [weak self] in + guard let self else { return } + self.showApprovalScreen(delegatedAuthenticationInput: delegatedAuthenticationInput, + completion: completion) + }, + notRegisteredHandler: { + completion(.failure(.authenticationFailed(cause: $0))) + } + ) + } + + // MARK: Delegated Authentication Approval + + private func showApprovalScreen( + delegatedAuthenticationInput: String, + completion: @escaping (Result + ) -> Void + ) { + presenter.showApprovalScreen( + component: self, + approveAuthenticationHandler: { [weak self] in + guard let self else { return } + self.executeDAAuthenticate(delegatedAuthenticationInput: delegatedAuthenticationInput, + authenticatedHandler: { + completion(.success(.init(delegatedAuthenticationOutput: $0, delete: nil))) + }, + failedAuthenticationHandler: { + completion(.failure(.authenticationFailed(cause: $0))) + }) + }, + fallbackHandler: { + completion(.failure(.authenticationFailed(cause: ThreeDS2PlusDACoreActionError.noConsentForApproval))) + }, + removeCredentialsHandler: { [weak self] in + guard let self else { return } + self.executeDAAuthenticate(delegatedAuthenticationInput: delegatedAuthenticationInput, + authenticatedHandler: { sdkOutput in + completion(.success(.init(delegatedAuthenticationOutput: sdkOutput, delete: true))) + }, + failedAuthenticationHandler: { error in + completion(.failure(.authenticationFailed(cause: error))) + }) + } + ) + } + + private func executeDAAuthenticate(delegatedAuthenticationInput: String, + authenticatedHandler: @escaping (String) -> Void, + failedAuthenticationHandler: @escaping (Error) -> Void) { + delegatedAuthenticationService.authenticate(withAuthenticationInput: delegatedAuthenticationInput) { result in + switch result { + case let .success(sdkOutput): + authenticatedHandler(sdkOutput) + case let .failure(error): + failedAuthenticationHandler(error) + } + } + } + + private func isDeviceRegisteredForDelegatedAuthentication(delegatedAuthenticationInput: String, + registeredHandler: @escaping () -> Void, + notRegisteredHandler: @escaping (Error) -> Void) { + delegatedAuthenticationService.isDeviceRegistered(withAuthenticationInput: delegatedAuthenticationInput) { result in + switch result { + case let .failure(error): + notRegisteredHandler(error) + case let .success(isRegistered): + if isRegistered { + registeredHandler() + } else { + notRegisteredHandler(ThreeDS2PlusDACoreActionError.deviceIsNotRegistered) + } + } + } + } + + // MARK: Delegated Authentication Registration + + internal var shouldShowRegistrationScreen: Bool { + delegatedAuthenticationState.isDeviceRegistrationFlow + && presenter.userInput.canShowRegistration + && deviceSupportCheckerService.isDeviceSupported + } + + internal func performDelegatedRegistration(_ sdkInput: String, + completion: @escaping (Result) -> Void) { + delegatedAuthenticationService.register(withRegistrationInput: sdkInput) { result in + switch result { + case let .success(sdkOutput): + completion(.success(sdkOutput)) + case let .failure(error): + completion(.failure(error)) + } + } + } + // MARK: - Challenge - + /// Handles the 3D Secure 2 challenge action. /// /// - Parameter challengeAction: The challenge action as received from the Checkout API. @@ -168,67 +304,69 @@ } } } - + private func addSDKOutputIfNeeded(toChallengeResult challengeResult: ThreeDSResult, _ challengeAction: ThreeDS2ChallengeAction, completionHandler: @escaping (Result) -> Void) { let token: ThreeDS2Component.ChallengeToken do { token = try AdyenCoder.decodeBase64(challengeAction.challengeToken) as ThreeDS2Component.ChallengeToken - } catch { - return didFail(with: error, completionHandler: completionHandler) - } - if delegatedAuthenticationState.isDeviceRegistrationFlow { - performDelegatedRegistration(token.delegatedAuthenticationSDKInput) { [weak self] result in - self?.deliver(challengeResult: challengeResult, - delegatedAuthenticationSDKOutput: result.successResult, - completionHandler: completionHandler) + guard let sdkInput = token.delegatedAuthenticationSDKInput else { + completionHandler(.success(challengeResult)) + return + } + if shouldShowRegistrationScreen { + showDelegatedAuthenticationRegistration(sdkInput: sdkInput, + challengeResult: challengeResult, + completionHandler: completionHandler) + } else { + completionHandler(.success(challengeResult)) } - } else { - completionHandler(.success(challengeResult)) + } catch { + return didFail(with: error, completionHandler: completionHandler) } } - internal func performDelegatedRegistration(_ sdkInput: String?, - completion: @escaping (Result) -> Void) { - guard let sdkInput else { - completion(.failure(DelegateAuthenticationError.registrationFailed(cause: nil))) - return - } - delegatedAuthenticationService.register(withRegistrationInput: sdkInput) { result in - switch result { - case let .success(sdkOutput): - completion(.success(sdkOutput)) - case let .failure(error): - completion(.failure(error)) - } - } + private func showDelegatedAuthenticationRegistration(sdkInput: String, + challengeResult: ThreeDSResult, + completionHandler: @escaping (Result) -> Void) { + presenter.showRegistrationScreen(component: self, + registerDelegatedAuthenticationHandler: { [weak self] in + guard let self else { return } + self.performDelegatedRegistration(sdkInput) { [weak self] result in + self?.deliver(challengeResult: challengeResult, + delegatedAuthenticationSDKOutput: result.successResult, + completionHandler: completionHandler) + } + }, + fallbackHandler: { + completionHandler(.success(challengeResult)) + }) } - + private func deliver(challengeResult: ThreeDSResult, delegatedAuthenticationSDKOutput: String?, completionHandler: @escaping (Result) -> Void) { - + do { let threeDSResult = try challengeResult.withDelegatedAuthenticationSDKOutput( delegatedAuthenticationSDKOutput: delegatedAuthenticationSDKOutput ) - transaction = nil completionHandler(.success(threeDSResult)) } catch { completionHandler(.failure(error)) } } - + private func didFail(with error: Error, completionHandler: @escaping (Result) -> Void) { transaction = nil - + completionHandler(.failure(error)) } - + } extension Result { diff --git a/AdyenActions/Components/3DS2/Action handlers/3DS2+Delegated Authentication/ThreeDS2PlusDAScreenPresenter.swift b/AdyenActions/Components/3DS2/Action handlers/3DS2+Delegated Authentication/ThreeDS2PlusDAScreenPresenter.swift new file mode 100644 index 0000000000..a90d8cd958 --- /dev/null +++ b/AdyenActions/Components/3DS2/Action handlers/3DS2+Delegated Authentication/ThreeDS2PlusDAScreenPresenter.swift @@ -0,0 +1,102 @@ +// +// Copyright (c) 2023 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +import Foundation +@_spi(AdyenInternal) import Adyen + +internal enum ThreeDS2PlusDAScreenUserInput { + case approveDifferently + case deleteDA + case noInput + case biometric + + internal var canShowRegistration: Bool { + switch self { + case .approveDifferently, .deleteDA, .biometric: + return false + case .noInput: + return true + } + } +} + +internal protocol ThreeDS2PlusDAScreenPresenterProtocol { + func showRegistrationScreen(component: Component, + registerDelegatedAuthenticationHandler: @escaping () -> Void, + fallbackHandler: @escaping () -> Void) + + func showApprovalScreen(component: Component, + approveAuthenticationHandler: @escaping () -> Void, + fallbackHandler: @escaping () -> Void, + removeCredentialsHandler: @escaping () -> Void) + + var userInput: ThreeDS2PlusDAScreenUserInput { get } +} + +/// This type handles the presenting of the Delegate authentication screens of Register and Approval. +internal final class ThreeDS2PlusDAScreenPresenter: ThreeDS2PlusDAScreenPresenterProtocol { + /// Delegates `PresentableComponent`'s presentation. + internal weak var presentationDelegate: PresentationDelegate? + private let style: DelegatedAuthenticationComponentStyle + private let localizedParameters: LocalizationParameters? + + internal var userInput: ThreeDS2PlusDAScreenUserInput = .noInput + + internal init(presentationDelegate: PresentationDelegate?, + style: DelegatedAuthenticationComponentStyle, + localizedParameters: LocalizationParameters?) { + self.presentationDelegate = presentationDelegate + self.style = style + self.localizedParameters = localizedParameters + } + + internal func showRegistrationScreen(component: Component, + registerDelegatedAuthenticationHandler: @escaping () -> Void, + fallbackHandler: @escaping () -> Void) { + AdyenAssertion.assert(message: "presentationDelegate should not be nil", condition: presentationDelegate == nil) + let registrationViewController = DARegistrationViewController(style: style, + localizationParameters: localizedParameters, + enableCheckoutHandler: { + registerDelegatedAuthenticationHandler() + }, notNowHandler: { + fallbackHandler() + }) + + let presentableComponent = PresentableComponentWrapper(component: component, + viewController: registrationViewController) + presentationDelegate?.present(component: presentableComponent) + registrationViewController.navigationItem.rightBarButtonItems = [] + registrationViewController.navigationItem.leftBarButtonItems = [] + } + + internal func showApprovalScreen(component: Component, + approveAuthenticationHandler: @escaping () -> Void, + fallbackHandler: @escaping () -> Void, + removeCredentialsHandler: @escaping () -> Void) { + AdyenAssertion.assert(message: "presentationDelegate should not be nil", condition: presentationDelegate == nil) + let approvalViewController = DAApprovalViewController(style: style, + localizationParameters: localizedParameters, + useBiometricsHandler: { [weak self] in + guard let self = self else { return } + self.userInput = .biometric + approveAuthenticationHandler() + }, approveDifferentlyHandler: { [weak self] in + guard let self = self else { return } + self.userInput = .approveDifferently + fallbackHandler() + }, removeCredentialsHandler: { [weak self] in + guard let self = self else { return } + self.userInput = .deleteDA + removeCredentialsHandler() + }) + + let presentableComponent = PresentableComponentWrapper(component: component, + viewController: approvalViewController) + presentationDelegate?.present(component: presentableComponent) + approvalViewController.navigationItem.rightBarButtonItems = [] + approvalViewController.navigationItem.leftBarButtonItems = [] + } +} diff --git a/AdyenActions/Components/3DS2/Action handlers/AnyThreeDS2ActionHandler.swift b/AdyenActions/Components/3DS2/Action handlers/AnyThreeDS2ActionHandler.swift index 68247dc5db..04f6727dc8 100644 --- a/AdyenActions/Components/3DS2/Action handlers/AnyThreeDS2ActionHandler.swift +++ b/AdyenActions/Components/3DS2/Action handlers/AnyThreeDS2ActionHandler.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -45,13 +45,15 @@ extension ComponentWrapper { internal func createDefaultThreeDS2CoreActionHandler( context: AdyenContext, appearanceConfiguration: ADYAppearanceConfiguration, - delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication? + delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication?, + presentationDelegate: PresentationDelegate? ) -> AnyThreeDS2CoreActionHandler { #if canImport(AdyenAuthentication) if #available(iOS 14.0, *), let delegatedAuthenticationConfiguration { return ThreeDS2PlusDACoreActionHandler(context: context, appearanceConfiguration: appearanceConfiguration, - delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration) + delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration, + presentationDelegate: presentationDelegate) } else { return ThreeDS2CoreActionHandler(context: context, appearanceConfiguration: appearanceConfiguration) diff --git a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler+Initializers.swift b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler+Initializers.swift index f5a9641b7c..37c084602a 100644 --- a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler+Initializers.swift +++ b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler+Initializers.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -16,11 +16,13 @@ extension ThreeDS2ClassicActionHandler { /// Initializes the 3D Secure 2 action handler. internal convenience init(context: AdyenContext, appearanceConfiguration: ADYAppearanceConfiguration, - delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication?) { + delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication?, + presentationDelegate: PresentationDelegate?) { let defaultHandler = createDefaultThreeDS2CoreActionHandler( context: context, appearanceConfiguration: appearanceConfiguration, - delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration + delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration, + presentationDelegate: presentationDelegate ) self.init( context: context, diff --git a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler.swift b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler.swift index 697677866b..89af1711a3 100644 --- a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler.swift +++ b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2ClassicActionHandler.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -16,7 +16,7 @@ internal class ThreeDS2ClassicActionHandler: AnyThreeDS2ActionHandler, Component internal var wrappedComponent: Component { coreActionHandler } internal let coreActionHandler: AnyThreeDS2CoreActionHandler - + internal weak var presentationDelegate: PresentationDelegate? internal var transaction: AnyADYTransaction? { get { coreActionHandler.transaction @@ -46,7 +46,8 @@ internal class ThreeDS2ClassicActionHandler: AnyThreeDS2ActionHandler, Component self.coreActionHandler = coreActionHandler ?? createDefaultThreeDS2CoreActionHandler( context: context, appearanceConfiguration: appearanceConfiguration, - delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration + delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration, + presentationDelegate: presentationDelegate ) self.context = context self.coreActionHandler.service = service diff --git a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler+Initializers.swift b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler+Initializers.swift index a0bad68dc6..b2133ea543 100644 --- a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler+Initializers.swift +++ b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler+Initializers.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -16,7 +16,8 @@ extension ThreeDS2CompactActionHandler { /// Initializes the 3D Secure 2 action handler. internal convenience init(context: AdyenContext, appearanceConfiguration: ADYAppearanceConfiguration, - delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication?) { + delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication?, + presentationDelegate: PresentationDelegate?) { let fingerprintSubmitter = ThreeDS2FingerprintSubmitter(apiContext: context.apiContext) self.init( @@ -26,9 +27,11 @@ extension ThreeDS2CompactActionHandler { coreActionHandler: createDefaultThreeDS2CoreActionHandler( context: context, appearanceConfiguration: appearanceConfiguration, - delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration + delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration, + presentationDelegate: presentationDelegate ), - delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration + delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration, + presentationDelegate: presentationDelegate ) } } diff --git a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler.swift b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler.swift index c904cc4615..4c24c1edf4 100644 --- a/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler.swift +++ b/AdyenActions/Components/3DS2/Action handlers/ThreeDS2CompactActionHandler.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -42,16 +42,20 @@ internal final class ThreeDS2CompactActionHandler: AnyThreeDS2ActionHandler, Com /// - Parameter fingerprintSubmitter: The fingerprint submitter. /// - Parameter service: The 3DS2 Service. /// - Parameter appearanceConfiguration: The appearance configuration of the 3D Secure 2 challenge UI. + /// - Parameter delegatedAuthenticationConfiguration: The delegated authentication configuration. + /// - Parameter presentationDelegate: The presentation delegate internal init(context: AdyenContext, fingerprintSubmitter: AnyThreeDS2FingerprintSubmitter? = nil, service: AnyADYService = ADYServiceAdapter(), appearanceConfiguration: ADYAppearanceConfiguration = ADYAppearanceConfiguration(), coreActionHandler: AnyThreeDS2CoreActionHandler? = nil, - delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication? = nil) { + delegatedAuthenticationConfiguration: ThreeDS2Component.Configuration.DelegatedAuthentication? = nil, + presentationDelegate: PresentationDelegate?) { self.coreActionHandler = coreActionHandler ?? createDefaultThreeDS2CoreActionHandler( context: context, appearanceConfiguration: appearanceConfiguration, - delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration + delegatedAuthenticationConfiguration: delegatedAuthenticationConfiguration, + presentationDelegate: presentationDelegate ) self.fingerprintSubmitter = fingerprintSubmitter ?? ThreeDS2FingerprintSubmitter(apiContext: context.apiContext) self.coreActionHandler.service = service diff --git a/AdyenActions/Components/3DS2/ThreeDS2Component.swift b/AdyenActions/Components/3DS2/ThreeDS2Component.swift index 574713702d..0a1d56b410 100644 --- a/AdyenActions/Components/3DS2/ThreeDS2Component.swift +++ b/AdyenActions/Components/3DS2/ThreeDS2Component.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2024 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -22,7 +22,7 @@ public final class ThreeDS2Component: ActionComponent { /// The delegate of the component. public weak var delegate: ActionComponentDelegate? - /// Delegates `PresentableComponent`'s presentation. + /// Delegates `PresentableComponent`'s presentation. This property must be set if you wish to use delegated authentication. public weak var presentationDelegate: PresentationDelegate? /// Three DS2 component configurations. @@ -58,15 +58,29 @@ public final class ThreeDS2Component: ActionComponent { /// The Apple registered development team identifier. public let appleTeamIdentifier: String + /// The configuration for Delegated Authentication Component style + public let delegatedAuthenticationComponentStyle: DelegatedAuthenticationComponentStyle + + /// The localization parameters, leave it nil to use the default parameters. + public let localizationParameters: LocalizationParameters? + /// Initializes a new instance. /// /// - Parameter localizedRegistrationReason: The localized reason string show to the user while registration flow. /// - Parameter localizedAuthenticationReason: The localized reason string show to the user while authentication flow. /// - Parameter appleTeamIdentifier: The Apple registered development team identifier. - public init(localizedRegistrationReason: String, localizedAuthenticationReason: String, appleTeamIdentifier: String) { + /// - Parameter delegatedAuthenticationComponentStyle: The delegated authentication component style. + /// - Parameter localizationParameters: The localization parameters, leave it nil to use the default parameters. + public init(localizedRegistrationReason: String, + localizedAuthenticationReason: String, + appleTeamIdentifier: String, + delegatedAuthenticationComponentStyle: DelegatedAuthenticationComponentStyle = .init(), + localizationParameters: LocalizationParameters? = nil) { self.localizedRegistrationReason = localizedRegistrationReason self.localizedAuthenticationReason = localizedAuthenticationReason self.appleTeamIdentifier = appleTeamIdentifier + self.delegatedAuthenticationComponentStyle = delegatedAuthenticationComponentStyle + self.localizationParameters = localizationParameters } } @@ -76,6 +90,7 @@ public final class ThreeDS2Component: ActionComponent { /// - redirectComponentStyle: `RedirectComponent` style /// - appearanceConfiguration: The appearance configuration of the 3D Secure 2 challenge UI. /// - requestorAppURL: `threeDSRequestorAppURL` for protocol version 2.2.0 OOB challenges + /// - delegateAuthentication: The configuration for delegate authentication public init(redirectComponentStyle: RedirectComponentStyle? = nil, appearanceConfiguration: ADYAppearanceConfiguration = ADYAppearanceConfiguration(), requestorAppURL: URL? = nil, @@ -95,6 +110,7 @@ public final class ThreeDS2Component: ActionComponent { configuration: Configuration = Configuration()) { self.context = context self.configuration = configuration + self.updateConfiguration() } @@ -208,7 +224,8 @@ public final class ThreeDS2Component: ActionComponent { internal lazy var threeDS2CompactFlowHandler: AnyThreeDS2ActionHandler = { let handler = ThreeDS2CompactActionHandler(context: context, appearanceConfiguration: configuration.appearanceConfiguration, - delegatedAuthenticationConfiguration: configuration.delegateAuthentication) + delegatedAuthenticationConfiguration: configuration.delegateAuthentication, + presentationDelegate: presentationDelegate) handler._isDropIn = _isDropIn handler.threeDSRequestorAppURL = configuration.requestorAppURL @@ -219,10 +236,10 @@ public final class ThreeDS2Component: ActionComponent { internal lazy var threeDS2ClassicFlowHandler: AnyThreeDS2ActionHandler = { let handler = ThreeDS2ClassicActionHandler(context: context, appearanceConfiguration: configuration.appearanceConfiguration, - delegatedAuthenticationConfiguration: configuration.delegateAuthentication) + delegatedAuthenticationConfiguration: configuration.delegateAuthentication, + presentationDelegate: presentationDelegate) handler._isDropIn = _isDropIn handler.threeDSRequestorAppURL = configuration.requestorAppURL - return handler }() diff --git a/AdyenActions/Components/3DS2/ThreeDS2ComponentFingerprint.swift b/AdyenActions/Components/3DS2/ThreeDS2ComponentFingerprint.swift index e0356d07ef..e4b5516324 100644 --- a/AdyenActions/Components/3DS2/ThreeDS2ComponentFingerprint.swift +++ b/AdyenActions/Components/3DS2/ThreeDS2ComponentFingerprint.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -16,15 +16,19 @@ internal extension ThreeDS2Component { internal let sdkReferenceNumber: String? internal let sdkApplicationIdentifier: String? internal let sdkTransactionIdentifier: String? + internal let delegatedAuthenticationSDKOutput: String? + internal let deleteDelegatedAuthenticationCredential: Bool? + internal let threeDS2SDKError: String? - + internal init(deviceInformation: String?, sdkEphemeralPublicKey: ThreeDS2Component.Fingerprint.EphemeralPublicKey?, sdkReferenceNumber: String?, sdkApplicationIdentifier: String?, sdkTransactionIdentifier: String?, delegatedAuthenticationSDKOutput: String?, + deleteDelegatedAuthenticationCredential: Bool?, threeDS2SDKError: String?) { self.deviceInformation = deviceInformation self.sdkEphemeralPublicKey = sdkEphemeralPublicKey @@ -33,11 +37,12 @@ internal extension ThreeDS2Component { self.sdkTransactionIdentifier = sdkTransactionIdentifier self.delegatedAuthenticationSDKOutput = delegatedAuthenticationSDKOutput self.threeDS2SDKError = threeDS2SDKError + self.deleteDelegatedAuthenticationCredential = deleteDelegatedAuthenticationCredential } internal init(threeDS2SDKError: String) { self.threeDS2SDKError = threeDS2SDKError - + self.deleteDelegatedAuthenticationCredential = nil self.deviceInformation = nil self.sdkEphemeralPublicKey = nil self.sdkReferenceNumber = nil @@ -47,7 +52,8 @@ internal extension ThreeDS2Component { } internal init(authenticationRequestParameters: AnyAuthenticationRequestParameters, - delegatedAuthenticationSDKOutput: String?) throws { + delegatedAuthenticationSDKOutput: String?, + deleteDelegatedAuthenticationCredential: Bool?) throws { let sdkEphemeralPublicKeyData = Data(authenticationRequestParameters.sdkEphemeralPublicKey.utf8) let sdkEphemeralPublicKey = try JSONDecoder().decode(EphemeralPublicKey.self, from: sdkEphemeralPublicKeyData) @@ -57,16 +63,19 @@ internal extension ThreeDS2Component { self.sdkApplicationIdentifier = authenticationRequestParameters.sdkApplicationIdentifier self.sdkTransactionIdentifier = authenticationRequestParameters.sdkTransactionIdentifier self.delegatedAuthenticationSDKOutput = delegatedAuthenticationSDKOutput + self.deleteDelegatedAuthenticationCredential = deleteDelegatedAuthenticationCredential self.threeDS2SDKError = nil } - internal func withDelegatedAuthenticationSDKOutput(delegatedAuthenticationSDKOutput: String?) -> Fingerprint { + internal func withDelegatedAuthenticationSDKOutput(delegatedAuthenticationSDKOutput: String?, + deleteDelegatedAuthenticationCredential: Bool?) -> Fingerprint { .init(deviceInformation: deviceInformation, sdkEphemeralPublicKey: sdkEphemeralPublicKey, sdkReferenceNumber: sdkReferenceNumber, sdkApplicationIdentifier: sdkApplicationIdentifier, sdkTransactionIdentifier: sdkTransactionIdentifier, delegatedAuthenticationSDKOutput: delegatedAuthenticationSDKOutput, + deleteDelegatedAuthenticationCredential: deleteDelegatedAuthenticationCredential, threeDS2SDKError: threeDS2SDKError) } @@ -78,6 +87,7 @@ internal extension ThreeDS2Component { case sdkTransactionIdentifier = "sdkTransID" case delegatedAuthenticationSDKOutput case threeDS2SDKError + case deleteDelegatedAuthenticationCredential } } diff --git a/AdyenActions/Components/3DS2/ThreeDSResult.swift b/AdyenActions/Components/3DS2/ThreeDSResult.swift index d506175aae..45b17578b6 100644 --- a/AdyenActions/Components/3DS2/ThreeDSResult.swift +++ b/AdyenActions/Components/3DS2/ThreeDSResult.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -55,7 +55,7 @@ public struct ThreeDSResult: Decodable { transStatus: oldPayload.transStatus) return try .init(payload: AdyenCoder.encode(newPayload).base64EncodedString()) } - + internal init(authenticated: Bool, authorizationToken: String?) throws { var payloadJson = ["transStatus": authenticated ? "Y" : "N"] diff --git a/AdyenActions/Components/QRCode/ExpirationTimer.swift b/AdyenActions/Components/QRCode/ExpirationTimer.swift index db75c2e279..0f95516437 100644 --- a/AdyenActions/Components/QRCode/ExpirationTimer.swift +++ b/AdyenActions/Components/QRCode/ExpirationTimer.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -51,6 +51,16 @@ internal final class ExpirationTimer { timer = nil } + internal func pauseTimer() { + stopTimer() + } + + internal func resumeTimer() { + timer = Timer.scheduledTimer(withTimeInterval: tickInterval, repeats: true) { [weak self] _ in + self?.onTimerTick() + } + } + private func onTimerTick() { timeLeft -= 1 diff --git a/AdyenActions/UI/UI Style/ActionComponentStyle.swift b/AdyenActions/UI/UI Style/ActionComponentStyle.swift index 70251aa296..8dd9aec640 100644 --- a/AdyenActions/UI/UI Style/ActionComponentStyle.swift +++ b/AdyenActions/UI/UI Style/ActionComponentStyle.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -25,6 +25,9 @@ public struct ActionComponentStyle { /// Indicates the UI configuration of the document action component. public var documentActionComponentStyle: DocumentComponentStyle + /// Indicates the UI configuration of the delegated authentication screens. + public var delegatedAuthenticationComponentStyle: DelegatedAuthenticationComponentStyle + /// Initializes the /// - Parameters: /// - redirectComponentStyle: The UI configuration of the redirect component. @@ -32,17 +35,20 @@ public struct ActionComponentStyle { /// - voucherComponentStyle: The UI configuration of the voucher component. /// - qrCodeComponentStyle: The UI configuration of the QR code component. /// - documentActionComponentStyle: The UI configuration of the document action component. + /// - delegatedAuthenticationComponentStyle: The UI configuration of the delegated authentication component. public init( redirectComponentStyle: RedirectComponentStyle = RedirectComponentStyle(), awaitComponentStyle: AwaitComponentStyle = AwaitComponentStyle(), voucherComponentStyle: VoucherComponentStyle = VoucherComponentStyle(), qrCodeComponentStyle: QRCodeComponentStyle = QRCodeComponentStyle(), - documentActionComponentStyle: DocumentComponentStyle = DocumentComponentStyle() + documentActionComponentStyle: DocumentComponentStyle = DocumentComponentStyle(), + delegatedAuthenticationComponentStyle: DelegatedAuthenticationComponentStyle = DelegatedAuthenticationComponentStyle() ) { self.redirectComponentStyle = redirectComponentStyle self.awaitComponentStyle = awaitComponentStyle self.voucherComponentStyle = voucherComponentStyle self.qrCodeComponentStyle = qrCodeComponentStyle self.documentActionComponentStyle = documentActionComponentStyle + self.delegatedAuthenticationComponentStyle = delegatedAuthenticationComponentStyle } } diff --git a/AdyenActions/UI/UI Style/DelegatedAuthenticationComponentStyle.swift b/AdyenActions/UI/UI Style/DelegatedAuthenticationComponentStyle.swift new file mode 100644 index 0000000000..d4490dfaa8 --- /dev/null +++ b/AdyenActions/UI/UI Style/DelegatedAuthenticationComponentStyle.swift @@ -0,0 +1,69 @@ +// +// Copyright (c) 2023 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +import Foundation +@_spi(AdyenInternal) import Adyen +import UIKit + +/// Contains the styling customization options for Delegated Authentication Screens(Registration & Approval) +public struct DelegatedAuthenticationComponentStyle { + + /// The background color of the screens + public var backgroundColor = UIColor.Adyen.componentBackground + + /// The Image style of the biometric logo + public var imageStyle: ImageStyle = .init(borderColor: nil, + borderWidth: 0.0, + cornerRadius: 0.0, + clipsToBounds: true, + contentMode: .scaleToFill) + /// The text style of the header. + public var headerTextStyle = TextStyle(font: .preferredFont(forTextStyle: .title1), + color: UIColor.Adyen.componentLabel, + textAlignment: .center) + + /// The text style of the description. + public var descriptionTextStyle = TextStyle(font: .preferredFont(forTextStyle: .body), + color: UIColor.Adyen.componentSecondaryLabel, + textAlignment: .center) + + /// The style of the timer progress view. + public var progressViewStyle = ProgressViewStyle( + progressTintColor: UIColor.Adyen.defaultBlue, + trackTintColor: UIColor.Adyen.lightGray + ) + + /// The text style of the text under the progress view to indicate the time remaining. + public var remainingTimeTextStyle = TextStyle(font: .preferredFont(forTextStyle: .caption1), + color: UIColor.Adyen.componentSecondaryLabel, + textAlignment: .center) + + /// The text style of the option to delete the credentials in the approval screen. + public var textViewStyle = TextStyle(font: .preferredFont(forTextStyle: .footnote), + color: UIColor.Adyen.componentLabel, + textAlignment: .center) + + /// The primary button style. + public var primaryButton = ButtonStyle( + title: TextStyle(font: .preferredFont(forTextStyle: .headline), + color: .white), + cornerRadius: 8, + background: UIColor.Adyen.defaultBlue + ) + + /// The secondary button style. + public var secondaryButton = ButtonStyle( + title: TextStyle(font: .preferredFont(forTextStyle: .headline), + color: UIColor.Adyen.defaultBlue), + cornerRadius: 8, + background: .clear + ) + + /// Creates a component style with the default styling + public init() { + imageStyle.tintColor = .systemGray + } +} diff --git a/AdyenActions/UI/View Controllers/DelegatedAuthentication/DAApprovalViewController.swift b/AdyenActions/UI/View Controllers/DelegatedAuthentication/DAApprovalViewController.swift new file mode 100644 index 0000000000..1ac558a785 --- /dev/null +++ b/AdyenActions/UI/View Controllers/DelegatedAuthentication/DAApprovalViewController.swift @@ -0,0 +1,157 @@ +// +// Copyright (c) 2023 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +@_spi(AdyenInternal) import Adyen +import UIKit + +internal final class DAApprovalViewController: UIViewController { + private enum Constants { + static let timeout: TimeInterval = 90.0 + } + + private let useBiometricsHandler: Handler + private let approveDifferentlyHandler: Handler + private let removeCredentialsHandler: Handler + private lazy var alert: UIAlertController = { + let alertController = UIAlertController(title: localizedString(.threeds2DAApprovalRemoveAlertTitle, localizationParameters), + message: localizedString(.threeds2DAApprovalRemoveAlertDescription, localizationParameters), + preferredStyle: .alert) + let removeAction = UIAlertAction(title: localizedString(.threeds2DAApprovalRemoveAlertPositiveButton, localizationParameters), + style: .cancel, + handler: { [weak self] _ in + self?.removeCredentialsHandler() + }) + let cancelAction = UIAlertAction(title: localizedString(.threeds2DAApprovalRemoveAlertNegativeButton, localizationParameters), + style: .default, + handler: { [weak self] _ in + self?.timeoutTimer?.resumeTimer() + }) + alertController.addAction(cancelAction) + alertController.addAction(removeAction) + return alertController + }() + + private lazy var containerView = UIView(frame: .zero) + private lazy var approvalView: DelegatedAuthenticationView = .init(logoStyle: style.imageStyle, + headerTextStyle: style.headerTextStyle, + descriptionTextStyle: style.descriptionTextStyle, + progressViewStyle: style.progressViewStyle, + progressTextStyle: style.remainingTimeTextStyle, + firstButtonStyle: style.primaryButton, + secondButtonStyle: style.secondaryButton, + textViewStyle: style.textViewStyle, + linkSelectionHandler: deleteCredentialSelected) + + private let style: DelegatedAuthenticationComponentStyle + private var timeoutTimer: ExpirationTimer? + private let localizationParameters: LocalizationParameters? + + internal typealias Handler = () -> Void + + internal init(style: DelegatedAuthenticationComponentStyle, + localizationParameters: LocalizationParameters?, + useBiometricsHandler: @escaping Handler, + approveDifferentlyHandler: @escaping Handler, + removeCredentialsHandler: @escaping Handler) { + self.style = style + self.useBiometricsHandler = useBiometricsHandler + self.approveDifferentlyHandler = approveDifferentlyHandler + self.removeCredentialsHandler = removeCredentialsHandler + self.localizationParameters = localizationParameters + super.init(nibName: nil, bundle: Bundle(for: DAApprovalViewController.self)) + approvalView.delegate = self + if #available(iOS 13.0, *) { + isModalInPresentation = true + } + } + + @available(*, unavailable) + internal required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override internal func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = style.backgroundColor + configureDelegateAuthenticationView() + buildUI() + } + + private func configureDelegateAuthenticationView() { + approvalView.titleLabel.text = localizedString(.threeds2DAApprovalTitle, localizationParameters) + approvalView.descriptionLabel.text = localizedString(.threeds2DAApprovalDescription, localizationParameters) + approvalView.firstButton.title = localizedString(.threeds2DAApprovalPositiveButton, localizationParameters) + approvalView.secondButton.title = localizedString(.threeds2DAApprovalNegativeButton, localizationParameters) + configureProgress() + approvalView.textView.update(text: localizedString(.threeds2DAApprovalRemoveCredentialsText, localizationParameters), + style: style.textViewStyle) + } + + private func configureProgress() { + let timeout = Constants.timeout + approvalView.progressText.text = timeLeft(timeInterval: timeout) + timeoutTimer = ExpirationTimer( + expirationTimeout: timeout, + onTick: { [weak self] in + self?.approvalView.progressView.progress = Float($0 / timeout) + self?.approvalView.progressText.text = self?.timeLeft(timeInterval: $0) + }, + onExpiration: { [weak self] in + self?.secondButtonTapped() + } + ) + timeoutTimer?.startTimer() + } + + private func timeLeft(timeInterval: TimeInterval) -> String { + String(format: localizedString(.threeds2DAApprovalTimeLeft, localizationParameters), timeInterval.adyen.timeLeftString() ?? "0") + } + + private func buildUI() { + containerView.addSubview(approvalView) + view.addSubview(containerView) + containerView.translatesAutoresizingMaskIntoConstraints = false + approvalView.translatesAutoresizingMaskIntoConstraints = false + containerView.adyen.anchor(inside: view.safeAreaLayoutGuide) + approvalView.adyen.anchor(inside: containerView) + } + + override internal var preferredContentSize: CGSize { + get { + containerView.adyen.minimalSize + } + + // swiftlint:disable:next unused_setter_value + set { AdyenAssertion.assertionFailure(message: """ + PreferredContentSize is overridden for this view controller. + getter - returns minimum possible content size. + setter - not implemented. + """) } + } + + private func deleteCredentialSelected(index: Int) { + removeCredential() + } +} + +extension DAApprovalViewController: DelegatedAuthenticationViewDelegate { + internal func removeCredential() { + timeoutTimer?.pauseTimer() + present(alert, animated: true) + } + + internal func firstButtonTapped() { + approvalView.firstButton.showsActivityIndicator = true + timeoutTimer?.stopTimer() + useBiometricsHandler() + } + + internal func secondButtonTapped() { + approvalView.secondButton.showsActivityIndicator = true + timeoutTimer?.stopTimer() + approveDifferentlyHandler() + } +} diff --git a/AdyenActions/UI/View Controllers/DelegatedAuthentication/DARegistrationViewController.swift b/AdyenActions/UI/View Controllers/DelegatedAuthentication/DARegistrationViewController.swift new file mode 100644 index 0000000000..9385e8697a --- /dev/null +++ b/AdyenActions/UI/View Controllers/DelegatedAuthentication/DARegistrationViewController.swift @@ -0,0 +1,125 @@ +// +// Copyright (c) 2023 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +@_spi(AdyenInternal) import Adyen +import UIKit + +internal final class DARegistrationViewController: UIViewController { + private enum Constants { + static let timeout: TimeInterval = 90.0 + } + + private let enableCheckoutHandler: Handler + private let notNowHandler: Handler + private lazy var containerView = UIView(frame: .zero) + + private lazy var scrollView = UIScrollView() + private lazy var registrationView: DelegatedAuthenticationView = .init(logoStyle: style.imageStyle, + headerTextStyle: style.headerTextStyle, + descriptionTextStyle: style.descriptionTextStyle, + progressViewStyle: style.progressViewStyle, + progressTextStyle: style.remainingTimeTextStyle, + firstButtonStyle: style.primaryButton, + secondButtonStyle: style.secondaryButton, + textViewStyle: style.textViewStyle) + private let style: DelegatedAuthenticationComponentStyle + private var timeoutTimer: ExpirationTimer? + internal typealias Handler = () -> Void + + private let localizationParameters: LocalizationParameters? + + internal init(style: DelegatedAuthenticationComponentStyle, + localizationParameters: LocalizationParameters?, + enableCheckoutHandler: @escaping Handler, + notNowHandler: @escaping Handler) { + self.style = style + self.localizationParameters = localizationParameters + self.enableCheckoutHandler = enableCheckoutHandler + self.notNowHandler = notNowHandler + super.init(nibName: nil, bundle: Bundle(for: DARegistrationViewController.self)) + registrationView.delegate = self + if #available(iOS 13.0, *) { + isModalInPresentation = true + } + } + + @available(*, unavailable) + internal required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override internal func viewDidLoad() { + super.viewDidLoad() + buildUI() + configureDelegateAuthenticationView() + view.backgroundColor = style.backgroundColor + configureDelegateAuthenticationView() + } + + private func configureDelegateAuthenticationView() { + registrationView.titleLabel.text = localizedString(.threeds2DARegistrationTitle, localizationParameters) + registrationView.descriptionLabel.text = localizedString(.threeds2DARegistrationDescription, localizationParameters) + registrationView.firstButton.title = localizedString(.threeds2DARegistrationPositiveButton, localizationParameters) + registrationView.secondButton.title = localizedString(.threeds2DARegistrationNegativeButton, localizationParameters) + configureProgress() + } + + private func configureProgress() { + let timeout = Constants.timeout + registrationView.progressText.text = timeLeft(timeInterval: timeout) + timeoutTimer = ExpirationTimer( + expirationTimeout: timeout, + onTick: { [weak self] in + self?.registrationView.progressView.progress = Float($0 / timeout) + self?.registrationView.progressText.text = self?.timeLeft(timeInterval: $0) + }, + onExpiration: { [weak self] in + self?.secondButtonTapped() + } + ) + timeoutTimer?.startTimer() + } + + private func timeLeft(timeInterval: TimeInterval) -> String { + String(format: localizedString(.threeds2DARegistrationTimeLeft, localizationParameters), timeInterval.adyen.timeLeftString() ?? "0") + } + + private func buildUI() { + containerView.addSubview(registrationView) + view.addSubview(containerView) + containerView.translatesAutoresizingMaskIntoConstraints = false + registrationView.translatesAutoresizingMaskIntoConstraints = false + containerView.adyen.anchor(inside: view.safeAreaLayoutGuide) + registrationView.adyen.anchor(inside: containerView) + } + + override internal var preferredContentSize: CGSize { + get { + containerView.adyen.minimalSize + } + + // swiftlint:disable:next unused_setter_value + set { AdyenAssertion.assertionFailure(message: """ + PreferredContentSize is overridden for this view controller. + getter - returns minimum possible content size. + setter - no implemented. + """) } + } +} + +extension DARegistrationViewController: DelegatedAuthenticationViewDelegate { + internal func firstButtonTapped() { + registrationView.firstButton.showsActivityIndicator = true + timeoutTimer?.stopTimer() + enableCheckoutHandler() + } + + internal func secondButtonTapped() { + timeoutTimer?.stopTimer() + registrationView.secondButton.showsActivityIndicator = true + notNowHandler() + } +} diff --git a/AdyenActions/UI/View Controllers/DelegatedAuthentication/DelegatedAuthenticationView.swift b/AdyenActions/UI/View Controllers/DelegatedAuthentication/DelegatedAuthenticationView.swift new file mode 100644 index 0000000000..069923148f --- /dev/null +++ b/AdyenActions/UI/View Controllers/DelegatedAuthentication/DelegatedAuthenticationView.swift @@ -0,0 +1,210 @@ +// +// Copyright (c) 2023 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +@_spi(AdyenInternal) import Adyen +import Foundation +import UIKit + +internal protocol DelegatedAuthenticationViewDelegate: AnyObject { + func firstButtonTapped() + func secondButtonTapped() +} + +internal final class DelegatedAuthenticationView: UIView { + private let logoStyle: ImageStyle + private let headerTextStyle: TextStyle + private let descriptionTextStyle: TextStyle + private let progressViewStyle: ProgressViewStyle + private let progressTextStyle: TextStyle + private let firstButtonStyle: ButtonStyle + private let secondButtonStyle: ButtonStyle + private let textViewStyle: TextStyle + private let linkSelectionHandler: (Int) -> Void + + internal weak var delegate: DelegatedAuthenticationViewDelegate? + + internal lazy var image: UIImageView = { + let image = UIImage(named: "biometric", in: Bundle.actionsInternalResources, compatibleWith: nil) + let imageView = UIImageView(style: logoStyle) + imageView.image = image?.withRenderingMode(.alwaysTemplate) + imageView.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "image") + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + + internal lazy var titleLabel: UILabel = { + let label = UILabel(style: headerTextStyle) + label.isAccessibilityElement = false + label.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "titleLabel") + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + internal lazy var descriptionLabel: UILabel = { + let label = UILabel(style: descriptionTextStyle) + label.isAccessibilityElement = false + label.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "descriptionLabel") + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + + return label + }() + + internal lazy var labelsStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel]) + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 8.0 + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + internal lazy var progressView: UIProgressView = { + let view = UIProgressView(style: progressViewStyle) + view.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "progressView") + view.translatesAutoresizingMaskIntoConstraints = false + view.progress = 1 + + return view + }() + + internal lazy var progressText: UILabel = { + let label = UILabel(style: progressTextStyle) + label.isAccessibilityElement = false + label.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "progressText") + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + internal lazy var progressStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [progressView, progressText]) + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 16.0 + stackView.translatesAutoresizingMaskIntoConstraints = false + + return stackView + }() + + internal lazy var firstButton: SubmitButton = { + let button = SubmitButton(style: firstButtonStyle) + + button.addTarget(self, action: #selector(firstButtonTapped), for: .touchUpInside) + button.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "primaryButton") + button.preservesSuperviewLayoutMargins = true + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + internal lazy var secondButton: SubmitButton = { + + let button = SubmitButton(style: secondButtonStyle) + + button.addTarget(self, action: #selector(secondButtonTapped), for: .touchUpInside) + button.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "secondaryButton") + button.preservesSuperviewLayoutMargins = true + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + internal lazy var buttonsStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [firstButton, secondButton]) + stackView.axis = .vertical + stackView.spacing = 20 + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + internal lazy var textView: LinkTextView = { + let textView = LinkTextView(linkSelectionHandler: linkSelectionHandler) + textView.accessibilityIdentifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "textView") + textView.translatesAutoresizingMaskIntoConstraints = false + return textView + }() + + // MARK: - initializers + + internal init(logoStyle: ImageStyle, + headerTextStyle: TextStyle, + descriptionTextStyle: TextStyle, + progressViewStyle: ProgressViewStyle, + progressTextStyle: TextStyle, + firstButtonStyle: ButtonStyle, + secondButtonStyle: ButtonStyle, + textViewStyle: TextStyle, + linkSelectionHandler: @escaping (Int) -> Void = { _ in }) { + self.logoStyle = logoStyle + self.headerTextStyle = headerTextStyle + self.descriptionTextStyle = descriptionTextStyle + self.progressViewStyle = progressViewStyle + self.progressTextStyle = progressTextStyle + self.firstButtonStyle = firstButtonStyle + self.secondButtonStyle = secondButtonStyle + self.textViewStyle = textViewStyle + self.linkSelectionHandler = linkSelectionHandler + super.init(frame: .zero) + configureViews() + } + + @available(*, unavailable) + internal required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Configuration + + private func configureViews() { + addSubview(image) + addSubview(labelsStackView) + addSubview(progressStackView) + addSubview(buttonsStackView) + addSubview(textView) + + NSLayoutConstraint.activate([ + image.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 50), + image.centerXAnchor.constraint(equalTo: layoutMarginsGuide.centerXAnchor), + image.widthAnchor.constraint(equalToConstant: 30), + image.heightAnchor.constraint(equalToConstant: 34), + + labelsStackView.topAnchor.constraint(equalTo: image.bottomAnchor, constant: 24), + labelsStackView.centerXAnchor.constraint(equalTo: centerXAnchor), + labelsStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 15.0), + labelsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: -15.0), + + progressView.widthAnchor.constraint(equalToConstant: 200), + progressStackView.topAnchor.constraint(equalTo: labelsStackView.bottomAnchor, constant: 24), + progressStackView.centerXAnchor.constraint(equalTo: layoutMarginsGuide.centerXAnchor), + + firstButton.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + firstButton.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + + secondButton.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + secondButton.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + + buttonsStackView.topAnchor.constraint(equalTo: progressStackView.bottomAnchor, constant: 24), + buttonsStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + buttonsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + + textView.heightAnchor.constraint(equalToConstant: 50), + textView.topAnchor.constraint(equalTo: buttonsStackView.bottomAnchor, constant: 24), + textView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: -15), + textView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 15.0), + textView.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor) + ]) + } + + @objc private func firstButtonTapped() { + delegate?.firstButtonTapped() + } + + @objc private func secondButtonTapped() { + delegate?.secondButtonTapped() + } + +} diff --git a/AdyenCard/Components/Card/CardDetails.swift b/AdyenCard/Components/Card/CardDetails.swift index a7222d0c7d..7ddd3fd92e 100644 --- a/AdyenCard/Components/Card/CardDetails.swift +++ b/AdyenCard/Components/Card/CardDetails.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2023 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -131,7 +131,7 @@ public struct CardDetails: PaymentMethodDetails, ShopperInformation { self.password = nil self.socialSecurityNumber = nil self.selectedBrand = nil - self.delegatedAuthenticationData = nil + self.delegatedAuthenticationData = Self.createDelegatedAuthenticationData() } // MARK: - Encoding diff --git a/AdyenCard/Components/Card/CardViewControllerItemsProvider.swift b/AdyenCard/Components/Card/CardViewControllerItemsProvider.swift index 6d23f925e6..c0a6a2e58b 100644 --- a/AdyenCard/Components/Card/CardViewControllerItemsProvider.swift +++ b/AdyenCard/Components/Card/CardViewControllerItemsProvider.swift @@ -108,7 +108,7 @@ extension CardViewController { localizationParameters: localizationParameters) expiryDateItem.localizationParameters = localizationParameters expiryDateItem.identifier = ViewIdentifierBuilder.build(scopeInstance: scope, postfix: "expiryDateItem") - + return expiryDateItem }() diff --git a/AdyenCard/Form/FormCardNumberItemView.swift b/AdyenCard/Form/FormCardNumberItemView.swift index 2dbf2dca30..f668752120 100644 --- a/AdyenCard/Form/FormCardNumberItemView.swift +++ b/AdyenCard/Form/FormCardNumberItemView.swift @@ -22,7 +22,7 @@ internal final class FormCardNumberItemView: FormTextItemView Cartfile + echo "github \"adyen/adyen-authentication-ios\" == 2.0.0" >> Cartfile carthage update --use-xcframeworks --configuration Debug else cd $PROJECT_NAME diff --git a/Tests/Card Tests/3DS2 Component/AuthenticationServiceMock.swift b/Tests/Card Tests/3DS2 Component/AuthenticationServiceMock.swift index 47ba2442a4..20bd645997 100644 --- a/Tests/Card Tests/3DS2 Component/AuthenticationServiceMock.swift +++ b/Tests/Card Tests/3DS2 Component/AuthenticationServiceMock.swift @@ -9,15 +9,23 @@ import Foundation import AdyenAuthentication @available(iOS 14.0, *) - internal final class AuthenticationServiceMock: AuthenticationServiceProtocol { +internal final class AuthenticationServiceMock: AuthenticationServiceProtocol { + func registeredCredentials(withAuthenticationInput input: String) async throws -> [String] { + return [] + } + + func isDeviceRegistered(withAuthenticationInput input: String) async throws -> Bool { + isDeviceRegistered + } + internal var isDeviceRegistered: Bool = true + internal var isDeviceSupported: Bool = true - internal var isRegistration: Bool = true internal var onRegister: ((_: String) async throws -> String)? - internal func register(withRegistrationInput input: String) async throws -> String { - if let onRegister { + internal func register(withRegistrationInput input: String) async throws -> String { + if let onRegister = onRegister { return try await onRegister(input) } else { // swiftlint:disable:next line_length @@ -26,9 +34,9 @@ import Foundation } internal var onAuthenticate: ((_: String) async throws -> String)? - - internal func authenticate(withAuthenticationInput input: String) async throws -> String { - if let onAuthenticate { + + internal func authenticate(withAuthenticationInput input: String) async throws -> String { + if let onAuthenticate = onAuthenticate { return try await onAuthenticate(input) } else if isRegistration { throw AdyenAuthenticationError.noStoredCredentialsMatch(nil) @@ -38,43 +46,15 @@ import Foundation } } - internal func reset() throws {} - - internal func checkSupport() throws -> String { - "eyJkZXZpY2UiOiJpT1MifQ" + internal var onReset: (() -> Void)? + + internal func reset() throws { + onReset?() } - internal func isDeviceRegistered(withAuthenticationInput input: String) async throws -> Bool { - fatalError("Not implemented") - } - - func registeredCredentials(withAuthenticationInput input: String) async throws -> [String] { - fatalError("Not implemented") + internal func checkSupport() throws -> String { + "eyJkZXZpY2UiOiJpT1MifQ" } } - - extension String { - - internal func dataFromBase64URL() throws -> Data { - var base64 = self - base64 = base64.replacingOccurrences(of: "-", with: "+") - base64 = base64.replacingOccurrences(of: "_", with: "/") - while base64.count % 4 != 0 { - base64 = base64.appending("=") - } - guard let data = Data(base64Encoded: base64) else { - throw AdyenAuthenticationError.invalidBase64String - } - return data - } - - internal func toBase64URL() -> String { - var result = self - result = result.replacingOccurrences(of: "+", with: "-") - result = result.replacingOccurrences(of: "/", with: "_") - result = result.replacingOccurrences(of: "=", with: "") - return result - } - } #endif diff --git a/Tests/Card Tests/3DS2 Component/ThreeDS2ClassicActionHandlerTests.swift b/Tests/Card Tests/3DS2 Component/ThreeDS2ClassicActionHandlerTests.swift index ec0ac309e5..632335b166 100644 --- a/Tests/Card Tests/3DS2 Component/ThreeDS2ClassicActionHandlerTests.swift +++ b/Tests/Card Tests/3DS2 Component/ThreeDS2ClassicActionHandlerTests.swift @@ -67,7 +67,8 @@ class ThreeDS2ClassicActionHandlerTests: XCTestCase { let fingerprint = try ThreeDS2Component.Fingerprint( authenticationRequestParameters: authenticationRequestParameters, - delegatedAuthenticationSDKOutput: nil + delegatedAuthenticationSDKOutput: nil, + deleteDelegatedAuthenticationCredential: nil ) let expectedFingerprint = try AdyenCoder.encodeBase64(fingerprint) diff --git a/Tests/Card Tests/3DS2 Component/ThreeDS2CompactActionHandlerTests.swift b/Tests/Card Tests/3DS2 Component/ThreeDS2CompactActionHandlerTests.swift index d958c258d2..747b00c17d 100644 --- a/Tests/Card Tests/3DS2 Component/ThreeDS2CompactActionHandlerTests.swift +++ b/Tests/Card Tests/3DS2 Component/ThreeDS2CompactActionHandlerTests.swift @@ -42,13 +42,13 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { } func testSettingThreeDSRequestorAppURL() { - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, appearanceConfiguration: ADYAppearanceConfiguration()) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, appearanceConfiguration: ADYAppearanceConfiguration(), presentationDelegate: nil) sut.threeDSRequestorAppURL = URL(string: "https://google.com") XCTAssertEqual(sut.coreActionHandler.threeDSRequestorAppURL, URL(string: "https://google.com")) } func testWrappedComponent() { - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, appearanceConfiguration: ADYAppearanceConfiguration()) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, appearanceConfiguration: ADYAppearanceConfiguration(), presentationDelegate: nil) XCTAssertEqual(sut.wrappedComponent.context.apiContext.clientKey, Dummy.apiContext.clientKey) XCTAssertEqual(sut.wrappedComponent.context.apiContext.environment.baseURL, Dummy.apiContext.environment.baseURL) @@ -73,7 +73,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { paymentData: "paymentData") let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) sut.handle(fingerprintAction) { result in switch result { case .success: @@ -104,7 +104,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { service.mockedTransaction = transaction let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, service: service, presentationDelegate: nil) sut.threeDSRequestorAppURL = URL(string: "https://google.com") sut.transaction = transaction sut.handle(challengeAction) { challengeResult in @@ -147,7 +147,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { completion(nil, Dummy.error) } - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) sut.transaction = mockedTransaction let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") @@ -192,7 +192,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { completion(nil, Dummy.error) } - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) sut.transaction = mockedTransaction let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") @@ -219,7 +219,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { let service = AnyADYServiceMock() - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") sut.handle(challengeAction) { result in @@ -256,7 +256,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { messageVersion: "messageVersion") let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) sut.handle(fingerprintAction) { result in switch result { case .success: @@ -285,7 +285,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { service.authenticationRequestParameters = authenticationRequestParameters let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) sut.handle(fingerprintAction) { result in switch result { case let .success(result): @@ -321,7 +321,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { service.authenticationRequestParameters = authenticationRequestParameters let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) sut.handle(fingerprintAction) { result in switch result { case let .success(result): @@ -354,7 +354,7 @@ class ThreeDS2CompactActionHandlerTests: XCTestCase { service.authenticationRequestParameters = authenticationRequestParameters let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") - let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service) + let sut = ThreeDS2CompactActionHandler(context: Dummy.context, fingerprintSubmitter: submitter, service: service, presentationDelegate: nil) sut.handle(fingerprintAction) { result in switch result { case .success: diff --git a/Tests/Card Tests/3DS2 Component/ThreeDS2ComponentTests.swift b/Tests/Card Tests/3DS2 Component/ThreeDS2ComponentTests.swift index cc44c2c0fe..1c5919039d 100644 --- a/Tests/Card Tests/3DS2 Component/ThreeDS2ComponentTests.swift +++ b/Tests/Card Tests/3DS2 Component/ThreeDS2ComponentTests.swift @@ -9,6 +9,7 @@ @_spi(AdyenInternal) @testable import AdyenActions @testable @_spi(AdyenInternal) import AdyenCard import XCTest +@_spi(AdyenInternal) import Adyen class ThreeDS2ComponentTests: XCTestCase { @@ -401,5 +402,249 @@ class ThreeDS2ComponentTests: XCTestCase { waitForExpectations(timeout: 2, handler: nil) } +#if canImport(AdyenAuthentication) + @available(iOS 14.0, *) + + /// A positive flow, when DA is registered on the device, & user taps on approve - PresentationDelegateMock. We expect the approval flow to succeed. + func testDelegatedAuthenticationWhenDeviceIsRegisteredAndUserApproves() { + enum TestData { + static let fingerprintToken = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES0lucHV0IjoiIyNTb21lZGVsZWdhdGVkQXV0aGVudGljYXRpb25TREtJbnB1dCMjIiwiZGlyZWN0b3J5U2VydmVySWQiOiJGMDEzMzcxMzM3IiwiZGlyZWN0b3J5U2VydmVyUHVibGljS2V5IjoiI0RpcmVjdG9yeVNlcnZlclB1YmxpY0tleSMiLCJkaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIjoiIyNEaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIyMiLCJ0aHJlZURTTWVzc2FnZVZlcnNpb24iOiIyLjIuMCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiMTUwZmEzYjgtZTZjOC00N2ExLTk2ZTAtOTEwNzYzYmVlYzU3In0=" + } + + let redirectComponent = AnyRedirectComponentMock() + redirectComponent.onHandle = { action in + XCTFail("RedirectComponent should never be invoked.") + } + + // A mock for the Authentication SDK + let authenticationServiceMock = AuthenticationServiceMock() + let onAuthenticateExpectation = expectation(description: "On Authentication - should be called in the AuthneticationSDK") + authenticationServiceMock.onAuthenticate = { _ in + onAuthenticateExpectation.fulfill() + return "onAuthenticate-Return" + } + authenticationServiceMock.onRegister = { _ in + XCTFail("On Register should not be called in the SDK.") + return "onRegister-Return" + } + + let delegate = ActionComponentDelegateMock() + + // A mock for the one which will present the screens if needed. + let presentationDelegateMock = PresentationDelegateMock() + + // A mock for the 3ds2 sdk + let mockService = AnyADYServiceMock() + mockService.authenticationRequestParameters = AuthenticationRequestParametersMock(deviceInformation: "device_info", + sdkApplicationIdentifier: "sdkApplicationIdentifier", + sdkTransactionIdentifier: "sdkTransactionIdentifier", + sdkReferenceNumber: "sdkReferenceNumber", + sdkEphemeralPublicKey: "{\"y\":\"zv0kz1SKfNvT3ql75L217de6ZszxfLA8LUKOIKe5Zf4\",\"x\":\"3b3mPfWhuOxwOWydLejS3DJEUPiMVFxtzGCV6906rfc\",\"kty\":\"EC\",\"crv\":\"P-256\"}", + messageVersion: "messageVersion") + + + let threeDS2ActionHandler = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: mockService, + presenter: ThreeDS2PlusDAScreenPresenter(presentationDelegate: presentationDelegateMock, + style: .init(), + localizedParameters: nil), + delegatedAuthenticationService: authenticationServiceMock, + deviceSupportCheckerService: DeviceSupportCheckerMock(isDeviceSupported: true)) + + let classicActionHandler = ThreeDS2ClassicActionHandler.init(context: Dummy.context, service: mockService, coreActionHandler: threeDS2ActionHandler) + + let sut = ThreeDS2Component(context: Dummy.context, + threeDS2CompactFlowHandler: AnyThreeDS2ActionHandlerMock(), + threeDS2ClassicFlowHandler: classicActionHandler, + redirectComponent: redirectComponent) + sut.presentationDelegate = presentationDelegateMock + let delegateExpectation = expectation(description: "Expect delegate didProvide(_:from:) function to be called.") + delegate.onDidProvide = { data, component in + XCTAssertTrue(component === sut) + XCTAssertEqual(data.paymentData, "data") + + let threeDS2Details = data.details as! ThreeDS2Details + + switch threeDS2Details { + case let .fingerprint(result): + let data = Data(base64Encoded: result) + let json = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String: AnyObject] + XCTAssertEqual(json?["delegatedAuthenticationSDKOutput"] as! String, "onAuthenticate-Return") // Should be the same one returned by the AuthenticationSDK + default: + XCTFail() + } + + delegateExpectation.fulfill() + } + + // Check if the UI is displayed & simulate the tap of the first button which is approve. + let presentationExpectation = expectation(description: "Approval view controller should be shown.") + presentationDelegateMock.doPresent = { component in + let approvalViewController = component.viewController as? DAApprovalViewController + XCTAssertNotNil(approvalViewController) + self.verifyApprovalView(viewController: approvalViewController) + approvalViewController?.firstButtonTapped() + presentationExpectation.fulfill() + } + + sut.delegate = delegate + + let fingerprintAction = ThreeDS2FingerprintAction(fingerprintToken: TestData.fingerprintToken, authorisationToken: "AuthToken", paymentData: "data") + sut.handle(fingerprintAction) + + waitForExpectations(timeout: 3, handler: nil) + } + + @available(iOS 14.0, *) + func testDelegatedAuthenticationWhenDeviceIsNotRegisteredAndGetsTheRegisterScreenAndTheUserTapsOnRegister() { + enum TestData { + static let challengeToken = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES0lucHV0IjogImV5SmphR0ZzYkdWdVoyVWlPaUpqYUdGc2JHVnVaMlVpZlEiLCAiYWNzUmVmZXJlbmNlTnVtYmVyIjoiQURZRU4tQUNTLVNJTVVMQVRPUiIsImFjc1NpZ25lZENvbnRlbnQiOiJleUpoYkdjaU9pSlFVekkxTmlJc0luZzFZeUk2V3lKTlNVbEVNMFJEUTBGelVVTkRVVVJVU205VFZHeFlXQzlQVkVGT1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVEwSjFha1ZNVFVGclIwRXhWVVZDYUUxRFZHdDNlRVpxUVZWQ1owNVdRa0ZuVFVSVk5YWmlNMHByVEZWb2RtSkhlR2hpYlZGNFJXcEJVVUpuVGxaQ1FXTk5RMVZHZEdNelVteGpiVkpvWWxSRlZFMUNSVWRCTVZWRlEyZDNTMUZYVWpWYVZ6Um5WR2sxVjB4cVJWSk5RVGhIUVRGVlJVTjNkMGxSTW1oc1dUSjBkbVJZVVhoT1ZFRjZRbWRPVmtKQlRVMU1SRTVGVlhwSloxVXliSFJrVjNob1pFYzVlVWxHV2twVk1FVm5Va1pOWjFFeVZubGtSMnh0WVZkT2FHUkhWV2RSV0ZZd1lVYzVlV0ZZVWpWTlUwRjNTR2RaU2t0dldrbG9kbU5PUVZGclFrWm9SbnBrV0VKM1lqTktNRkZIUm10bFYxWjFURzFPZG1KVVFXVkdkekI0VDBSQk5FMXFZM2hOZWxGNlRWUlNZVVozTUhsUFJFRTBUV3BSZUUxNlVYcE5WRkpoVFVsSGEwMVJjM2REVVZsRVZsRlJSMFYzU2s5VVJFVlhUVUpSUjBFeFZVVkRRWGRPVkcwNWRtTnRVWFJUUnpsellrZEdkVnBFUlZOTlFrRkhRVEZWUlVKM2QwcFJWekY2WkVkV2VWcEhSblJOVWsxM1JWRlpSRlpSVVV0RVFYQkNXa2hzYkdKcFFrOU1iRmwxVFZKRmQwUjNXVVJXVVZGTVJFRm9SR0ZIVm1waE1qa3haRVJGWmsxQ01FZEJNVlZGUVhkM1YwMHdVbFJOYVVKVVlWY3hNV0pIUmpCaU0wbG5WbXRzVkZGVFFrVlZla1ZuVFVJMFIwTlRjVWRUU1dJelJGRkZTa0ZTV1ZKak0xWjNZMGM1ZVdSRlFtaGFTR3hzWW1rMWFtSXlNSGRuWjBWcFRVRXdSME5UY1VkVFNXSXpSRkZGUWtGUlZVRkJORWxDUkhkQmQyZG5SVXRCYjBsQ1FWRkRZeTlYZDA0MlpuWXhZVWwxYTNwTmFGZGhVbVJhWjBRNVVHdDFOV0Y1VFdWaGJXeE9SelIwVld3eVV6WTNURXRFT1VKU2VXTm9RWFp2TlVFclJXdG1Na2hMVWpKWVZHVnhUMlpIWm05TVJFbHhNa3hYUnpsSGEwdHlSeTlMUW5RdlFWQkVNWGhDYUdkdFNHNXJORmxxY0VGV2JsQTBabUZLVEhSU2NXRlFSVVZPVHpnd2JXTjZXV3hoZHpoWmFuUlJObmxJV0ZCTk5FOVBMMlo2TjJZMU9GRmxjRWhoZFUxYWNIcDZlazByUkc5dFZEQk1NVWhDYkZoVWVGcG5kVlpETUM5MVpVUk5ZMU5SVFdZNFQzSldZa3hVZHpOa1FubEVPRmQ1TlhkNFFXZFJkbFl2UkdaRWVWRllXamRMUTFabVpqbDFaVkZZYmtSdVQzbEdNRTVoTDBKSlZXMXFlbWgyU205R1kxZzJVeTg0V1V3MmRtSnBNMjVrWVhsTFdXdHVkRFZ2Y1RKb1ZrZG1hRm9yYURVM2VWbzNabmxXWmtJd2MyRlFhRkZyTTBjMlNqQlBLMHBzTWxWWE9GRjJRVFl3VmpoNk5HWkJaMDFDUVVGRmQwUlJXVXBMYjFwSmFIWmpUa0ZSUlV4Q1VVRkVaMmRGUWtGSFVVeGpVRzlRTkRoQllVTnlVV0p4ZWl0MlJsQTBNbWx5YjJKR1VHWnhjRlZyWkZZMlFVeE5lRXBDWTJOMlNEbENibHBuVmxKNkwzRk9UbE0yUlVnMmJIWnZabGt3YkVoVGRVdGthMEo0TDFCV09FcE9jR1JvYldNdllVTkZTM2RtY1dsMFZuWndNemxFUnpsTlVrcFZNWG8zYlhRdlVsSklTbWxWUkRGR01WRlJlR0pUT0dSTWJXOTBTR1pOU1dsVmR5OXpVWEpXWm1WRU5sQk1VRGxxUTNrdlZXbDFkMlZIWTNOaWFGRXpiekJJUVdjeGRrbDJUVUpXU0RKaWNDdG1iREJpUVhFNVRHczBXWGhCTUVSdlMyTklZbGhtUTBWS2J6ZFBMM1JMV1d4YVoxcHJOVk5rUzFGbGNFTmhWRkV6Tmk5V2IxUnliSHBKTUU5d1ZVY3hkSEpGT1dWVk1TdEpOa3hxTVU5bFpYbGtaalU1Wm5aWVFXUm9MMmhrWkdoTFZEUm5TbkoyZGtaaU1IYzBlbHBwV0hWNlZscFFTMUF3ZG5sclFXNDVLMGsyUVZJeVVrczRZVUUwTkc5S1FVd3pkMGd4U1VFOUlpd2lUVWxKUkRocVEwTkJkRzlEUTFGRVRtNVllV05XUlVsM2RYcEJUa0puYTNGb2EybEhPWGN3UWtGUmMwWkJSRU5DZFdwRlRFMUJhMGRCTVZWRlFtaE5RMVJyZDNoR2FrRlZRbWRPVmtKQlowMUVWVFYyWWpOS2EweFZhSFppUjNob1ltMVJlRVZxUVZGQ1owNVdRa0ZqVFVOVlJuUmpNMUpzWTIxU2FHSlVSVlJOUWtWSFFURlZSVU5uZDB0UlYxSTFXbGMwWjFScE5WZE1ha1ZTVFVFNFIwRXhWVVZEZDNkSlVUSm9iRmt5ZEhaa1dGRjRUbFJCZWtKblRsWkNRVTFOVEVST1JWVjZTV2RWTW14MFpGZDRhR1JIT1hsSlJscEtWVEJGWjFKR1RXZFJNbFo1WkVkc2JXRlhUbWhrUjFWblVWaFdNR0ZIT1hsaFdGSTFUVk5CZDBobldVcExiMXBKYUhaalRrRlJhMEpHYUVaNlpGaENkMkl6U2pCUlIwWnJaVmRXZFV4dFRuWmlWRUZsUm5jd2VFOUVRVFJOYW1ONFRYcFJkMDVVYUdGR2R6QjVUMFJCTkUxcVVYaE5lbEYzVGxSb1lVMUpSelpOVVhOM1ExRlpSRlpSVVVkRmQwcFBWRVJGVjAxQ1VVZEJNVlZGUTBGM1RsUnRPWFpqYlZGMFUwYzVjMkpIUm5WYVJFVlRUVUpCUjBFeFZVVkNkM2RLVVZjeGVtUkhWbmxhUjBaMFRWSk5kMFZSV1VSV1VWRkxSRUZ3UWxwSWJHeGlhVUpQVEd4WmRVMVNSWGRFZDFsRVZsRlJURVJCYUVSaFIxWnFZVEk1TVdSRVJURk5SRTFIUVRGVlJVRjNkM05OTUZKVVRXbENWR0ZYTVRGaVIwWXdZak5KWjFacmJGUlJVMEpGVlhsQ1JGcFlTakJoVjFwd1dUSkdNRnBUUWtKa1dGSnZZak5LY0dSSWEzaEpSRUZsUW1kcmNXaHJhVWM1ZHpCQ1ExRkZWMFZZVGpGalNFSjJZMjVTUVZsWFVqVmFWelIxV1RJNWRFMUpTVUpKYWtGT1FtZHJjV2hyYVVjNWR6QkNRVkZGUmtGQlQwTkJVVGhCVFVsSlFrTm5TME5CVVVWQmRYQTNLMlowZDFkblIyUmpZVFl4Y1ZaQ1l6RkNkbFJwTlZrME0wSjNhRm96VTJoS1NXdHRSMGwzWjFsUWMwbzVjSEpQWTFwVlZtVkhhMFZvWXpWSFdIY3ZPVkpNWTJ4WmJXbHBNbG92VEZCRmVYazJWVWxqVUhORlJtbGtVbnBYVERaa09EUmlaR0k0VkRWcE5rbEJUSE5JVTJkUFptTlFUekpFUTFsdlRqVkdLMGd2ZGxWaGNIZFpSMnBDTkZrcmFYZE5abEV5WlhOTU0xRkVaRVVyTDI4NUwxbzBUbkJtYnprclkyWXhSSHBsY0ZOWFZuaFVlRkpTYTFOWU1VY3JVWFpyUWswdmNHeDBOVzAxZUN0TVZWa3dlalpWTkN0MVVYRkNVVmx6YVRCVlVEVk5iV0k0UlRaVmQwY3lhMk00Tm5SelpYYzBXVXh4VTJOWWRGVTVaeTl2T1Rsbk9VVnJia1ZUV205Q09GRnRhbWRKTUhOYVVYSkZNMHR2TkVFeUwxbERaVEkxU21kYU56RkxZemRGTHpsSVMxRkxNMVl4ZEdsTWNucDRhVk5MTUhFNFlrVk9NMnBoWWpSelVVcFhZM3BTTlU1UlNVUkJVVUZDVFVFd1IwTlRjVWRUU1dJelJGRkZRa04zVlVGQk5FbENRVkZCYlRoeFQwUkJUa0l6VUhBck9UaFJablZSVlVWVVdHVXhUbEp3U25aRWIyTjVjMlJ2U2l0emREaEhXbVJpVjJsdWEwOXdOMlpzV1RSWWNFWnlNbHBKY1U1SVRYbEtjMlkzT1VsQlMyeENaVzlUV0RkNFZHWnFaM0l5T0hkbmExTjVkRFZRVjJJM1drWXpXRlF3Ym1kWGMzaHlNbVIwUmpkU2RVRjRVVGhLWWxSd1VFeGtSVmt4V2pKeWFFbzFZWFJLZERkRlNsbEZOa0ZZU25wQmNqVlZTamQ1YlRCaldTczVUazB6VmtKcVUzQmpPV1ZNVDA0elZHdFpXRzlWZGpKa2JVdDFNVWg2VEhaaU1XMUVNR1ZJZVhWRmNsRlBjbUpVS3pGdlJrMWxMMHRvZW5ZeE4weHJXRGhxTjA5NFUwdHRVaTlJTDFReWVYRm5iWHBQZUdkTk1HeExlbXN6VjJsUlQyNHhhMVJYWVc5WU9FTm9VRFpwVTIxS2EzSjNTVlY1V2l0V01WVkpVRU5VYm5Sc1VYcEZVVXBJT1RaUk5XNVpUbFJNVGpocVZteHdOVzF1UzBkMFVrRlljbXgxY25oTWFUbFpOa1VpWFgwLmV5SmhZM05GY0dobGJWQjFZa3RsZVNJNmV5SmpjbllpT2lKUUxUSTFOaUlzSW10MGVTSTZJa1ZESWl3aWVDSTZJbWRKTUVkQlNVeENaSFUzVkRVellXdHlSbTFOZVVkamMwWXpialZrVHpkTmJYZE9Ra2hMVnpWVFZqQWlMQ0o1SWpvaVUweFhYM2hUWm1aNmJGQlhja2hGVmtrek1FUklUVjgwWldkV2QzUXpUbEZ4WlZWRU4yNU5SbkJ3Y3lKOUxDSnpaR3RGY0dobGJWQjFZa3RsZVNJNmV5SmpjbllpT2lKUUxUSTFOaUlzSW10MGVTSTZJa1ZESWl3aWVDSTZJa1ozUldGc1prOTZRV1l0YmpaRU5GRjJSRnBHZG1oU2FEVm5ORkJIWm00NVYzcEVWelZSTkZNeU1VVWlMQ0o1SWpvaVJrYzBiMDFXTjJ0dWNUSllSVEZHVFRsUE0zb3hTVXBPTWxNeE9FSk9RVTB0ZGt0SFRXeDNjbU5UV1NKOUxDSmhZM05WVWt3aU9pSm9kSFJ3Y3pwY0wxd3ZjR0ZzTFhSbGMzUXVZV1I1Wlc0dVkyOXRYQzkwYUhKbFpXUnpNbk5wYlhWc1lYUnZjbHd2YzJWeWRtbGpaWE5jTDFSb2NtVmxSRk15VTJsdGRXeGhkRzl5WEM5Mk1Wd3ZhR0Z1Wkd4bFhDOHlOR1ZsTnpJME1pMHhaRGcxTFRRNFpHTXRPREV6WWkwM01EaGhOVFl3Wm1WaVpUVWlmUS5UM0pSUUg4UlkwNzYta1BCRGl1LU9lRlJLcEtyX0tfX3RCdUxscnZSeWlsc3JxMHA2dzVMcGM2STVXMHE1V1Awbk5hUmE2VFdYMWZVc1g2Rldhbk5LYzJXczRWYk0zejg5M3BjRFNSZVZYWVp3eWs5WnZzaWhRNzAzQjJoTzNQbXZPM09QT0VTWi1xRGFJckRLRkVHUEdSQnVwQlhUVmVRaFNkdUlPOWpUekxEZW5NZGdEMlFDNU9BR3VTTEVKN0o4VnFiM0htV0k4bGZJLWNQQ3YxSkpEY3YxMWJ2ZEZOVC1WNzVIT0xGNjN2WGY3UkxhZTVLbFQwalJtMW93NDFTMG9Td3lrN1BjeTBvN3A0S0o2LWxGaGRvc2ZEVGJQWXp5VkprSHdfR0J2YzhNNWU2QV8zcUdtbWJtYjlvaUJkWC1taEtJc0RrVkI4bW5CbkdKdzRRY0EiLCJhY3NUcmFuc0lEIjoiMjRlZTcyNDItMWQ4NS00OGRjLTgxM2ItNzA4YTU2MGZlYmU1IiwiYWNzVVJMIjoiaHR0cHM6XC9cL3BhbC10ZXN0LmFkeWVuLmNvbVwvdGhyZWVkczJzaW11bGF0b3JcL3NlcnZpY2VzXC9UaHJlZURTMlNpbXVsYXRvclwvdjFcL2hhbmRsZVwvMjRlZTcyNDItMWQ4NS00OGRjLTgxM2ItNzA4YTU2MGZlYmU1IiwibWVzc2FnZVZlcnNpb24iOiIyLjEuMCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiN2IyNjBkNzMtNzE2NC00MWNkLWE3MGMtOGFhOGQxYTFjOWEyIn0=" + } + + let redirectComponent = AnyRedirectComponentMock() + redirectComponent.onHandle = { action in + XCTFail("RedirectComponent should never be invoked.") + } + + // A mock for the Authentication SDK + let authenticationServiceMock = AuthenticationServiceMock() + authenticationServiceMock.isDeviceRegistered = false + authenticationServiceMock.onAuthenticate = { _ in + XCTFail("On Authentication should not be called in a register flow") + return "onAuthenticate-Return" + } + let onRegisterExpectation = expectation(description: "On Authentication - should be called in the AuthneticationSDK") + authenticationServiceMock.onRegister = { _ in + onRegisterExpectation.fulfill() + return "onRegister-Return" + } + + let delegate = ActionComponentDelegateMock() + + // A mock for the one which will present the screens if needed. + let presentationDelegateMock = PresentationDelegateMock() + + // A mock for the 3ds2 sdk, which would successfully complete a challenge. + let mockService = AnyADYServiceMock() + let authenticationRequestParameters = AuthenticationRequestParametersMock(deviceInformation: "device_info", + sdkApplicationIdentifier: "sdkApplicationIdentifier", + sdkTransactionIdentifier: "sdkTransactionIdentifier", + sdkReferenceNumber: "sdkReferenceNumber", + sdkEphemeralPublicKey: "{\"y\":\"zv0kz1SKfNvT3ql75L217de6ZszxfLA8LUKOIKe5Zf4\",\"x\":\"3b3mPfWhuOxwOWydLejS3DJEUPiMVFxtzGCV6906rfc\",\"kty\":\"EC\",\"crv\":\"P-256\"}", + messageVersion: "messageVersion") + mockService.authenticationRequestParameters = authenticationRequestParameters + + let threeDS2ActionHandler = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: mockService, + presenter: ThreeDS2PlusDAScreenPresenter(presentationDelegate: presentationDelegateMock, + style: .init(), + localizedParameters: nil), + delegatedAuthenticationService: authenticationServiceMock, + deviceSupportCheckerService: DeviceSupportCheckerMock(isDeviceSupported: true)) + threeDS2ActionHandler.delegatedAuthenticationState.isDeviceRegistrationFlow = true + let classicActionHandler = ThreeDS2ClassicActionHandler.init(context: Dummy.context, service: mockService, coreActionHandler: threeDS2ActionHandler) + + let mockedTransaction = AnyADYTransactionMock(parameters: authenticationRequestParameters) + classicActionHandler.transaction = mockedTransaction + mockedTransaction.onPerformChallenge = { params, completion in + completion(AnyChallengeResultMock(sdkTransactionIdentifier: "sdkTxId", transactionStatus: "Y"), nil) + } + let sut = ThreeDS2Component(context: Dummy.context, + threeDS2CompactFlowHandler: AnyThreeDS2ActionHandlerMock(), + threeDS2ClassicFlowHandler: classicActionHandler, + redirectComponent: redirectComponent) + sut.presentationDelegate = presentationDelegateMock + // Verify if we get a challengeResult. + let delegateExpectation = expectation(description: "Expect delegate didProvide(_:from:) function to be called.") + delegate.onDidProvide = { data, component in + XCTAssertTrue(component === sut) + XCTAssertEqual(data.paymentData, "paymentData") + + let threeDS2Details = data.details as! ThreeDS2Details + + switch threeDS2Details { + case let .challengeResult(result): + // Check if the result has transStatus Y, and delegatedAuthenticationSDKOutput":"onRegister-Return" + XCTAssertEqual(result.payload, "eyJhdXRob3Jpc2F0aW9uVG9rZW4iOiJhdXRoVG9rZW4iLCJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES091dHB1dCI6Im9uUmVnaXN0ZXItUmV0dXJuIiwidHJhbnNTdGF0dXMiOiJZIn0=") + default: + XCTFail() + } + + delegateExpectation.fulfill() + } + + // Verify if the UI is displayed & simulate the tap of the first button which is approve. + let presentationExpectation = expectation(description: "Approval view controller should be shown.") + presentationDelegateMock.doPresent = { component in + let registrationViewController = component.viewController as? DARegistrationViewController + self.verifyRegistrationView(viewController: registrationViewController) + XCTAssertNotNil(registrationViewController) + registrationViewController?.firstButtonTapped() + presentationExpectation.fulfill() + } + + sut.delegate = delegate + + // execute a challenge - as the registration flow is triggered only during a challenge flow. + sut.handle(ThreeDS2ChallengeAction(challengeToken: TestData.challengeToken, authorisationToken: "authToken", paymentData: "paymentData")) + + waitForExpectations(timeout: 3, handler: nil) + } + + func verifyApprovalView(viewController: DAApprovalViewController?) { + guard let viewController else { XCTFail("No DARegistrationViewController passed"); return } + let image: UIImageView? = viewController.view.findView(by: "image") + XCTAssertNotNil(image) + let titleLabel: UILabel? = viewController.view.findView(by: "titleLabel") + XCTAssertNotNil(titleLabel) + XCTAssertEqual(titleLabel?.text, "Approve transaction") + + let descriptionLabel: UILabel? = viewController.view.findView(by: "descriptionLabel") + XCTAssertNotNil(descriptionLabel) + XCTAssertEqual(descriptionLabel?.text, "To make sure it’s you, approve this transaction with your biometrics to complete your purchase.") + + let progressView: UIProgressView? = viewController.view.findView(by: "progressView") + XCTAssertNotNil(progressView) + let progressText: UILabel? = viewController.view.findView(by: "progressText") + XCTAssertNotNil(progressText) + let firstButton: SubmitButton? = viewController.view.findView(by: "primaryButton") + XCTAssertNotNil(firstButton) + XCTAssertEqual(firstButton?.title, "Use biometrics") + + let secondButton: SubmitButton? = viewController.view.findView(by: "secondaryButton") + XCTAssertNotNil(secondButton) + XCTAssertEqual(secondButton?.title, "Approve differently") + + let textView: UITextView? = viewController.view.findView(by: "textView") + XCTAssertEqual(textView?.text, "Opt out any time by removing your credentials.") + } + + func verifyRegistrationView(viewController: DARegistrationViewController?) { + guard let viewController else { XCTFail("No DAApprovalViewController passed"); return } + let image: UIImageView? = viewController.view.findView(by: "image") + XCTAssertNotNil(image) + let titleLabel: UILabel? = viewController.view.findView(by: "titleLabel") + XCTAssertNotNil(titleLabel) + XCTAssertEqual(titleLabel?.text, "Safe and swift checkout!") + + let descriptionLabel: UILabel? = viewController.view.findView(by: "descriptionLabel") + XCTAssertNotNil(descriptionLabel) + XCTAssertEqual(descriptionLabel?.text, "You can check out faster next time on this device using your biometrics.") + + let progressView: UIProgressView? = viewController.view.findView(by: "progressView") + XCTAssertNotNil(progressView) + let progressText: UILabel? = viewController.view.findView(by: "progressText") + XCTAssertNotNil(progressText) + + let firstButton: SubmitButton? = viewController.view.findView(by: "primaryButton") + XCTAssertNotNil(firstButton) + XCTAssertEqual(firstButton?.title, "Enable swift checkout") + let secondButton: SubmitButton? = viewController.view.findView(by: "secondaryButton") + XCTAssertNotNil(secondButton) + XCTAssertEqual(secondButton?.title, "Not now") + let textView: UITextView? = viewController.view.findView(by: "textView") + XCTAssertEqual(textView?.text, "") + } + #endif } diff --git a/Tests/Card Tests/3DS2 Component/ThreeDS2DAScreenPresenterMock.swift b/Tests/Card Tests/3DS2 Component/ThreeDS2DAScreenPresenterMock.swift new file mode 100644 index 0000000000..9066d5b784 --- /dev/null +++ b/Tests/Card Tests/3DS2 Component/ThreeDS2DAScreenPresenterMock.swift @@ -0,0 +1,68 @@ +// +// Copyright (c) 2022 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +import Foundation + +#if canImport(AdyenAuthentication) + @_spi(AdyenInternal) @testable import Adyen + import Adyen3DS2 + import AdyenAuthentication + import Foundation + import UIKit + +final class ThreeDS2DAScreenPresenterMock: ThreeDS2PlusDAScreenPresenterProtocol { + + enum ShowRegistrationScreenMockState { + case register + case fallback + } + + let showRegistrationReturnState: ShowRegistrationScreenMockState + func showRegistrationScreen(component: Adyen.Component, + registerDelegatedAuthenticationHandler: @escaping () -> Void, + fallbackHandler: @escaping () -> Void) { + switch showRegistrationReturnState { + case .register: + registerDelegatedAuthenticationHandler() + case .fallback: + fallbackHandler() + } + } + + enum ShowApprovalScreenMockState { + case approve + case fallback + case removeCredentials + } + + let showApprovalScreenReturnState: ShowApprovalScreenMockState + + func showApprovalScreen(component: Adyen.Component, + approveAuthenticationHandler: @escaping () -> Void, + fallbackHandler: @escaping () -> Void, + removeCredentialsHandler: @escaping () -> Void) { + switch showApprovalScreenReturnState { + case .approve: + approveAuthenticationHandler() + case .fallback: + fallbackHandler() + case .removeCredentials: + removeCredentialsHandler() + } + } + + var userInput: ThreeDS2PlusDAScreenUserInput = .noInput + + init(showRegistrationReturnState: ShowRegistrationScreenMockState, + showApprovalScreenReturnState: ShowApprovalScreenMockState, + userInput: ThreeDS2PlusDAScreenUserInput = .noInput) { + self.showRegistrationReturnState = showRegistrationReturnState + self.showApprovalScreenReturnState = showApprovalScreenReturnState + self.userInput = userInput + } +} + +#endif diff --git a/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests+Constants.swift b/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests+Constants.swift index 2fc6218506..5c294f642f 100644 --- a/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests+Constants.swift +++ b/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests+Constants.swift @@ -6,6 +6,7 @@ import Foundation +@available(iOS 14.0, *) extension ThreeDS2PlusDACoreActionHandlerTests { var sdkPublicKey: String { diff --git a/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests.swift b/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests.swift index 9f6ffe52fc..0c7a57ba3c 100644 --- a/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests.swift +++ b/Tests/Card Tests/3DS2 Component/ThreeDS2PlusDACoreActionHandlerTests.swift @@ -16,8 +16,8 @@ import XCTest import Foundation import UIKit - class ThreeDS2PlusDACoreActionHandlerTests: XCTestCase { - +@available(iOS 14.0, *) + final class ThreeDS2PlusDACoreActionHandlerTests: XCTestCase { var authenticationRequestParameters: AnyAuthenticationRequestParameters! var fingerprintAction: ThreeDS2FingerprintAction! @@ -55,27 +55,17 @@ import XCTest } func testSettingThreeDSRequestorAppURL() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } - let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, appearanceConfiguration: ADYAppearanceConfiguration(), - delegatedAuthenticationConfiguration: Self.delegatedAuthenticationConfigurations) + delegatedAuthenticationConfiguration: Self.delegatedAuthenticationConfigurations, presentationDelegate: nil) sut.threeDSRequestorAppURL = URL(string: "https://google.com") XCTAssertEqual(sut.threeDSRequestorAppURL, URL(string: "https://google.com")) } func testWrappedComponent() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } - let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, appearanceConfiguration: ADYAppearanceConfiguration(), - delegatedAuthenticationConfiguration: Self.delegatedAuthenticationConfigurations) + delegatedAuthenticationConfiguration: Self.delegatedAuthenticationConfigurations, presentationDelegate: nil) XCTAssertEqual(sut.context.apiContext.clientKey, Dummy.apiContext.clientKey) XCTAssertEqual(sut.context.apiContext.environment.baseURL, Dummy.apiContext.environment.baseURL) @@ -88,17 +78,13 @@ import XCTest } func testFingerprintFlowSuccess() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } - let service = AnyADYServiceMock() service.authenticationRequestParameters = authenticationRequestParameters let expectedFingerprint = try ThreeDS2Component.Fingerprint( authenticationRequestParameters: authenticationRequestParameters, - delegatedAuthenticationSDKOutput: nil + delegatedAuthenticationSDKOutput: nil, + deleteDelegatedAuthenticationCredential: nil ) let authenticationServiceMock = AuthenticationServiceMock() @@ -106,6 +92,7 @@ import XCTest let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, showApprovalScreenReturnState: .fallback), delegatedAuthenticationService: authenticationServiceMock) sut.handle(fingerprintAction, event: analyticsEvent) { fingerprintResult in switch fingerprintResult { @@ -118,15 +105,10 @@ import XCTest resultExpectation.fulfill() } - waitForExpectations(timeout: 2, handler: nil) + waitForExpectations(timeout: 20, handler: nil) } func testInvalidFingerprintToken() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } - let service = AnyADYServiceMock() service.authenticationRequestParameters = authenticationRequestParameters @@ -139,6 +121,7 @@ import XCTest let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, showApprovalScreenReturnState: .fallback), delegatedAuthenticationService: authenticationServiceMock) sut.handle(fingerprintAction, event: analyticsEvent) { result in @@ -160,10 +143,6 @@ import XCTest } func testChallengeFlowSuccess() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } let service = AnyADYServiceMock() service.authenticationRequestParameters = authenticationRequestParameters @@ -181,7 +160,7 @@ import XCTest self.expectedSDKRegistrationOutput } - let expectedResult = try ThreeDSResult( + let expectedResult = try! ThreeDSResult( from: AnyChallengeResultMock( sdkTransactionIdentifier: "sdkTransactionIdentifier", transactionStatus: "Y" @@ -190,52 +169,20 @@ import XCTest authorizationToken: "authToken", threeDS2SDKError: nil ) - + let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, service: service, - delegatedAuthenticationService: authenticationServiceMock) + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .register, showApprovalScreenReturnState: .fallback), + delegatedAuthenticationService: authenticationServiceMock, + deviceSupportCheckerService: DeviceSupportCheckerMock(isDeviceSupported: true)) sut.threeDSRequestorAppURL = URL(string: "https://google.com") sut.transaction = transaction sut.delegatedAuthenticationState.isDeviceRegistrationFlow = true sut.handle(challengeAction, event: analyticsEvent) { challengeResult in switch challengeResult { case let .success(result): - do { - let json = try XCTUnwrap(JSONSerialization.jsonObject( - with: result.payload.dataFromBase64URL(), - options: [] - ) as? [String: Any] - ) - - let expectedJson = try XCTUnwrap(JSONSerialization.jsonObject( - with: expectedResult.payload.dataFromBase64URL(), - options: [] - ) as? [String: Any] - ) - - XCTAssertEqual(json["transStatus"] as? String, expectedJson["transStatus"] as? String) - XCTAssertEqual(json["authorisationToken"] as? String, expectedJson["authorisationToken"] as? String) - - let delegatedAuthenticationSDKOutput = try XCTUnwrap(JSONSerialization.jsonObject( - with: XCTUnwrap(json["delegatedAuthenticationSDKOutput"] as? String).dataFromBase64URL(), - options: [] - ) as? [String: Any] - ) - - let expectedDelegatedAuthenticationSDKOutput = try XCTUnwrap(JSONSerialization.jsonObject( - with: XCTUnwrap(expectedJson["delegatedAuthenticationSDKOutput"] as? String).dataFromBase64URL(), - options: [] - ) as? [String: Any] - ) - - XCTAssertEqual( - NSDictionary(dictionary: delegatedAuthenticationSDKOutput), - NSDictionary(dictionary: expectedDelegatedAuthenticationSDKOutput) - ) - } catch { - XCTFail(error.localizedDescription) - } + XCTAssertEqual(result, expectedResult) case .failure: XCTFail() } @@ -246,11 +193,6 @@ import XCTest } func testChallengeFlowFailure() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } - let service = AnyADYServiceMock() service.authenticationRequestParameters = authenticationRequestParameters let mockedTransaction = AnyADYTransactionMock(parameters: authenticationRequestParameters) @@ -263,7 +205,7 @@ import XCTest let authenticationServiceMock = AuthenticationServiceMock() let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, - service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, showApprovalScreenReturnState: .fallback), delegatedAuthenticationService: authenticationServiceMock) sut.transaction = mockedTransaction @@ -289,17 +231,11 @@ import XCTest } func testChallengeFlowMissingTransaction() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } - let service = AnyADYServiceMock() let authenticationServiceMock = AuthenticationServiceMock() - let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, - service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, showApprovalScreenReturnState: .fallback), delegatedAuthenticationService: authenticationServiceMock) let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") @@ -322,11 +258,6 @@ import XCTest } func testInvalidChallengeToken() throws { - guard #available(iOS 14.0, *) else { - // XCTestCase does not respect @available so we have skip all tests here - throw XCTSkip("Unsupported iOS version") - } - let service = AnyADYServiceMock() service.authenticationRequestParameters = authenticationRequestParameters let mockedTransaction = AnyADYTransactionMock(parameters: authenticationRequestParameters) @@ -339,7 +270,7 @@ import XCTest let authenticationServiceMock = AuthenticationServiceMock() let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, - service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, showApprovalScreenReturnState: .fallback), delegatedAuthenticationService: authenticationServiceMock) sut.transaction = mockedTransaction @@ -363,7 +294,284 @@ import XCTest waitForExpectations(timeout: 2, handler: nil) } + + // MARK: - Delegated Authentication tests + // The approval flow of delegated authentication + func testDelegatedAuthenticationApprovalFlowWhenUserApproves() throws { + + // The token and result are base 64 encoded. + enum TestData { + static let fingerprintToken = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES0lucHV0IjoiIyNTb21lZGVsZWdhdGVkQXV0aGVudGljYXRpb25TREtJbnB1dCMjIiwiZGlyZWN0b3J5U2VydmVySWQiOiJGMDEzMzcxMzM3IiwiZGlyZWN0b3J5U2VydmVyUHVibGljS2V5IjoiI0RpcmVjdG9yeVNlcnZlclB1YmxpY0tleSMiLCJkaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIjoiIyNEaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIyMiLCJ0aHJlZURTTWVzc2FnZVZlcnNpb24iOiIyLjIuMCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiMTUwZmEzYjgtZTZjOC00N2ExLTk2ZTAtOTEwNzYzYmVlYzU3In0=" + + static let expectedFingerprintResult = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES091dHB1dCI6Ik9uQXV0aGVudGljYXRlIiwic2RrQXBwSUQiOiJzZGtBcHBsaWNhdGlvbklkZW50aWZpZXIiLCJzZGtFbmNEYXRhIjoiZGV2aWNlX2luZm8iLCJzZGtFcGhlbVB1YktleSI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IjNiM21QZldodU94d09XeWRMZWpTM0RKRVVQaU1WRnh0ekdDVjY5MDZyZmMiLCJ5IjoienYwa3oxU0tmTnZUM3FsNzVMMjE3ZGU2WnN6eGZMQThMVUtPSUtlNVpmNCJ9LCJzZGtSZWZlcmVuY2VOdW1iZXIiOiJzZGtSZWZlcmVuY2VOdW1iZXIiLCJzZGtUcmFuc0lEIjoic2RrVHJhbnNhY3Rpb25JZGVudGlmaWVyIn0=" + } + let service = AnyADYServiceMock() + service.authenticationRequestParameters = authenticationRequestParameters + + let authenticationServiceMock = AuthenticationServiceMock() + authenticationServiceMock.onAuthenticate = { input in + return "OnAuthenticate" + } + let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") + let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, + showApprovalScreenReturnState: .approve), + delegatedAuthenticationService: authenticationServiceMock) + + let fingerprintAction = ThreeDS2FingerprintAction(fingerprintToken: TestData.fingerprintToken, + authorisationToken: "AuthToken", + paymentData: "paymentData") + sut.handle(fingerprintAction, event: analyticsEvent) { fingerprintResult in + switch fingerprintResult { + case let .success(fingerprintString): + XCTAssertEqual(fingerprintString, TestData.expectedFingerprintResult) + case .failure: + XCTFail() + } + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 20, handler: nil) + } + + func testDelegatedAuthenticationApprovalFlowWhenUserApprovesButVerificationFails() throws { + // The token and result are base 64 encoded. + enum TestData { + static let fingerprintToken = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES0lucHV0IjoiIyNTb21lZGVsZWdhdGVkQXV0aGVudGljYXRpb25TREtJbnB1dCMjIiwiZGlyZWN0b3J5U2VydmVySWQiOiJGMDEzMzcxMzM3IiwiZGlyZWN0b3J5U2VydmVyUHVibGljS2V5IjoiI0RpcmVjdG9yeVNlcnZlclB1YmxpY0tleSMiLCJkaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIjoiIyNEaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIyMiLCJ0aHJlZURTTWVzc2FnZVZlcnNpb24iOiIyLjIuMCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiMTUwZmEzYjgtZTZjOC00N2ExLTk2ZTAtOTEwNzYzYmVlYzU3In0=" + // Result without delegatedAuthenticationSDKOutput + static let expectedFingerprintResult = "eyJzZGtBcHBJRCI6InNka0FwcGxpY2F0aW9uSWRlbnRpZmllciIsInNka0VuY0RhdGEiOiJkZXZpY2VfaW5mbyIsInNka0VwaGVtUHViS2V5Ijp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiM2IzbVBmV2h1T3h3T1d5ZExlalMzREpFVVBpTVZGeHR6R0NWNjkwNnJmYyIsInkiOiJ6djBrejFTS2ZOdlQzcWw3NUwyMTdkZTZac3p4ZkxBOExVS09JS2U1WmY0In0sInNka1JlZmVyZW5jZU51bWJlciI6InNka1JlZmVyZW5jZU51bWJlciIsInNka1RyYW5zSUQiOiJzZGtUcmFuc2FjdGlvbklkZW50aWZpZXIifQ==" + } + + let service = AnyADYServiceMock() + service.authenticationRequestParameters = authenticationRequestParameters + + let authenticationServiceMock = AuthenticationServiceMock() + authenticationServiceMock.onAuthenticate = { input in + throw NSError(domain: "Error during Authentication", code: 123) + } + let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") + let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, + showApprovalScreenReturnState: .approve), + delegatedAuthenticationService: authenticationServiceMock) + + let fingerprintAction = ThreeDS2FingerprintAction(fingerprintToken: TestData.fingerprintToken, + authorisationToken: "AuthToken", + paymentData: "paymentData") + sut.handle(fingerprintAction, event: analyticsEvent) { fingerprintResult in + switch fingerprintResult { + case let .success(fingerprintString): + XCTAssertEqual(fingerprintString, TestData.expectedFingerprintResult) + case .failure: + XCTFail() + } + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 20, handler: nil) + } + + + func testDelegatedAuthenticationApprovalFlowWhenUserDoesntConsentToApprove() throws { + // The token and result are base 64 encoded. + enum TestData { + // Token with delegatedAuthenticationSDKInput + static let fingerprintToken = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES0lucHV0IjoiIyNTb21lZGVsZWdhdGVkQXV0aGVudGljYXRpb25TREtJbnB1dCMjIiwiZGlyZWN0b3J5U2VydmVySWQiOiJGMDEzMzcxMzM3IiwiZGlyZWN0b3J5U2VydmVyUHVibGljS2V5IjoiI0RpcmVjdG9yeVNlcnZlclB1YmxpY0tleSMiLCJkaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIjoiIyNEaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIyMiLCJ0aHJlZURTTWVzc2FnZVZlcnNpb24iOiIyLjIuMCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiMTUwZmEzYjgtZTZjOC00N2ExLTk2ZTAtOTEwNzYzYmVlYzU3In0=" + + // Result without delegatedAuthenticationSDKOutput + static let expectedFingerprintResult = "eyJzZGtBcHBJRCI6InNka0FwcGxpY2F0aW9uSWRlbnRpZmllciIsInNka0VuY0RhdGEiOiJkZXZpY2VfaW5mbyIsInNka0VwaGVtUHViS2V5Ijp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiM2IzbVBmV2h1T3h3T1d5ZExlalMzREpFVVBpTVZGeHR6R0NWNjkwNnJmYyIsInkiOiJ6djBrejFTS2ZOdlQzcWw3NUwyMTdkZTZac3p4ZkxBOExVS09JS2U1WmY0In0sInNka1JlZmVyZW5jZU51bWJlciI6InNka1JlZmVyZW5jZU51bWJlciIsInNka1RyYW5zSUQiOiJzZGtUcmFuc2FjdGlvbklkZW50aWZpZXIifQ==" + } + + let service = AnyADYServiceMock() + service.authenticationRequestParameters = authenticationRequestParameters + + let authenticationServiceMock = AuthenticationServiceMock() + authenticationServiceMock.onAuthenticate = { input in + return "OnAuthenticate" + } + let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") + let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, + showApprovalScreenReturnState: .fallback), + delegatedAuthenticationService: authenticationServiceMock) + + let fingerprintAction = ThreeDS2FingerprintAction(fingerprintToken: TestData.fingerprintToken, + authorisationToken: "AuthToken", + paymentData: "paymentData") + sut.handle(fingerprintAction, event: analyticsEvent) { fingerprintResult in + switch fingerprintResult { + case let .success(fingerprintString): + XCTAssertEqual(fingerprintString, TestData.expectedFingerprintResult) + case .failure: + XCTFail() + } + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 20, handler: nil) + } + + func testDelegatedAuthenticationFingerPrintResultWhenRemovingCredentials() { + // The token and result are base 64 encoded. + enum TestData { + // Token with delegatedAuthenticationSDKInput + static let fingerprintToken = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES0lucHV0IjoiIyNTb21lZGVsZWdhdGVkQXV0aGVudGljYXRpb25TREtJbnB1dCMjIiwiZGlyZWN0b3J5U2VydmVySWQiOiJGMDEzMzcxMzM3IiwiZGlyZWN0b3J5U2VydmVyUHVibGljS2V5IjoiI0RpcmVjdG9yeVNlcnZlclB1YmxpY0tleSMiLCJkaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIjoiIyNEaXJlY3RvcnlTZXJ2ZXJSb290Q2VydGlmaWNhdGVzIyMiLCJ0aHJlZURTTWVzc2FnZVZlcnNpb24iOiIyLjIuMCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiMTUwZmEzYjgtZTZjOC00N2ExLTk2ZTAtOTEwNzYzYmVlYzU3In0=" + + // Result with delegatedAuthenticationSDKOutput & the deleteCredentials flag + static let expectedFingerprintResult = "eyJkZWxlZ2F0ZWRBdXRoZW50aWNhdGlvblNES091dHB1dCI6Im9uQXV0aGVudGljYXRlLXNka091dHB1dCIsImRlbGV0ZURlbGVnYXRlZEF1dGhlbnRpY2F0aW9uQ3JlZGVudGlhbCI6dHJ1ZSwic2RrQXBwSUQiOiJzZGtBcHBsaWNhdGlvbklkZW50aWZpZXIiLCJzZGtFbmNEYXRhIjoiZGV2aWNlX2luZm8iLCJzZGtFcGhlbVB1YktleSI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IjNiM21QZldodU94d09XeWRMZWpTM0RKRVVQaU1WRnh0ekdDVjY5MDZyZmMiLCJ5IjoienYwa3oxU0tmTnZUM3FsNzVMMjE3ZGU2WnN6eGZMQThMVUtPSUtlNVpmNCJ9LCJzZGtSZWZlcmVuY2VOdW1iZXIiOiJzZGtSZWZlcmVuY2VOdW1iZXIiLCJzZGtUcmFuc0lEIjoic2RrVHJhbnNhY3Rpb25JZGVudGlmaWVyIn0=" + } + + let service = AnyADYServiceMock() + service.authenticationRequestParameters = authenticationRequestParameters + + let onAuthenticateExpectation = expectation(description: "Expect onReset to be called") + let authenticationServiceMock = AuthenticationServiceMock() + authenticationServiceMock.onAuthenticate = { input in + onAuthenticateExpectation.fulfill() + return "onAuthenticate-sdkOutput" + } + + let onResetExpectation = expectation(description: "Expect onReset to not be called") + onResetExpectation.isInverted = true + authenticationServiceMock.onReset = { + onResetExpectation.fulfill() + } + + let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") + + let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, + showApprovalScreenReturnState: .removeCredentials, + userInput: .deleteDA), + delegatedAuthenticationService: authenticationServiceMock) + + let fingerprintAction = ThreeDS2FingerprintAction(fingerprintToken: TestData.fingerprintToken, + authorisationToken: "AuthToken", + paymentData: "paymentData") + sut.handle(fingerprintAction, event: analyticsEvent) { fingerprintResult in + switch fingerprintResult { + case let .success(fingerprintString): + XCTAssertEqual(fingerprintString, TestData.expectedFingerprintResult) + case .failure: + XCTFail() + } + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 20, handler: nil) + } + + func testDelegatedAuthenticationChallengeResultWhenRemovingCredentials() throws { + let service = AnyADYServiceMock() + service.authenticationRequestParameters = authenticationRequestParameters + + let transaction = AnyADYTransactionMock(parameters: authenticationRequestParameters) + transaction.onPerformChallenge = { params, completion in + completion(AnyChallengeResultMock(sdkTransactionIdentifier: "sdkTxId", transactionStatus: "Y"), nil) + } + service.mockedTransaction = transaction + + let authenticationServiceMock = AuthenticationServiceMock() + + authenticationServiceMock.onRegister = { _ in + XCTFail("On Register should not be called when the user doesn't consent to register") + return self.expectedSDKRegistrationOutput + } + + let expectedResult = try XCTUnwrap(try? ThreeDSResult(from: AnyChallengeResultMock(sdkTransactionIdentifier: "sdkTransactionIdentifier", transactionStatus: "Y"), + delegatedAuthenticationSDKOutput: nil, + authorizationToken: "authToken", + threeDS2SDKError: nil)) + + let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") + let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, + showApprovalScreenReturnState: .fallback, + userInput: .deleteDA), + delegatedAuthenticationService: authenticationServiceMock, + deviceSupportCheckerService: DeviceSupportCheckerMock(isDeviceSupported: true)) + sut.transaction = transaction + sut.delegatedAuthenticationState.isDeviceRegistrationFlow = true + sut.handle(challengeAction, event: analyticsEvent) { challengeResult in + switch challengeResult { + case let .success(result): + XCTAssertEqual(result, expectedResult) + case .failure: + XCTFail() + } + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + + func testDelegatedAuthenticationRegistrationFlowWhenUserDoesntConsentToRegister() throws { + + let service = AnyADYServiceMock() + service.authenticationRequestParameters = authenticationRequestParameters + + let transaction = AnyADYTransactionMock(parameters: authenticationRequestParameters) + transaction.onPerformChallenge = { params, completion in + XCTAssertEqual(params.threeDSRequestorAppURL, URL(string: "https://google.com")) + completion(AnyChallengeResultMock(sdkTransactionIdentifier: "sdkTxId", transactionStatus: "Y"), nil) + } + service.mockedTransaction = transaction + + let authenticationServiceMock = AuthenticationServiceMock() + + authenticationServiceMock.onRegister = { _ in + XCTFail("On Register should not be called when the user doesn't consent to register") + return self.expectedSDKRegistrationOutput + } + + let expectedResult = try XCTUnwrap(try? ThreeDSResult(from: AnyChallengeResultMock(sdkTransactionIdentifier: "sdkTransactionIdentifier", transactionStatus: "Y"), + delegatedAuthenticationSDKOutput: nil, // // We shouldn't receive + authorizationToken: "authToken", + threeDS2SDKError: nil)) + + let resultExpectation = expectation(description: "Expect ThreeDS2ActionHandler completion closure to be called.") + let sut = ThreeDS2PlusDACoreActionHandler(context: Dummy.context, + service: service, + presenter: ThreeDS2DAScreenPresenterMock(showRegistrationReturnState: .fallback, showApprovalScreenReturnState: .fallback), + delegatedAuthenticationService: authenticationServiceMock, + deviceSupportCheckerService: DeviceSupportCheckerMock(isDeviceSupported: true)) + sut.threeDSRequestorAppURL = URL(string: "https://google.com") + sut.transaction = transaction + sut.delegatedAuthenticationState.isDeviceRegistrationFlow = true + sut.handle(challengeAction, event: analyticsEvent) { challengeResult in + switch challengeResult { + case let .success(result): + XCTAssertEqual(result, expectedResult) + case .failure: + XCTFail() + } + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testThreeDS2PlusDAScreenUserInput() { + XCTAssertTrue(ThreeDS2PlusDAScreenUserInput.noInput.canShowRegistration) + XCTAssertFalse(ThreeDS2PlusDAScreenUserInput.approveDifferently.canShowRegistration) + XCTAssertFalse(ThreeDS2PlusDAScreenUserInput.biometric.canShowRegistration) + XCTAssertFalse(ThreeDS2PlusDAScreenUserInput.deleteDA.canShowRegistration) + } + } + +internal struct DeviceSupportCheckerMock: AdyenAuthentication.DeviceSupportCheckerProtocol { + var isDeviceSupported: Bool + + func checkSupport() throws -> String { + return "" } +} #endif diff --git a/Tests/DropIn Tests/DropInActionTests.swift b/Tests/DropIn Tests/DropInActionTests.swift index 611326265a..3cb7b5f438 100644 --- a/Tests/DropIn Tests/DropInActionTests.swift +++ b/Tests/DropIn Tests/DropInActionTests.swift @@ -30,23 +30,18 @@ class DropInActionsTests: XCTestCase { let config = DropInComponent.Configuration() let paymentMethods = try! JSONDecoder().decode(PaymentMethods.self, from: DropInTests.paymentMethods.data(using: .utf8)!) - sut = DropInComponent(paymentMethods: paymentMethods, - context: context, - configuration: config) - - let waitExpectation = expectation(description: "Expect SafariViewController to open") + let sut = DropInComponent( + paymentMethods: paymentMethods, + context: context, + configuration: config + ) presentOnRoot(sut.viewController) { let action = Action.redirect(RedirectAction(url: URL(string: "https://www.adyen.com")!, paymentData: "test_data")) - self.sut.handle(action) - - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) { - XCTAssertNotNil(self.sut.viewController.adyen.topPresenter as? SFSafariViewController) - waitExpectation.fulfill() - } + sut.handle(action) } - waitForExpectations(timeout: 15, handler: nil) + wait(until: { sut.viewController.adyen.topPresenter is SFSafariViewController }) } func testOpenExternalApp() { diff --git a/Tests/DropIn Tests/DropInTestInternal.swift b/Tests/DropIn Tests/DropInTestInternal.swift index a32ab26a42..a99bfb37be 100644 --- a/Tests/DropIn Tests/DropInTestInternal.swift +++ b/Tests/DropIn Tests/DropInTestInternal.swift @@ -12,7 +12,7 @@ import XCTest class DropInInternalTests: XCTestCase { - func testFinaliseIfNeededSelectedComponent() { + func testFinaliseIfNeededSelectedComponent() throws { let config = DropInComponent.Configuration() let paymentMethods = try! JSONDecoder().decode(PaymentMethods.self, from: DropInTests.paymentMethodsWithSingleInstant.data(using: .utf8)!) @@ -24,15 +24,12 @@ class DropInInternalTests: XCTestCase { let waitExpectation = expectation(description: "Expect Drop-In to finalize") - wait(for: .seconds(1)) - - let topVC = sut.viewController.findChild(of: ListViewController.self) - topVC?.tableView(topVC!.tableView, didSelectRowAt: .init(item: 0, section: 0)) - let cell = topVC?.tableView.cellForRow(at: .init(item: 0, section: 0)) as! ListCell + let topVC = try waitForViewController(ofType: ListViewController.self, toBecomeChildOf: sut.viewController) + topVC.tableView(topVC.tableView, didSelectRowAt: .init(item: 0, section: 0)) + + let cell = try XCTUnwrap(topVC.tableView.cellForRow(at: .init(item: 0, section: 0)) as? ListCell) XCTAssertTrue(cell.showsActivityIndicator) - wait(for: .seconds(1)) - sut.finalizeIfNeeded(with: true) { XCTAssertFalse(cell.showsActivityIndicator) waitExpectation.fulfill()