diff --git a/AdyenActions/Actions/RedirectAction.swift b/AdyenActions/Actions/RedirectAction.swift index c2a91a9722..d6efe801b9 100644 --- a/AdyenActions/Actions/RedirectAction.swift +++ b/AdyenActions/Actions/RedirectAction.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2019 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -8,25 +8,63 @@ import Foundation /// Describes an action in which the user is redirected to a URL. public struct RedirectAction: Decodable { - + + /// Defines the type of redirect flow utilized by the `RedirectAction` object. + public enum RedirectType: String, Decodable { + case redirect + case nativeRedirect + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let type = try container.decode(String.self) + self = RedirectType(rawValue: type) ?? .redirect + } + } + /// The URL to which to redirect the user. public let url: URL - + /// The server-generated payment data that should be submitted to the `/payments/details` endpoint. public let paymentData: String? - + + internal let type: RedirectType + /// Native redirect data. public let nativeRedirectData: String? - + /// Initializes a redirect action. /// /// - Parameters: /// - url: The URL to which to redirect the user. /// - paymentData: The server-generated payment data that should be submitted to the `/payments/details` endpoint. - /// - nativeRedirectData: Native redirect data. - public init(url: URL, paymentData: String?, nativeRedirectData: String? = nil) { + /// - type: The redirect flow used by the action. Defaults to `redirect`. + /// - nativeRedirectData: Native redirect data. Defaults to `nil`. + public init( + url: URL, + paymentData: String?, + type: RedirectType = .redirect, + nativeRedirectData: String? = nil + ) { self.url = url self.paymentData = paymentData + self.type = type self.nativeRedirectData = nativeRedirectData } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.url = try container.decode(URL.self, forKey: .url) + self.paymentData = try container.decodeIfPresent(String.self, forKey: .paymentData) + self.type = try container.decode(RedirectType.self, forKey: .type) + self.nativeRedirectData = try container.decodeIfPresent(String.self, forKey: .nativeRedirectData) + } + + // MARK: - Private + + private enum CodingKeys: CodingKey { + case url + case paymentData + case type + case nativeRedirectData + } } diff --git a/AdyenActions/Components/Redirect/RedirectComponent.swift b/AdyenActions/Components/Redirect/RedirectComponent.swift index 7c36fb4945..9b9d2ffde2 100644 --- a/AdyenActions/Components/Redirect/RedirectComponent.swift +++ b/AdyenActions/Components/Redirect/RedirectComponent.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2019 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -137,15 +137,19 @@ public final class RedirectComponent: ActionComponent { private func registerRedirectBounceBackListener(_ action: RedirectAction) { RedirectListener.registerForURL { [weak self] returnURL in guard let self else { return } - self.didOpen(url: returnURL, action) } } private func didOpen(url returnURL: URL, _ action: RedirectAction) { - if let redirectStateData = action.nativeRedirectData { - handleNativeMobileRedirect(withReturnURL: returnURL, redirectStateData: redirectStateData, action) - } else { + switch action.type { + case .nativeRedirect: + handleNativeMobileRedirect( + withReturnURL: returnURL, + redirectStateData: action.nativeRedirectData, + action + ) + case .redirect: do { try notifyDelegateDidProvide(redirectDetails: RedirectDetails(returnURL: returnURL), action) } catch { @@ -154,7 +158,7 @@ public final class RedirectComponent: ActionComponent { } } - private func handleNativeMobileRedirect(withReturnURL returnURL: URL, redirectStateData: String, _ action: RedirectAction) { + private func handleNativeMobileRedirect(withReturnURL returnURL: URL, redirectStateData: String?, _ action: RedirectAction) { guard let queryString = returnURL.query else { delegate?.didFail(with: Error.invalidRedirectParameters, from: self) return diff --git a/Tests/AdyenTests/Adyen Tests/Components/Redirect/RedirectComponentTests.swift b/Tests/AdyenTests/Adyen Tests/Components/Redirect/RedirectComponentTests.swift index e33fe52be6..3dc21a6ea2 100644 --- a/Tests/AdyenTests/Adyen Tests/Components/Redirect/RedirectComponentTests.swift +++ b/Tests/AdyenTests/Adyen Tests/Components/Redirect/RedirectComponentTests.swift @@ -13,7 +13,11 @@ import XCTest class RedirectComponentTests: XCTestCase { func testUIConfiguration() { - let action = RedirectAction(url: URL(string: "https://adyen.com")!, paymentData: "data") + let action = RedirectAction( + url: URL(string: "https://adyen.com")!, + paymentData: "data", + type: .redirect + ) let style = RedirectComponentStyle(preferredBarTintColor: UIColor.red, preferredControlTintColor: UIColor.black, modalPresentationStyle: .fullScreen) @@ -48,7 +52,11 @@ class RedirectComponentTests: XCTestCase { delegateExpectation.fulfill() } - let action = RedirectAction(url: URL(string: "bla://")!, paymentData: "test_data") + let action = RedirectAction( + url: URL(string: "bla://")!, + paymentData: "test_data", + type: .redirect + ) sut.handle(action) waitForExpectations(timeout: 10, handler: nil) @@ -82,7 +90,11 @@ class RedirectComponentTests: XCTestCase { XCTAssertTrue(component === sut) } - let action = RedirectAction(url: URL(string: "bla://")!, paymentData: "test_data") + let action = RedirectAction( + url: URL(string: "bla://")!, + paymentData: "test_data", + type: .redirect + ) sut.handle(action) waitForExpectations(timeout: 10, handler: nil) @@ -112,7 +124,11 @@ class RedirectComponentTests: XCTestCase { delegateExpectation.fulfill() } - let action = RedirectAction(url: URL(string: "http://maps.apple.com")!, paymentData: "test_data") + let action = RedirectAction( + url: URL(string: "http://maps.apple.com")!, + paymentData: "test_data", + type: .redirect + ) sut.handle(action) waitForExpectations(timeout: 10, handler: nil) @@ -152,7 +168,11 @@ class RedirectComponentTests: XCTestCase { XCTFail("delegate.didOpenExternalApplication() must not to be called") } - let action = RedirectAction(url: URL(string: "http://maps.apple.com")!, paymentData: "test_data") + let action = RedirectAction( + url: URL(string: "http://maps.apple.com")!, + paymentData: "test_data", + type: .redirect + ) sut.handle(action) waitForExpectations(timeout: 10, handler: nil) @@ -178,7 +198,11 @@ class RedirectComponentTests: XCTestCase { XCTFail("delegate.didOpenExternalApplication() must not to be called") } - let action = RedirectAction(url: URL(string: "https://www.adyen.com")!, paymentData: "test_data") + let action = RedirectAction( + url: URL(string: "https://www.adyen.com")!, + paymentData: "test_data", + type: .redirect + ) sut.handle(action) let waitExpectation = expectation(description: "Expect in app browser to be presented and then dismissed") @@ -200,7 +224,11 @@ class RedirectComponentTests: XCTestCase { let delegate = ActionComponentDelegateMock() sut.delegate = delegate - let action = RedirectAction(url: URL(string: "https://www.adyen.com")!, paymentData: "test_data") + let action = RedirectAction( + url: URL(string: "https://www.adyen.com")!, + paymentData: "test_data", + type: .redirect + ) sut.handle(action) let waitExpectation = expectation(description: "Expect in app browser to be presented and then dismissed") @@ -228,7 +256,11 @@ class RedirectComponentTests: XCTestCase { sut.presentationDelegate = presentationDelegate let delegate = ActionComponentDelegateMock() sut.delegate = delegate - let action = RedirectAction(url: URL(string: "https://www.adyen.com")!, paymentData: "test_data") + let action = RedirectAction( + url: URL(string: "https://www.adyen.com")!, + paymentData: "test_data", + type: .redirect + ) let presentExpectation = expectation(description: "Expect in app browser to be presented") presentationDelegate.doPresent = { component in @@ -279,7 +311,14 @@ class RedirectComponentTests: XCTestCase { } delegate.onDidFail = { _, _ in XCTFail("Should not call onDidFail") } - let action = RedirectAction(url: URL(string: "http://google.com")!, paymentData: nil, nativeRedirectData: "test_nativeRedirectData") + let action = RedirectAction( + url: URL( + string: "http://google.com" + )!, + paymentData: nil, + type: .nativeRedirect, + nativeRedirectData: "test_nativeRedirectData" + ) sut.handle(action) _ = RedirectListener.applicationDidOpen(from: URL(string: "url://?queryParam=value")!) @@ -309,7 +348,12 @@ class RedirectComponentTests: XCTestCase { redirectExpectation.fulfill() } - let action = RedirectAction(url: URL(string: "http://google.com")!, paymentData: nil, nativeRedirectData: "test_nativeRedirectData") + let action = RedirectAction( + url: URL(string: "http://google.com")!, + paymentData: nil, + type: .nativeRedirect, + nativeRedirectData: "test_nativeRedirectData" + ) sut.handle(action) _ = RedirectListener.applicationDidOpen(from: URL(string: "url://")!) @@ -341,12 +385,56 @@ class RedirectComponentTests: XCTestCase { redirectExpectation.fulfill() } - let action = RedirectAction(url: URL(string: "http://google.com")!, paymentData: nil, nativeRedirectData: "test_nativeRedirectData") + let action = RedirectAction( + url: URL(string: "http://google.com")!, + paymentData: nil, + type: .nativeRedirect, + nativeRedirectData: "test_nativeRedirectData" + ) sut.handle(action) _ = RedirectListener.applicationDidOpen(from: URL(string: "url://?queryParam=value")!) waitForExpectations(timeout: 2) } + + func testNativeRedirectWithNativeRedirectDataNilShouldPerformNativeRedirectResultRequest() { + // Given + let apiClient = APIClientMock() + let sut = RedirectComponent(apiContext: Dummy.context, apiClient: apiClient.retryAPIClient(with: SimpleScheduler(maximumCount: 2))) + apiClient.mockedResults = [.success(try! RedirectDetails(returnURL: URL(string: "url://?redirectResult=test_redirectResult")!))] + + let appLauncher = AppLauncherMock() + sut.appLauncher = appLauncher + let appLauncherExpectation = expectation(description: "Expect appLauncher.openUniversalAppUrl() to be called") + appLauncher.onOpenUniversalAppUrl = { url, completion in + XCTAssertEqual(url, URL(string: "https://google.com")!) + completion?(true) + appLauncherExpectation.fulfill() + } + + let delegate = ActionComponentDelegateMock() + sut.delegate = delegate + let redirectExpectation = expectation(description: "Expect redirect to be proccessed") + delegate.onDidProvide = { data, component in + XCTAssertTrue(component === sut) + XCTAssertNotNil(data.details) + redirectExpectation.fulfill() + } + delegate.onDidFail = { _, _ in XCTFail("Should not call onDidFail") } + + // When + let action = RedirectAction( + url: URL(string: "https://google.com")!, + paymentData: nil, + type: .nativeRedirect, + nativeRedirectData: nil + ) + sut.handle(action) + + // Then + XCTAssertTrue(RedirectComponent.applicationDidOpen(from: URL(string: "url://?queryParam=value")!)) + waitForExpectations(timeout: 10) + } } extension UIViewController {