Skip to content

Commit

Permalink
COIOS-812: Identify native redirect flow (v4) (#1886)
Browse files Browse the repository at this point in the history
## Summary
This development enables us to differentiate between regular and native
redirects. To handle cases where native redirects fail (indicated by
`nativeRedirectData` being `nil`), it's essential to track the
originating flow type of each redirect.

## Motivation
Native redirect flows can occasionally fail if `nativeRedirectData` is
`nil` within the action object. Currently, we handle this by discarding
the native redirect (checking if `nativeRedirectData` is nil), and
defaulting to a "direct issuer flow" using a `/details` call.

To avoid the additional steps of the "direct issuer flow," we can
address this issue on the backend. By identifying the native redirect
flow, we can still retrieve the native redirect result directly.

# Release notes

<new> 

- Added support for distinguishing between regular and native redirects
to improve handling of failed native redirect flows.

</new>

# Ticket

<ticket>
COIOS-812
</ticket>
  • Loading branch information
nauaros authored Jan 22, 2025
2 parents c1e8eda + 33c4c24 commit 20a5be9
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 24 deletions.
35 changes: 28 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,46 @@ 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
}
}
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.
//
// 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

0 comments on commit 20a5be9

Please sign in to comment.