Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

COIOS-812: Identify native redirect flow (v4) #1886

Draft
wants to merge 4 commits into
base: v4
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions AdyenActions/Actions/RedirectAction.swift
Original file line number Diff line number Diff line change
@@ -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.
//
Expand All @@ -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
}
}
16 changes: 10 additions & 6 deletions AdyenActions/Components/Redirect/RedirectComponent.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2022 Adyen N.V.
// Copyright (c) 2019 Adyen N.V.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Different swiftformats fighting each other again

//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")!)

Expand Down Expand Up @@ -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://")!)

Expand Down Expand Up @@ -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 {
Expand Down
Loading