Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,22 @@ public struct PONativeAlternativePaymentConfiguration {
/// and if it's the only required step, it will complete the flow without starting the bottom sheet.
public let enableHeadlessMode: Bool

/// Redirect confirmation button configuration. To remove button use `nil`, this is default behaviour.
///
/// Displays a confirmation button when the user needs to perform a redirect. The user
/// must press this button to continue.
public let redirectButton: SubmitButton?

public init(
callback: POWebAuthenticationCallback? = nil,
prefersEphemeralSession: Bool = true,
enableHeadlessMode: Bool = false
enableHeadlessMode: Bool = false,
redirectButton: SubmitButton? = nil
) {
self.callback = callback
self.prefersEphemeralSession = prefersEphemeralSession
self.enableHeadlessMode = enableHeadlessMode
self.redirectButton = redirectButton
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,29 +142,7 @@ final class NativeAlternativePaymentDefaultInteractor:
}
let task = Task {
do {
let didOpenUrl: Bool
switch currentState.redirect.type {
case .deepLink:
didOpenUrl = await openDeepLink(url: currentState.redirect.url)
case .web:
let authenticationRequest = POAlternativePaymentAuthenticationRequest(
url: currentState.redirect.url,
callback: configuration.redirect.callback,
prefersEphemeralSession: configuration.redirect.prefersEphemeralSession
)
_ = try await alternativePaymentsService.authenticate(request: authenticationRequest)
didOpenUrl = true
default:
throw POFailure(errorDescription: "Unknown redirect type.", code: .Mobile.internal)
}
let response = try await serviceAdapter.continuePayment(
with: .init(
flow: configuration.flow,
redirect: currentState.redirect.confirmationRequired ? .init(success: didOpenUrl) : nil,
localeIdentifier: configuration.localization.localeOverride?.identifier
)
)
try await setState(with: response)
try await uncheckedRedirect(to: currentState.redirect)
} catch {
setFailureState(error: error)
}
Expand Down Expand Up @@ -212,9 +190,7 @@ final class NativeAlternativePaymentDefaultInteractor:
private func setState(with response: NativeAlternativePaymentServiceAdapterResponse) async throws {
switch response.state {
case .nextStepRequired:
if case .starting = state, let redirect = response.redirect, configuration.redirect.enableHeadlessMode {
try await continueStart(withHeadlessRedirect: redirect)
} else if let redirect = response.redirect {
if let redirect = response.redirect {
try await setAwaitingRedirectState(response: response, redirect: redirect)
} else {
try await setStartedState(response: response)
Expand All @@ -230,38 +206,6 @@ final class NativeAlternativePaymentDefaultInteractor:
}
}

// MARK: - Starting State

private func continueStart(withHeadlessRedirect redirect: PONativeAlternativePaymentRedirectV2) async throws {
guard case .starting = state else {
logger.error("Attempted to handle headless redirect while not in starting state. Ignoring.")
return
}
let didOpenUrl: Bool
switch redirect.type {
case .deepLink:
didOpenUrl = await openDeepLink(url: redirect.url)
case .web:
let authenticationRequest = POAlternativePaymentAuthenticationRequest(
url: redirect.url,
callback: configuration.redirect.callback,
prefersEphemeralSession: configuration.redirect.prefersEphemeralSession
)
_ = try await alternativePaymentsService.authenticate(request: authenticationRequest)
didOpenUrl = true
default:
throw POFailure(errorDescription: "Unknown redirect type.", code: .Mobile.internal)
}
let response = try await serviceAdapter.continuePayment(
with: .init(
flow: configuration.flow,
redirect: redirect.confirmationRequired ? .init(success: didOpenUrl) : nil,
localeIdentifier: configuration.localization.localeOverride?.identifier
)
)
try await setState(with: response)
}

// MARK: - Started State

private func setStartedState(response: NativeAlternativePaymentServiceAdapterResponse) async throws {
Expand Down Expand Up @@ -401,49 +345,80 @@ final class NativeAlternativePaymentDefaultInteractor:
response: NativeAlternativePaymentServiceAdapterResponse,
redirect: PONativeAlternativePaymentRedirectV2
) async throws {
let paymentMethod = await resolve(paymentMethod: response.paymentMethod)
let elements = try await resolve(elements: response.elements ?? [])
if shouldConfirmRedirect(redirect, in: state) {
let paymentMethod = await resolve(paymentMethod: response.paymentMethod)
let elements = try await resolve(elements: response.elements ?? [])
switch state {
case .starting, .submitting, .redirecting:
break // todo(andrii-vysotskyi): check if more states should be supported
default:
logger.debug("Ignoring attempt to set started state in unsupported state: \(state).")
return
}
let newState = State.AwaitingRedirect(
paymentMethod: paymentMethod,
invoice: response.invoice,
elements: elements,
redirect: redirect,
isCancellable: configuration.cancelButton?.disabledFor.isZero ?? true
)
sendDidStartEventIfNeeded()
state = .awaitingRedirect(newState)
enableCancellationAfterDelay()
} else {
try await uncheckedRedirect(to: redirect)
}
}

private func shouldConfirmRedirect(
_ redirect: PONativeAlternativePaymentRedirectV2, in state: NativeAlternativePaymentInteractorState
) -> Bool {
switch state {
case .starting, .submitting, .redirecting:
break // todo(andrii-vysotskyi): check if more states should be supported
case .redirecting:
return false
case .starting where configuration.redirect.enableHeadlessMode:
return false
default:
logger.debug("Ignoring attempt to set started state in unsupported state: \(state).")
return
return configuration.redirect.redirectButton != nil
}
let newState = State.AwaitingRedirect(
paymentMethod: paymentMethod,
invoice: response.invoice,
elements: elements,
redirect: redirect,
isCancellable: configuration.cancelButton?.disabledFor.isZero ?? true
)
sendDidStartEventIfNeeded()
state = .awaitingRedirect(newState)
enableCancellationAfterDelay()
}

/// Requests state change to redirecting while bypassing actual redirect assuming it was performed elsewhere.
private func setRedirectingState(didOpenUrl: Bool) {
guard case .awaitingRedirect(let currentState) = state else {
logger.debug("Ignoring redirect confirmation in unsupported state \(state).")
return
// MARK: - Redirecting State

private func uncheckedRedirect(to redirect: PONativeAlternativePaymentRedirectV2) async throws {
let didOpenUrl: Bool
switch redirect.type {
case .deepLink:
didOpenUrl = await openDeepLink(url: redirect.url)
case .web:
let authenticationRequest = POAlternativePaymentAuthenticationRequest(
url: redirect.url,
callback: configuration.redirect.callback,
prefersEphemeralSession: configuration.redirect.prefersEphemeralSession
)
_ = try await alternativePaymentsService.authenticate(request: authenticationRequest)
didOpenUrl = true
default:
throw POFailure(errorDescription: "Unknown redirect type.", code: .Mobile.internal)
}
let task = Task {
do {
let response = try await serviceAdapter.continuePayment(
with: .init(
flow: configuration.flow,
redirect: currentState.redirect.confirmationRequired ? .init(success: didOpenUrl) : nil,
localeIdentifier: configuration.localization.localeOverride?.identifier
)
)
try await setState(with: response)
} catch {
setFailureState(error: error)
}
let response = try await serviceAdapter.continuePayment(
with: .init(
flow: configuration.flow,
redirect: redirect.confirmationRequired ? .init(success: didOpenUrl) : nil,
localeIdentifier: configuration.localization.localeOverride?.identifier
)
)
try await setState(with: response)
}

private func openDeepLink(url: URL) async -> Bool {
let options: [UIApplication.OpenExternalURLOptionsKey: Any]
if url.scheme == "https" || url.scheme == "http" { // Determines whether link could be universal
options = [.universalLinksOnly: true]
} else {
options = [:]
}
let newState = State.Redirecting(task: task, snapshot: currentState)
state = .redirecting(newState)
return await UIApplication.shared.open(url, options: options)
}

// MARK: - Completed State
Expand Down Expand Up @@ -935,18 +910,6 @@ final class NativeAlternativePaymentDefaultInteractor:
return nil
}

// MARK: - Redirect Utils

private func openDeepLink(url: URL) async -> Bool {
let options: [UIApplication.OpenExternalURLOptionsKey: Any]
if url.scheme == "https" || url.scheme == "http" { // Determines whether link could be universal
options = [.universalLinksOnly: true]
} else {
options = [:]
}
return await UIApplication.shared.open(url, options: options)
}

// MARK: - External Events

private func observeEvents() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel {
) -> NativeAlternativePaymentViewModelControlGroup? {
var buttons: [POButtonViewModel] = [
createRedirectButton(state: state, isRedirecting: isRedirecting)
]
].compactMap(\.self)
let cancelButton = createCancelButton(
configuration: interactor.configuration.cancelButton, isEnabled: state.isCancellable
)
Expand All @@ -324,11 +324,14 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel {

private func createRedirectButton(
state: InteractorState.AwaitingRedirect, isRedirecting: Bool
) -> POButtonViewModel {
// todo(andrii-vysotskyi): decide whether button should be customizable
) -> POButtonViewModel? {
guard let buttonConfiguration = interactor.configuration.redirect.redirectButton else {
return nil
}
let viewModel = POButtonViewModel(
id: "redirect-button",
title: state.redirect.hint,
title: buttonConfiguration.title ?? state.redirect.hint,
icon: buttonConfiguration.icon,
isEnabled: true,
isLoading: isRedirecting,
role: .primary,
Expand Down
Loading