From 8d701d82a10bbfae6b6366b5b40e31be9139ba5f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 27 Mar 2026 18:01:05 +0200 Subject: [PATCH 01/15] Update deep/app links in example app --- example/src/main/AndroidManifest.xml | 11 +++++++++++ .../kotlin/com/processout/example/shared/Constants.kt | 5 +++-- .../ui/screen/card/payment/CardPaymentFragment.kt | 4 ++-- .../ui/screen/card/payment/CardPaymentViewModel.kt | 2 +- .../screen/checkout/DefaultDynamicCheckoutDelegate.kt | 2 +- .../ui/screen/checkout/DynamicCheckoutFragment.kt | 4 ++-- .../ui/screen/checkout/DynamicCheckoutViewModel.kt | 2 +- .../example/ui/screen/nativeapm/NativeApmFragment.kt | 6 +++--- .../example/ui/screen/nativeapm/NativeApmViewModel.kt | 2 +- 9 files changed, 25 insertions(+), 13 deletions(-) diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 45e3dd3c..87f46bc2 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -30,6 +30,17 @@ + + + + + + + + diff --git a/example/src/main/kotlin/com/processout/example/shared/Constants.kt b/example/src/main/kotlin/com/processout/example/shared/Constants.kt index 486bf5b5..914962d2 100644 --- a/example/src/main/kotlin/com/processout/example/shared/Constants.kt +++ b/example/src/main/kotlin/com/processout/example/shared/Constants.kt @@ -3,6 +3,7 @@ package com.processout.example.shared import com.processout.example.BuildConfig object Constants { - const val DEFAULT_RETURN_URL = "${BuildConfig.APPLICATION_ID}://processout/return" - const val MERCHANT_RETURN_URL = "merchant://example/return" + const val RETURN_URL_DEFAULT = "${BuildConfig.APPLICATION_ID}://processout/return" + const val RETURN_URL_APP_LINK = "https://merchant-example.com/return" + const val RETURN_URL_DEEP_LINK = "merchant://example/return" } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt index 3ad13f8e..51bf6af7 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt @@ -117,7 +117,7 @@ class CardPaymentFragment : BaseFragment( delegate = Netcetera3DS2ServiceDelegate( provideActivity = { POCardTokenizationActivity.instance }, customTabLauncher = customTabLauncher, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ), configuration = PONetcetera3DS2ServiceConfiguration( bridgingExtensionVersion = BridgingMessageExtensionVersion.V20 @@ -130,7 +130,7 @@ class CardPaymentFragment : BaseFragment( delegate = Checkout3DSServiceDelegate( activity = requireActivity(), customTabLauncher = customTabLauncher, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ).build() diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt index 49551d20..e7b6c89a 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt @@ -66,7 +66,7 @@ class CardPaymentViewModel( amount = state.amount, currency = state.currency, customerId = state.customerId, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ).getOrNull() _state.update { it.copy(invoiceId = invoice?.id) } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt index 99843217..ec9c4814 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt @@ -43,7 +43,7 @@ class DefaultDynamicCheckoutDelegate( amount = details.amount, currency = details.currency, customerId = customerId, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ) } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt index f3f772c4..f3b33328 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt @@ -98,7 +98,7 @@ class DynamicCheckoutFragment : BaseFragment( clientSecret = uiModel.clientSecret ), alternativePayment = AlternativePaymentConfiguration( - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ) ) @@ -111,7 +111,7 @@ class DynamicCheckoutFragment : BaseFragment( delegate = Netcetera3DS2ServiceDelegate( provideActivity = { PODynamicCheckoutActivity.instance }, customTabLauncher = customTabLauncher, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ), configuration = PONetcetera3DS2ServiceConfiguration( bridgingExtensionVersion = BridgingMessageExtensionVersion.V20 diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt index cc3f82d9..ef24d9b3 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt @@ -59,7 +59,7 @@ class DynamicCheckoutViewModel( amount = details.amount, currency = details.currency, customerId = customerId, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ).onSuccess { invoice -> _uiState.value = Submitted( diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt index 0a7433b6..3411b4d0 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt @@ -120,7 +120,7 @@ class NativeApmFragment : BaseFragment( ), cancelButton = CancelButton(), redirect = RedirectConfiguration( - returnUrl = Constants.MERCHANT_RETURN_URL, + returnUrl = Constants.RETURN_URL_DEEP_LINK, enableHeadlessMode = true ), paymentConfirmation = PaymentConfirmationConfiguration( @@ -138,7 +138,7 @@ class NativeApmFragment : BaseFragment( ), cancelButton = CancelButton(), redirect = RedirectConfiguration( - returnUrl = Constants.MERCHANT_RETURN_URL, + returnUrl = Constants.RETURN_URL_DEEP_LINK, enableHeadlessMode = true ), paymentConfirmation = PaymentConfirmationConfiguration( @@ -163,7 +163,7 @@ class NativeApmFragment : BaseFragment( ), cancelButton = CancelButton(), redirect = RedirectConfiguration( - returnUrl = Constants.MERCHANT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEEP_LINK ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt index 6f5acae7..af2903fa 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt @@ -62,7 +62,7 @@ class NativeApmViewModel( amount = amount, currency = currency, customerId = customerId, - returnUrl = Constants.MERCHANT_RETURN_URL, + returnUrl = Constants.RETURN_URL_DEEP_LINK, shipping = POContact( address1 = "6th Street", city = "Paris", From bb2e940a920d534c41d9fe0a0897151229c852f5 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 6 Apr 2026 20:19:14 +0300 Subject: [PATCH 02/15] Refactor headless mode --- .../ui/screen/nativeapm/NativeApmFragment.kt | 7 +- .../main/res/layout/fragment_native_apm.xml | 9 + .../com/processout/sdk/core/POFailure.kt | 1 + .../NativeAlternativePaymentInteractor.kt | 53 +- ...PONativeAlternativePaymentConfiguration.kt | 8 +- .../PONativeAlternativePaymentLauncher.kt | 572 +++++++----------- 6 files changed, 254 insertions(+), 396 deletions(-) diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt index 3411b4d0..933b971a 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt @@ -121,7 +121,7 @@ class NativeApmFragment : BaseFragment( cancelButton = CancelButton(), redirect = RedirectConfiguration( returnUrl = Constants.RETURN_URL_DEEP_LINK, - enableHeadlessMode = true + enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), @@ -139,7 +139,7 @@ class NativeApmFragment : BaseFragment( cancelButton = CancelButton(), redirect = RedirectConfiguration( returnUrl = Constants.RETURN_URL_DEEP_LINK, - enableHeadlessMode = true + enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), @@ -163,7 +163,8 @@ class NativeApmFragment : BaseFragment( ), cancelButton = CancelButton(), redirect = RedirectConfiguration( - returnUrl = Constants.RETURN_URL_DEEP_LINK + returnUrl = Constants.RETURN_URL_DEEP_LINK, + enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), diff --git a/example/src/main/res/layout/fragment_native_apm.xml b/example/src/main/res/layout/fragment_native_apm.xml index 8b29f4fb..2f0b6ac7 100644 --- a/example/src/main/res/layout/fragment_native_apm.xml +++ b/example/src/main/res/layout/fragment_native_apm.xml @@ -74,6 +74,15 @@ android:inputType="text|textCapCharacters" /> + + + if (stateValue.redirect == null) { + val failure = ProcessOutResult.Failure( + code = Generic(genericCode = mobileOperationNotSupported), + message = "Headless mode is not supported: missing redirect parameters." + ) + POLogger.error( + message = "Unsupported operation: %s", failure, + attributes = configuration.logAttributes + ) + _completion.update { Failure(failure) } + return@whenNextStep + } + redirect( + stateValue = stateValue, + redirect = stateValue.redirect + ) + } } //endregion diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt index 25aceadd..4253ad78 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt @@ -4,8 +4,6 @@ import android.os.Parcelable import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.IntRange -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentTokenizationResponse import com.processout.sdk.ui.core.shared.image.PODrawableImage import com.processout.sdk.ui.core.style.* import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.CancelButton @@ -69,8 +67,7 @@ data class PONativeAlternativePaymentConfiguration( data class Authorization( val invoiceId: String, val gatewayConfigurationId: String, - val customerTokenId: String? = null, - internal val initialResponse: PONativeAlternativePaymentAuthorizationResponse? = null + val customerTokenId: String? = null ) : Flow() /** @@ -84,8 +81,7 @@ data class PONativeAlternativePaymentConfiguration( data class Tokenization( val customerId: String, val customerTokenId: String, - val gatewayConfigurationId: String, - internal val initialResponse: PONativeAlternativePaymentTokenizationResponse? = null + val gatewayConfigurationId: String ) : Flow() } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt index 9e5d4aca..558e5819 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt @@ -1,84 +1,55 @@ package com.processout.sdk.ui.napm import android.app.Application +import android.content.Context import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.core.app.ActivityOptionsCompat import androidx.core.net.toUri import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.processout.sdk.R -import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.POEventDispatcher -import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest -import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRedirectConfirmation -import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest import com.processout.sdk.api.model.response.POAlternativePaymentMethodResponse -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.DeepLinkConfiguration -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.RedirectType -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.* -import com.processout.sdk.api.service.POCustomerTokensService -import com.processout.sdk.api.service.POInvoicesService -import com.processout.sdk.core.* -import com.processout.sdk.core.POFailure.Code.Generic -import com.processout.sdk.core.POFailure.Code.Internal +import com.processout.sdk.core.POUnit +import com.processout.sdk.core.ProcessOutActivityResult +import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.core.toActivityResult import com.processout.sdk.ui.apm.POAlternativePaymentMethodCustomTabLauncher +import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Failure +import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Success +import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.WebRedirectResult +import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.WebRedirect import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Authorization -import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Tokenization import com.processout.sdk.ui.napm.delegate.v2.NativeAlternativePaymentDefaultValuesRequest import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentDelegate import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent -import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent.DidFail import com.processout.sdk.ui.napm.delegate.v2.toResponse -import com.processout.sdk.ui.shared.extension.openDeepLink import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Launcher that starts [NativeAlternativePaymentActivity] and provides the result. */ class PONativeAlternativePaymentLauncher private constructor( - private val hostActivity: ComponentActivity, + private val viewModel: NativeAlternativePaymentViewModel, private val launcher: ActivityResultLauncher, + private val activityOptions: ActivityOptionsCompat, private val delegate: PONativeAlternativePaymentDelegate, private val callback: (ProcessOutActivityResult) -> Unit, - private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance, - private val invoicesService: POInvoicesService = ProcessOut.instance.invoices, - private val customerTokensService: POCustomerTokensService = ProcessOut.instance.customerTokens + private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance ) { - private val app: Application = hostActivity.application - private val scope: CoroutineScope = hostActivity.lifecycleScope - - private val activityOptions = ActivityOptionsCompat.makeCustomAnimation( - hostActivity, R.anim.po_slide_in_vertical, 0 - ) - - private val viewModel: NativeAlternativePaymentViewModel by hostActivity.viewModels { - NativeAlternativePaymentViewModel.Factory( - app = app, - configuration = PONativeAlternativePaymentConfiguration( - flow = Authorization( - invoiceId = String(), - gatewayConfigurationId = String() - ), - header = null - ) - ) - } - private lateinit var customTabLauncher: POAlternativePaymentMethodCustomTabLauncher - private object LocalCache { - var configuration: PONativeAlternativePaymentConfiguration? = null - } - companion object { /** * Creates the launcher from Fragment. @@ -88,19 +59,29 @@ class PONativeAlternativePaymentLauncher private constructor( from: Fragment, delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from.requireActivity(), - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - callback - ), - delegate = delegate, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.requireActivity().application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + callback + ), + activityOptions = createActivityOptions(context = from.requireContext()), + delegate = delegate, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + from.viewLifecycleOwnerLiveData.observe(from) { lifecycleOwner -> + collectViewModelState(lifecycleOwner) + dispatchAllEvents(coroutineScope = lifecycleOwner.lifecycleScope) + } + } } /** @@ -114,19 +95,29 @@ class PONativeAlternativePaymentLauncher private constructor( from: Fragment, delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from.requireActivity(), - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.requireActivity().application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + callback + ), + activityOptions = createActivityOptions(context = from.requireContext()), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + from.viewLifecycleOwnerLiveData.observe(from) { lifecycleOwner -> + collectViewModelState(lifecycleOwner) + dispatchAllEvents(coroutineScope = lifecycleOwner.lifecycleScope) + } + } } /** @@ -137,19 +128,29 @@ class PONativeAlternativePaymentLauncher private constructor( fun create( from: Fragment, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from.requireActivity(), - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.requireActivity().application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + callback + ), + activityOptions = createActivityOptions(context = from.requireContext()), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + from.viewLifecycleOwnerLiveData.observe(from) { lifecycleOwner -> + collectViewModelState(lifecycleOwner) + dispatchAllEvents(coroutineScope = lifecycleOwner.lifecycleScope) + } + } } /** @@ -160,20 +161,28 @@ class PONativeAlternativePaymentLauncher private constructor( from: ComponentActivity, delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from, - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - from.activityResultRegistry, - callback - ), - delegate = delegate, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(context = from), + delegate = delegate, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + collectViewModelState(lifecycleOwner = from) + dispatchAllEvents(coroutineScope = from.lifecycleScope) + } } /** @@ -187,20 +196,28 @@ class PONativeAlternativePaymentLauncher private constructor( from: ComponentActivity, delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from, - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - from.activityResultRegistry, - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(context = from), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + collectViewModelState(lifecycleOwner = from) + dispatchAllEvents(coroutineScope = from.lifecycleScope) + } } /** @@ -211,38 +228,60 @@ class PONativeAlternativePaymentLauncher private constructor( fun create( from: ComponentActivity, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from, - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - from.activityResultRegistry, - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(context = from), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + collectViewModelState(lifecycleOwner = from) + dispatchAllEvents(coroutineScope = from.lifecycleScope) + } } + + private fun createViewModelFactory(app: Application) = + NativeAlternativePaymentViewModel.Factory( + app = app, + configuration = PONativeAlternativePaymentConfiguration( + flow = Authorization( + invoiceId = String(), + gatewayConfigurationId = String() + ), + header = null + ) + ) + + private fun createActivityOptions(context: Context) = + ActivityOptionsCompat.makeCustomAnimation( + context, R.anim.po_slide_in_vertical, 0 + ) } - init { - collectViewModelCompletion() - dispatchEvents() - dispatchDefaultValues() + private fun collectViewModelState(lifecycleOwner: LifecycleOwner) { + collectCompletion(lifecycleOwner) + collectSideEffects(lifecycleOwner) } - private fun collectViewModelCompletion() { - hostActivity.lifecycleScope.launch { - hostActivity.repeatOnLifecycle(Lifecycle.State.STARTED) { + private fun collectCompletion(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.completion.collect { completion -> when (completion) { - NativeAlternativePaymentCompletion.Success -> - completeHeadlessMode(result = ProcessOutResult.Success(value = POUnit)) - is NativeAlternativePaymentCompletion.Failure -> - completeHeadlessMode(result = completion.failure) + Success -> complete(result = ProcessOutResult.Success(POUnit)) + is Failure -> complete(result = completion.failure) else -> {} } } @@ -250,17 +289,34 @@ class PONativeAlternativePaymentLauncher private constructor( } } - private fun dispatchEvents() { + private fun collectSideEffects(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + withContext(Dispatchers.Main.immediate) { + viewModel.sideEffects.collect { + handle(sideEffect = it) + } + } + } + } + } + + private fun dispatchAllEvents(coroutineScope: CoroutineScope) { + dispatchEvents(coroutineScope) + dispatchDefaultValues(coroutineScope) + } + + private fun dispatchEvents(coroutineScope: CoroutineScope) { eventDispatcher.subscribe( - coroutineScope = scope + coroutineScope ) { delegate.onEvent(it) } } - private fun dispatchDefaultValues() { + private fun dispatchDefaultValues(coroutineScope: CoroutineScope) { eventDispatcher.subscribeForRequest( - coroutineScope = scope + coroutineScope ) { request -> - scope.launch { + coroutineScope.launch { val defaultValues = delegate.defaultValues( gatewayConfigurationId = request.gatewayConfigurationId, parameters = request.parameters @@ -271,240 +327,34 @@ class PONativeAlternativePaymentLauncher private constructor( } /** - * Launches the activity. + * Launches the payment. */ fun launch(configuration: PONativeAlternativePaymentConfiguration) { if (configuration.redirect?.enableHeadlessMode == true) { - launchHeadlessMode(configuration) + POLogger.info("Starting native alternative payment in headless mode.") + viewModel.start(configuration) } else { - launchActivity(configuration) - } - } - - private fun launchActivity(configuration: PONativeAlternativePaymentConfiguration) { - launcher.launch( - input = configuration, - options = activityOptions - ) - } - - private fun launchHeadlessMode(configuration: PONativeAlternativePaymentConfiguration) { - POLogger.info("Starting native alternative payment in headless mode.") - LocalCache.configuration = configuration - continuePayment(configuration) - } - - private fun continuePayment( - configuration: PONativeAlternativePaymentConfiguration, - redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null - ) { - scope.launch { - when (val flow = configuration.flow) { - is Authorization -> authorize(flow, configuration, redirectConfirmation) - is Tokenization -> tokenize(flow, configuration, redirectConfirmation) - } - } - } - - private suspend fun authorize( - flow: Authorization, - configuration: PONativeAlternativePaymentConfiguration, - redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null - ) { - val request = PONativeAlternativePaymentAuthorizationRequest( - invoiceId = flow.invoiceId, - gatewayConfigurationId = flow.gatewayConfigurationId, - source = flow.customerTokenId, - redirectConfirmation = redirectConfirmation - ) - invoicesService.authorize(request) - .onSuccess { response -> - val updatedConfiguration = configuration.copy( - flow = flow.copy(initialResponse = response) - ) - LocalCache.configuration = updatedConfiguration - handlePaymentState( - state = response.state, - redirect = response.redirect, - configuration = updatedConfiguration - ) - }.onFailure { failure -> - POLogger.info("Failed to fetch authorization details: %s", failure) - completeHeadlessMode(result = failure) - } - } - - private suspend fun tokenize( - flow: Tokenization, - configuration: PONativeAlternativePaymentConfiguration, - redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null - ) { - val request = PONativeAlternativePaymentTokenizationRequest( - customerId = flow.customerId, - customerTokenId = flow.customerTokenId, - gatewayConfigurationId = flow.gatewayConfigurationId, - redirectConfirmation = redirectConfirmation - ) - customerTokensService.tokenize(request) - .onSuccess { response -> - val updatedConfiguration = configuration.copy( - flow = flow.copy(initialResponse = response) - ) - LocalCache.configuration = updatedConfiguration - handlePaymentState( - state = response.state, - redirect = response.redirect, - configuration = updatedConfiguration - ) - }.onFailure { failure -> - POLogger.info("Failed to fetch tokenization details: %s", failure) - completeHeadlessMode(result = failure) - } - } - - private fun handlePaymentState( - state: PONativeAlternativePaymentState, - redirect: PONativeAlternativePaymentRedirect?, - configuration: PONativeAlternativePaymentConfiguration - ) { - when (state) { - NEXT_STEP_REQUIRED -> handleNextStep(redirect, configuration) - PENDING -> viewModel.start(configuration) - SUCCESS -> { - POLogger.info("Success: payment completed.") - completeHeadlessMode(result = ProcessOutResult.Success(value = POUnit)) - } - UNKNOWN -> { - val failure = ProcessOutResult.Failure( - code = Internal(), - message = "Unsupported payment state." - ) - POLogger.error( - message = "%s", failure, - attributes = configuration.logAttributes - ) - completeHeadlessMode(result = failure) - } - } - } - - private fun handleNextStep( - redirect: PONativeAlternativePaymentRedirect?, - configuration: PONativeAlternativePaymentConfiguration - ) { - if (redirect == null) { - launchActivity(configuration) - return - } - when (redirect.type) { - RedirectType.WEB -> webRedirect( - redirectUrl = redirect.url, - configuration = configuration - ) - RedirectType.DEEP_LINK -> deepLinkRedirect( - redirectUrl = redirect.url, - deepLinkConfiguration = redirect.deepLinkConfiguration, - configuration = configuration + launcher.launch( + input = configuration, + options = activityOptions ) - RedirectType.UNKNOWN -> { - val failure = ProcessOutResult.Failure( - code = Internal(), - message = "Unknown redirect type: ${redirect.rawType}" - ) - POLogger.error( - message = "Unexpected response: %s", failure, - attributes = configuration.logAttributes - ) - completeHeadlessMode(result = failure) - } } } - private fun webRedirect( - redirectUrl: String, - configuration: PONativeAlternativePaymentConfiguration - ) { - val returnUrl = configuration.redirect?.returnUrl - if (returnUrl.isNullOrBlank()) { - val failure = ProcessOutResult.Failure( - code = Generic(), - message = "Return URL is missing in configuration during web redirect flow." + private fun handle(sideEffect: NativeAlternativePaymentSideEffect) { + if (sideEffect is WebRedirect) { + customTabLauncher.launch( + uri = sideEffect.redirectUrl.toUri(), + returnUrl = sideEffect.returnUrl ) - POLogger.warn( - message = "Failed headless web redirect: %s", failure, - attributes = configuration.logAttributes - ) - completeHeadlessMode(result = failure) - return } - customTabLauncher.launch( - uri = redirectUrl.toUri(), - returnUrl = returnUrl - ) } private fun handleWebRedirect(result: ProcessOutResult) { - val configuration = LocalCache.configuration - result.onSuccess { - if (configuration == null) { - val failure = ProcessOutResult.Failure( - code = Internal(), - message = "Configuration is not cached when handling web redirect result." - ) - POLogger.error(message = "Failed headless web redirect: %s", failure) - completeHeadlessMode(result = failure) - return - } - val confirmationRequired = when (val flow = configuration.flow) { - is Authorization -> flow.initialResponse?.redirect?.confirmationRequired - is Tokenization -> flow.initialResponse?.redirect?.confirmationRequired - } - val redirectConfirmation = if (confirmationRequired == true) - PONativeAlternativePaymentRedirectConfirmation(success = true) else null - continuePayment(configuration, redirectConfirmation) - }.onFailure { failure -> - POLogger.warn( - message = "Failed headless web redirect: %s", failure, - attributes = configuration?.logAttributes - ) - completeHeadlessMode(result = failure) - } + viewModel.onEvent(WebRedirectResult(result)) } - private fun deepLinkRedirect( - redirectUrl: String, - deepLinkConfiguration: DeepLinkConfiguration?, - configuration: PONativeAlternativePaymentConfiguration - ) { - val didOpenUrl = app.openDeepLink( - url = redirectUrl, - packageNames = deepLinkConfiguration?.packageNames - ) - val confirmationRequired = when (val flow = configuration.flow) { - is Authorization -> flow.initialResponse?.redirect?.confirmationRequired - is Tokenization -> flow.initialResponse?.redirect?.confirmationRequired - } - val redirectConfirmation = if (confirmationRequired == true) - PONativeAlternativePaymentRedirectConfirmation(success = didOpenUrl) else null - continuePayment(configuration, redirectConfirmation) - } - - private fun completeHeadlessMode(result: ProcessOutResult) { - result.onFailure { failure -> - scope.launch { - eventDispatcher.send( - event = DidFail( - failure = failure, - paymentState = when (val flow = LocalCache.configuration?.flow) { - is Authorization -> flow.initialResponse?.state ?: UNKNOWN - is Tokenization -> flow.initialResponse?.state ?: UNKNOWN - null -> UNKNOWN - } - ) - ) - } - } - LocalCache.configuration = null + private fun complete(result: ProcessOutResult) { viewModel.reset() callback(result.toActivityResult()) } From 80447ba89a1d92de0716703a06ef9e333b4513d0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 7 Apr 2026 12:47:53 +0300 Subject: [PATCH 03/15] AGP 9.1.0 --- build.gradle | 2 +- gradle.properties | 10 ++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f1ae41b6..3c6103c1 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension buildscript { ext { - androidGradlePluginVersion = '8.13.2' + androidGradlePluginVersion = '9.1.0' kotlinVersion = '2.1.20' kspVersion = '2.1.20-1.0.32' dokkaVersion = '1.9.20' diff --git a/gradle.properties b/gradle.properties index 5887c0d4..8a009d8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,3 +22,13 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.nonFinalResIds=true +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false +android.builtInKotlin=false +android.newDsl=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1bc512c2..a9745575 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Oct 05 19:43:19 EEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From fd93e1a2145f2f7881354dde28b723e0ebc1d396 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 7 Apr 2026 12:51:16 +0300 Subject: [PATCH 04/15] Update gradle --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a9745575..e58d4d51 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Oct 05 19:43:19 EEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 22e1672f5bee012f1854e61564c5de75106f9cca Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 7 Apr 2026 17:24:07 +0300 Subject: [PATCH 05/15] Send return redirect type configuration --- .../v2/NativeAlternativePaymentRequestBody.kt | 7 ++++ ...eAlternativePaymentAuthorizationRequest.kt | 3 ++ ...eAlternativePaymentRequestConfiguration.kt | 26 +++++++++++++++ ...veAlternativePaymentTokenizationRequest.kt | 3 ++ .../DefaultCustomerTokensRepository.kt | 2 ++ .../repository/DefaultInvoicesRepository.kt | 2 ++ .../NativeAlternativePaymentInteractor.kt | 32 ++++++++++++++----- 7 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRequestConfiguration.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt index 43500d7d..7e6efaef 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt @@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass internal data class NativeAlternativePaymentRequestBody( @Json(name = "gateway_configuration_id") val gatewayConfigurationId: String, + val configuration: Configuration, val source: String?, @Json(name = "submit_data") val submitData: SubmitData?, @@ -14,6 +15,12 @@ internal data class NativeAlternativePaymentRequestBody( val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? ) { + @JsonClass(generateAdapter = true) + data class Configuration( + @Json(name = "return_redirect_type") + val returnRedirectType: String + ) + @JsonClass(generateAdapter = true) data class SubmitData( val parameters: Map diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt index cbc813fd..ee27e3ad 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt @@ -5,6 +5,8 @@ package com.processout.sdk.api.model.request.napm.v2 * * @param[invoiceId] Invoice identifier. * @param[gatewayConfigurationId] Gateway configuration identifier. + * @param[configuration] Payment configuration. + * __Note:__ Configuration is respected _only on the first_ payment request and ignored on subsequent calls. * @param[source] Payment source. * @param[submitData] Payment payload. * @param[redirectConfirmation] Redirect confirmation. @@ -12,6 +14,7 @@ package com.processout.sdk.api.model.request.napm.v2 data class PONativeAlternativePaymentAuthorizationRequest( val invoiceId: String, val gatewayConfigurationId: String, + val configuration: PONativeAlternativePaymentRequestConfiguration = PONativeAlternativePaymentRequestConfiguration(), val source: String? = null, val submitData: PONativeAlternativePaymentSubmitData? = null, val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRequestConfiguration.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRequestConfiguration.kt new file mode 100644 index 00000000..29bb78ca --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRequestConfiguration.kt @@ -0,0 +1,26 @@ +package com.processout.sdk.api.model.request.napm.v2 + +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRequestConfiguration.ReturnRedirectType.AUTOMATIC +import com.squareup.moshi.JsonClass + +/** + * Payment configuration. + * + * @param[returnRedirectType] Return redirect type. By default [AUTOMATIC]. + */ +data class PONativeAlternativePaymentRequestConfiguration( + val returnRedirectType: ReturnRedirectType = AUTOMATIC +) { + + /** + * Return redirect type. + */ + @JsonClass(generateAdapter = false) + enum class ReturnRedirectType(val rawValue: String) { + /** Redirect result is handled automatically. */ + AUTOMATIC("automatic"), + + /** Redirect result is not processed automatically and should be resolved explicitly. */ + MANUAL("manual") + } +} diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt index 7266e717..9d04abf4 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt @@ -6,6 +6,8 @@ package com.processout.sdk.api.model.request.napm.v2 * @param[customerId] Customer identifier. * @param[customerTokenId] Customer token identifier. * @param[gatewayConfigurationId] Gateway configuration identifier. + * @param[configuration] Payment configuration. + * __Note:__ Configuration is respected _only on the first_ payment request and ignored on subsequent calls. * @param[submitData] Payment payload. * @param[redirectConfirmation] Redirect confirmation. */ @@ -13,6 +15,7 @@ data class PONativeAlternativePaymentTokenizationRequest( val customerId: String, val customerTokenId: String, val gatewayConfigurationId: String, + val configuration: PONativeAlternativePaymentRequestConfiguration = PONativeAlternativePaymentRequestConfiguration(), val submitData: PONativeAlternativePaymentSubmitData? = null, val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt index 6e09402a..753fbe97 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt @@ -2,6 +2,7 @@ package com.processout.sdk.api.repository import com.processout.sdk.api.model.request.* import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody +import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.Configuration import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.SubmitData import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Value @@ -80,6 +81,7 @@ internal class DefaultCustomerTokensRepository( private fun PONativeAlternativePaymentTokenizationRequest.toBody() = NativeAlternativePaymentRequestBody( gatewayConfigurationId = gatewayConfigurationId, + configuration = Configuration(returnRedirectType = configuration.returnRedirectType.rawValue), source = null, submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) }, redirectConfirmation = redirectConfirmation diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt index ad8864ee..16998767 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt @@ -2,6 +2,7 @@ package com.processout.sdk.api.repository import com.processout.sdk.api.model.request.* import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody +import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.Configuration import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.SubmitData import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter @@ -152,6 +153,7 @@ internal class DefaultInvoicesRepository( private fun PONativeAlternativePaymentAuthorizationRequest.toBody() = NativeAlternativePaymentRequestBody( gatewayConfigurationId = gatewayConfigurationId, + configuration = Configuration(returnRedirectType = configuration.returnRedirectType.rawValue), source = source, submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) }, redirectConfirmation = redirectConfirmation diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 38e17650..923f9f1b 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -19,12 +19,10 @@ import coil.request.ImageRequest import coil.request.ImageResult import com.processout.sdk.R import com.processout.sdk.api.dispatcher.POEventDispatcher -import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest -import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRedirectConfirmation -import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData +import com.processout.sdk.api.model.request.napm.v2.* +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRequestConfiguration.ReturnRedirectType import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Companion.phoneNumber import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Companion.string -import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest import com.processout.sdk.api.model.response.POAlternativePaymentMethodResponse import com.processout.sdk.api.model.response.POImageResource import com.processout.sdk.api.model.response.napm.v2.* @@ -149,6 +147,9 @@ internal class NativeAlternativePaymentInteractor( val request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ), source = flow.customerTokenId ) invoicesService.authorize(request) @@ -172,7 +173,10 @@ internal class NativeAlternativePaymentInteractor( val request = PONativeAlternativePaymentTokenizationRequest( customerId = flow.customerId, customerTokenId = flow.customerTokenId, - gatewayConfigurationId = flow.gatewayConfigurationId + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ) ) customerTokensService.tokenize(request) .onSuccess { response -> @@ -442,7 +446,7 @@ internal class NativeAlternativePaymentInteractor( if (stateValue.redirect == null) { val failure = ProcessOutResult.Failure( code = Generic(genericCode = mobileOperationNotSupported), - message = "Headless mode is not supported: missing redirect parameters." + message = "Headless mode is not supported: redirect parameters are missing in the response." ) POLogger.error( message = "Unsupported operation: %s", failure, @@ -843,6 +847,9 @@ internal class NativeAlternativePaymentInteractor( val request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ), submitData = stateValue.fields.toSubmitData(), redirectConfirmation = redirectConfirmation ) @@ -871,6 +878,9 @@ internal class NativeAlternativePaymentInteractor( customerId = flow.customerId, customerTokenId = flow.customerTokenId, gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ), submitData = stateValue.fields.toSubmitData(), redirectConfirmation = redirectConfirmation ) @@ -1015,14 +1025,20 @@ internal class NativeAlternativePaymentInteractor( is Authorization -> invoicesService.authorize( request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, - gatewayConfigurationId = flow.gatewayConfigurationId + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ) ) ).map() is Tokenization -> customerTokensService.tokenize( request = PONativeAlternativePaymentTokenizationRequest( customerId = flow.customerId, customerTokenId = flow.customerTokenId, - gatewayConfigurationId = flow.gatewayConfigurationId + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ) ) ).map() } From 525f7ec7d9364ad616d6fe7c86cc3ccfae4cc66a Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 7 Apr 2026 20:50:38 +0300 Subject: [PATCH 06/15] Parse IDs --- .../napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt | 2 ++ .../napm/v2/PONativeAlternativePaymentMethodDetails.kt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt index fa29e3d0..119c2581 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt @@ -27,12 +27,14 @@ data class PONativeAlternativePaymentAuthorizationResponse( /** * Invoice details. * + * @param[id] Invoice identifier. * @param[amount] Invoice amount. * @param[currency] Invoice currency. */ @Parcelize @JsonClass(generateAdapter = true) data class Invoice( + val id: String, val amount: String, val currency: String ) : Parcelable diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt index 52d48cfd..e6beb1b3 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt @@ -9,12 +9,15 @@ import kotlinx.parcelize.Parcelize /** * Specifies payment method details. * + * @param[gatewayConfigurationId] Gateway configuration identifier. * @param[displayName] Payment method display name. * @param[logo] Image resource for light/dark themes. */ @Parcelize @JsonClass(generateAdapter = true) data class PONativeAlternativePaymentMethodDetails( + @Json(name = "gateway_configuration_id") + val gatewayConfigurationId: String, @Json(name = "display_name") val displayName: String, val logo: POImageResource From f4248f6a27f1cd354b1bf7e02a05fe0463b42ef0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 8 Apr 2026 15:51:30 +0300 Subject: [PATCH 07/15] Add "/apm-payments" endpoint and request/response --- ...eAlternativePaymentUrlResolutionRequest.kt | 37 +++++++++++++++++++ ...PONativeAlternativePaymentCustomerToken.kt | 15 ++++++++ .../v2/PONativeAlternativePaymentInvoice.kt | 19 ++++++++++ ...AlternativePaymentUrlResolutionResponse.kt | 37 +++++++++++++++++++ .../processout/sdk/api/network/InvoicesApi.kt | 7 ++++ 5 files changed, 115 insertions(+) create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentUrlResolutionRequest.kt create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerToken.kt create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentInvoice.kt create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentUrlResolutionRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentUrlResolutionRequest.kt new file mode 100644 index 00000000..0cb05a9c --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentUrlResolutionRequest.kt @@ -0,0 +1,37 @@ +package com.processout.sdk.api.model.request.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.JsonClass + +/** + * Request parameters for redirect URL resolution during native alternative payment. + * + * @param[redirect] Redirect information. + */ +@ProcessOutInternalApi +@JsonClass(generateAdapter = true) +data class PONativeAlternativePaymentUrlResolutionRequest( + val redirect: Redirect +) { + + /** + * Redirect information. + * + * @param[result] Redirect result. + */ + @JsonClass(generateAdapter = true) + data class Redirect( + val result: Result + ) { + + /** + * Redirect result. + * + * @param[url] Result URL. + */ + @JsonClass(generateAdapter = true) + data class Result( + val url: String + ) + } +} diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerToken.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerToken.kt new file mode 100644 index 00000000..5961eabe --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerToken.kt @@ -0,0 +1,15 @@ +package com.processout.sdk.api.model.response.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.JsonClass + +/** + * Customer token details. + * + * @param[id] Customer token. + */ +@ProcessOutInternalApi +@JsonClass(generateAdapter = true) +data class PONativeAlternativePaymentCustomerToken( + val id: String +) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentInvoice.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentInvoice.kt new file mode 100644 index 00000000..c46aa3de --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentInvoice.kt @@ -0,0 +1,19 @@ +package com.processout.sdk.api.model.response.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.JsonClass + +/** + * Invoice details. + * + * @param[id] Invoice identifier. + * @param[amount] Invoice amount. + * @param[currency] Invoice currency. + */ +@ProcessOutInternalApi +@JsonClass(generateAdapter = true) +data class PONativeAlternativePaymentInvoice( + val id: String, + val amount: String, + val currency: String +) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt new file mode 100644 index 00000000..49a6e5e4 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt @@ -0,0 +1,37 @@ +package com.processout.sdk.api.model.response.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Specifies details of native alternative payment after redirect URL resolution. + * + * @param[state] State of native alternative payment. + * @param[paymentMethod] Payment method details. + * @param[invoice] Invoice details if any. + * @param[customerToken] Customer token details if any. + * @param[elements] An ordered list of elements that needs to be rendered on the UI during native alternative payment flow. + * @param[redirect] Indicates required redirect. + */ +@ProcessOutInternalApi +data class PONativeAlternativePaymentUrlResolutionResponse( + val state: PONativeAlternativePaymentState, + val paymentMethod: PONativeAlternativePaymentMethodDetails, + val invoice: PONativeAlternativePaymentInvoice?, + val customerToken: PONativeAlternativePaymentCustomerToken?, + val elements: List?, + val redirect: PONativeAlternativePaymentRedirect? +) + +@JsonClass(generateAdapter = true) +internal data class NativeAlternativePaymentUrlResolutionResponseBody( + val state: PONativeAlternativePaymentState, + @Json(name = "payment_method") + val paymentMethod: PONativeAlternativePaymentMethodDetails, + val invoice: PONativeAlternativePaymentInvoice?, + @Json(name = "customer_token") + val customerToken: PONativeAlternativePaymentCustomerToken?, + val elements: List?, + val redirect: PONativeAlternativePaymentRedirect? +) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt b/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt index e0bf7248..15b058ab 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt @@ -5,8 +5,10 @@ import com.processout.sdk.api.model.request.NativeAPMRequestBody import com.processout.sdk.api.model.request.NativeAlternativePaymentCaptureRequest import com.processout.sdk.api.model.request.POCreateInvoiceRequest import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.* import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentAuthorizationResponseBody +import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentUrlResolutionResponseBody import com.processout.sdk.api.network.HeaderConstants.CLIENT_SECRET import retrofit2.Response import retrofit2.http.* @@ -55,4 +57,9 @@ internal interface InvoicesApi { suspend fun createInvoice( @Body request: POCreateInvoiceRequest ): Response + + @POST("/apm-payments") + suspend fun resolveUrl( + @Body request: PONativeAlternativePaymentUrlResolutionRequest + ): Response } From cdc7ed6692cffdfc7adccce4e96749140dea7bd5 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 8 Apr 2026 16:13:00 +0300 Subject: [PATCH 08/15] resolveUrl() impl in service and repository --- .../repository/DefaultInvoicesRepository.kt | 60 ++++++++++++------- .../sdk/api/repository/InvoicesRepository.kt | 8 +++ .../sdk/api/service/DefaultInvoicesService.kt | 7 +++ .../sdk/api/service/POInvoicesService.kt | 8 +++ 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt index 16998767..30e86d1f 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt @@ -7,11 +7,9 @@ import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequ import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Value +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.* -import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentAuthorizationResponseBody -import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentElement -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement +import com.processout.sdk.api.model.response.napm.v2.* import com.processout.sdk.api.network.HeaderConstants.CLIENT_SECRET import com.processout.sdk.api.network.InvoicesApi import com.processout.sdk.core.* @@ -128,6 +126,11 @@ internal class DefaultInvoicesRepository( override suspend fun createInvoice(request: POCreateInvoiceRequest) = plainApiCall { api.createInvoice(request) }.map() + override suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ) = apiCall { api.resolveUrl(request) } + .map { it.toModel() } + private fun POInvoiceAuthorizationRequest.withDeviceData() = InvoiceAuthorizationRequestWithDeviceData( source = source, @@ -172,28 +175,39 @@ internal class DefaultInvoicesRepository( state = state, invoice = invoice, paymentMethod = paymentMethod, - elements = elements?.map { - when (it) { - is NativeAlternativePaymentElement.Form -> - PONativeAlternativePaymentElement.Form( - parameterDefinitions = it.parameters.parameterDefinitions - ) - is NativeAlternativePaymentElement.CustomerInstruction -> - PONativeAlternativePaymentElement.CustomerInstruction( - instruction = it.instruction - ) - is NativeAlternativePaymentElement.CustomerInstructionGroup -> - PONativeAlternativePaymentElement.CustomerInstructionGroup( - label = it.label, - instructions = it.instructions - ) - NativeAlternativePaymentElement.Unknown -> - PONativeAlternativePaymentElement.Unknown - } - }, + elements = elements?.map { it.toModel() }, redirect = redirect ) + private fun NativeAlternativePaymentUrlResolutionResponseBody.toModel() = + PONativeAlternativePaymentUrlResolutionResponse( + state = state, + paymentMethod = paymentMethod, + invoice = invoice, + customerToken = customerToken, + elements = elements?.map { it.toModel() }, + redirect = redirect + ) + + private fun NativeAlternativePaymentElement.toModel() = + when (this) { + is NativeAlternativePaymentElement.Form -> + PONativeAlternativePaymentElement.Form( + parameterDefinitions = parameters.parameterDefinitions + ) + is NativeAlternativePaymentElement.CustomerInstruction -> + PONativeAlternativePaymentElement.CustomerInstruction( + instruction = instruction + ) + is NativeAlternativePaymentElement.CustomerInstructionGroup -> + PONativeAlternativePaymentElement.CustomerInstructionGroup( + label = label, + instructions = instructions + ) + NativeAlternativePaymentElement.Unknown -> + PONativeAlternativePaymentElement.Unknown + } + private fun ProcessOutResult>.map() = fold( onSuccess = { response -> response.body()?.let { invoice -> diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt index 64f931cf..6a9989ca 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt @@ -5,8 +5,10 @@ import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.* import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.core.ProcessOutCallback import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.annotation.ProcessOutInternalApi @@ -62,4 +64,10 @@ internal interface InvoicesRepository { /** @suppress */ @ProcessOutInternalApi suspend fun createInvoice(request: POCreateInvoiceRequest): ProcessOutResult + + /** @suppress */ + @ProcessOutInternalApi + suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ): ProcessOutResult } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt index c7527f61..82b32057 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt @@ -7,11 +7,13 @@ import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.POInvoice import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethod import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodCapture import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.api.repository.InvoicesRepository import com.processout.sdk.core.* import com.processout.sdk.core.POFailure.Code.Cancelled @@ -244,4 +246,9 @@ internal class DefaultInvoicesService( request: POCreateInvoiceRequest ): ProcessOutResult = repository.createInvoice(request) + + override suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ): ProcessOutResult = + repository.resolveUrl(request) } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt index 36d01917..7dafd043 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt @@ -5,11 +5,13 @@ import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.POInvoice import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethod import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodCapture import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.core.ProcessOutCallback import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.annotation.ProcessOutInternalApi @@ -158,4 +160,10 @@ interface POInvoicesService { /** @suppress */ @ProcessOutInternalApi suspend fun createInvoice(request: POCreateInvoiceRequest): ProcessOutResult + + /** @suppress */ + @ProcessOutInternalApi + suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ): ProcessOutResult } From cca7fad07ef966f7b53d3e967d100693a97a5b7b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 8 Apr 2026 16:52:11 +0300 Subject: [PATCH 09/15] Don't preload images in headless mode --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 923f9f1b..67ea4f93 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -1172,6 +1172,9 @@ internal class NativeAlternativePaymentInteractor( //region Images private suspend fun preloadImages(resources: List) { + if (configuration.redirect?.enableHeadlessMode == true) { + return + } coroutineScope { val urls = resources.flatMap { it.urls() } val deferredResults = urls.map { url -> From 3461e1f2ed830cd26a28053f74443e6d47131669 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 8 Apr 2026 20:26:32 +0300 Subject: [PATCH 10/15] Track event subscriptions in POEventDispatcher --- .../sdk/api/dispatcher/POEventDispatcher.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatcher.kt b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatcher.kt index 4e45a520..9b52ed6e 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatcher.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatcher.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch import java.util.UUID +import kotlin.reflect.KClass /** @suppress */ @ProcessOutInternalApi @@ -31,6 +32,9 @@ class POEventDispatcher { private val _events = MutableSharedFlow() val events = _events.asSharedFlow() + @PublishedApi + internal val subscriptions = mutableSetOf>() + private val _requests = MutableSharedFlow() val requests = _requests.asSharedFlow() @@ -53,15 +57,29 @@ class POEventDispatcher { coroutineScope: CoroutineScope, crossinline onEvent: (T) -> Unit ) { + synchronized(subscriptions) { + subscriptions.add(T::class) + } coroutineScope.launch { - events.filterIsInstance() - .collect { event -> - coroutineContext.ensureActive() - onEvent(event) + try { + events.filterIsInstance() + .collect { event -> + coroutineContext.ensureActive() + onEvent(event) + } + } finally { + synchronized(subscriptions) { + subscriptions.remove(T::class) } + } } } + inline fun hasSubscribers(): Boolean = + synchronized(subscriptions) { + subscriptions.contains(T::class) + } + inline fun subscribeForRequest( coroutineScope: CoroutineScope, crossinline onRequest: (T) -> Unit From ce65ae693d4d74662817daf0c2f496f2a3743c46 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 9 Apr 2026 12:41:03 +0300 Subject: [PATCH 11/15] PODeepLinkReceivedEvent --- .../sdk/api/model/event/PODeepLinkReceivedEvent.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/event/PODeepLinkReceivedEvent.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/event/PODeepLinkReceivedEvent.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/event/PODeepLinkReceivedEvent.kt new file mode 100644 index 00000000..738953e0 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/event/PODeepLinkReceivedEvent.kt @@ -0,0 +1,10 @@ +package com.processout.sdk.api.model.event + +import android.net.Uri +import com.processout.sdk.core.annotation.ProcessOutInternalApi + +/** @suppress */ +@ProcessOutInternalApi +data class PODeepLinkReceivedEvent( + val uri: Uri +) From ced5db7fd91a80e4366ea185888b18463a991d60 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 9 Apr 2026 13:22:24 +0300 Subject: [PATCH 12/15] fun processDeepLink(hostActivity: ComponentActivity, uri: Uri) --- .../redirect/MerchantRedirectActivity.kt | 7 ++-- .../com/processout/sdk/api/ProcessOut.kt | 37 +++++++++++++++++-- .../customtab/POCustomTabRedirectActivity.kt | 8 ++-- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt b/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt index d8f42d67..124ac73a 100644 --- a/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt +++ b/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt @@ -8,10 +8,9 @@ class MerchantRedirectActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - ProcessOut.instance.processDeepLink( - activity = this, - uri = intent.data - ) + intent.data?.let { uri -> + ProcessOut.instance.processDeepLink(hostActivity = this, uri) + } finish() } } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt b/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt index da18c93c..b4197488 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt @@ -5,11 +5,15 @@ package com.processout.sdk.api import android.app.Activity import android.content.Intent import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.lifecycle.lifecycleScope import com.processout.processout_sdk.ProcessOutLegacyAccessor import com.processout.sdk.BuildConfig import com.processout.sdk.api.dispatcher.PODefaultEventDispatchers +import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.dispatcher.POEventDispatchers import com.processout.sdk.api.dispatcher.PONativeAlternativePaymentMethodEventDispatcher +import com.processout.sdk.api.model.event.PODeepLinkReceivedEvent import com.processout.sdk.api.network.ApiConstants import com.processout.sdk.api.preferences.Preferences import com.processout.sdk.api.repository.POCardsRepository @@ -18,9 +22,11 @@ import com.processout.sdk.api.service.POAlternativePaymentMethodsService import com.processout.sdk.api.service.POBrowserCapabilitiesService import com.processout.sdk.api.service.POCustomerTokensService import com.processout.sdk.api.service.POInvoicesService +import com.processout.sdk.core.annotation.ProcessOutInternalApi import com.processout.sdk.core.logger.POLogger import com.processout.sdk.di.* import com.processout.sdk.ui.web.customtab.POCustomTabAuthorizationActivity +import kotlinx.coroutines.launch /** * Entry point to ProcessOut Android SDK. @@ -76,15 +82,40 @@ class ProcessOut private constructor( /** * Processes an incoming deep/app link received by your app. - * Call this method even when [uri] is _null_ to complete the flow correctly. */ + @Deprecated(message = "Use alternative function.") fun processDeepLink(activity: Activity, uri: Uri?) { POLogger.info("Processing deep link: %s", uri) - val intent = Intent(activity, POCustomTabAuthorizationActivity::class.java) + val componentActivity = activity as? ComponentActivity + if (componentActivity != null && uri != null) { + processDeepLink(hostActivity = componentActivity, uri) + } else { + redirectToCustomTabAuthorizationActivity(hostActivity = activity, uri) + } + } + + /** + * Processes an incoming deep/app link received by your app. + */ + fun processDeepLink(hostActivity: ComponentActivity, uri: Uri) { + POLogger.info("Processing deep link: %s", uri) + val eventDispatcher = POEventDispatcher.instance + if (eventDispatcher.hasSubscribers()) { + hostActivity.lifecycleScope.launch { + eventDispatcher.send(PODeepLinkReceivedEvent(uri)) + } + } else { + redirectToCustomTabAuthorizationActivity(hostActivity, uri) + } + } + + @ProcessOutInternalApi + fun redirectToCustomTabAuthorizationActivity(hostActivity: Activity, uri: Uri?) { + val intent = Intent(hostActivity, POCustomTabAuthorizationActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) intent.data = uri - activity.startActivity(intent) + hostActivity.startActivity(intent) } /** diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt index 24001bc9..5c5f4e81 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt @@ -1,8 +1,8 @@ package com.processout.sdk.ui.web.customtab -import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.processout.sdk.api.ProcessOut /** * Redirect activity that receives deep link and starts [POCustomTabAuthorizationActivity] providing return URL. @@ -12,10 +12,8 @@ class POCustomTabRedirectActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Intent(this, POCustomTabAuthorizationActivity::class.java).let { - it.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - intent.data?.let { uri -> it.data = uri } - startActivity(it) + intent.data?.let { uri -> + ProcessOut.instance.processDeepLink(hostActivity = this, uri) } finish() } From d873e851242e3eab93b0c18518831f12e91cced5 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 10 Apr 2026 13:51:10 +0300 Subject: [PATCH 13/15] Handle deep link event in POCustomTabAuthorizationActivity --- .../com/processout/sdk/api/ProcessOut.kt | 22 +++-------------- .../POCustomTabAuthorizationActivity.kt | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt b/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt index b4197488..1033955f 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt @@ -3,7 +3,6 @@ package com.processout.sdk.api import android.app.Activity -import android.content.Intent import android.net.Uri import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope @@ -22,7 +21,6 @@ import com.processout.sdk.api.service.POAlternativePaymentMethodsService import com.processout.sdk.api.service.POBrowserCapabilitiesService import com.processout.sdk.api.service.POCustomerTokensService import com.processout.sdk.api.service.POInvoicesService -import com.processout.sdk.core.annotation.ProcessOutInternalApi import com.processout.sdk.core.logger.POLogger import com.processout.sdk.di.* import com.processout.sdk.ui.web.customtab.POCustomTabAuthorizationActivity @@ -90,7 +88,7 @@ class ProcessOut private constructor( if (componentActivity != null && uri != null) { processDeepLink(hostActivity = componentActivity, uri) } else { - redirectToCustomTabAuthorizationActivity(hostActivity = activity, uri) + POCustomTabAuthorizationActivity.redirect(hostActivity = activity, uri) } } @@ -99,25 +97,11 @@ class ProcessOut private constructor( */ fun processDeepLink(hostActivity: ComponentActivity, uri: Uri) { POLogger.info("Processing deep link: %s", uri) - val eventDispatcher = POEventDispatcher.instance - if (eventDispatcher.hasSubscribers()) { - hostActivity.lifecycleScope.launch { - eventDispatcher.send(PODeepLinkReceivedEvent(uri)) - } - } else { - redirectToCustomTabAuthorizationActivity(hostActivity, uri) + hostActivity.lifecycleScope.launch { + POEventDispatcher.instance.send(event = PODeepLinkReceivedEvent(uri)) } } - @ProcessOutInternalApi - fun redirectToCustomTabAuthorizationActivity(hostActivity: Activity, uri: Uri?) { - val intent = Intent(hostActivity, POCustomTabAuthorizationActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - intent.data = uri - hostActivity.startActivity(intent) - } - /** * Entry point to ProcessOut Android SDK. * Provides configuration and access to services. diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt index c124c87d..19e14221 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt @@ -14,9 +14,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.processout.sdk.R +import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.event.PODeepLinkReceivedEvent import com.processout.sdk.api.service.POBrowserCapabilitiesService.Companion.CHROME_PACKAGE import com.processout.sdk.core.POFailure import com.processout.sdk.core.ProcessOutActivityResult +import com.processout.sdk.core.annotation.ProcessOutInternalApi import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure import com.processout.sdk.ui.web.ActivityResultDispatcher @@ -37,6 +40,17 @@ import kotlinx.coroutines.launch */ class POCustomTabAuthorizationActivity : AppCompatActivity() { + @ProcessOutInternalApi + companion object { + fun redirect(hostActivity: Activity, uri: Uri?) { + val intent = Intent(hostActivity, POCustomTabAuthorizationActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + intent.data = uri + hostActivity.startActivity(intent) + } + } + private val resultDispatcher: ActivityResultDispatcher = WebAuthorizationActivityResultDispatcher private lateinit var configuration: POCustomTabConfiguration @@ -76,6 +90,7 @@ class POCustomTabAuthorizationActivity : AppCompatActivity() { return } + collectDeepLink() lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { viewModel.uiState.collect { handleUiState(it) } @@ -83,6 +98,15 @@ class POCustomTabAuthorizationActivity : AppCompatActivity() { } } + private fun collectDeepLink() { + POEventDispatcher.instance.subscribe( + coroutineScope = lifecycleScope + ) { event -> + // Activity restarts itself with proper intent flags to close the Custom Tab. + redirect(hostActivity = this, event.uri) + } + } + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) From 8518f1dd9ec0f1bd23eb612c4d4746bf35ef90ff Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 10 Apr 2026 14:16:12 +0300 Subject: [PATCH 14/15] Added error code: request.validation.redirect-result-invalid --- sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt b/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt index 77ce2a9e..36a9a65d 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt @@ -147,6 +147,7 @@ class POFailure private constructor() { invalidType("request.validation.invalid-type"), invalidUrl("request.validation.invalid-url"), invalidUser("request.validation.invalid-user"), + invalidRedirectResult("request.validation.redirect-result-invalid"), missingCurrency("request.validation.missing-currency"), missingCustomerInput("gateway.missing-customer-input"), missingDescription("request.validation.missing-description"), From c12085644bf5be744915ed29d9ff41484e2c98d6 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 10 Apr 2026 18:34:20 +0300 Subject: [PATCH 15/15] Collect and handle deep link in interactor --- .../NativeAlternativePaymentInteractor.kt | 67 ++++++++++++++++++- ...NativeAlternativePaymentInteractorState.kt | 1 + 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 67ea4f93..5bbd8c1f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -4,6 +4,7 @@ package com.processout.sdk.ui.napm import android.Manifest import android.app.Application +import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper @@ -19,6 +20,7 @@ import coil.request.ImageRequest import coil.request.ImageResult import com.processout.sdk.R import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.event.PODeepLinkReceivedEvent import com.processout.sdk.api.model.request.napm.v2.* import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRequestConfiguration.ReturnRedirectType import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Companion.phoneNumber @@ -113,6 +115,7 @@ internal class NativeAlternativePaymentInteractor( dispatch(WillStart) dispatchFailure() collectDefaultValues() + collectDeepLink() fetchPaymentDetails() } @@ -540,6 +543,65 @@ internal class NativeAlternativePaymentInteractor( //endregion + //region Deep Link + + private fun collectDeepLink() { + eventDispatcher.subscribe( + coroutineScope = interactorScope + ) { event -> + if (_completion.value !is Awaiting) { + return@subscribe + } + _state.whenNextStep { stateValue -> + if (stateValue.redirect?.type == RedirectType.DEEP_LINK) { + handleDeepLink(event.uri) + } + } + _state.whenPending { stateValue -> + if (stateValue.redirect?.type == RedirectType.DEEP_LINK) { + handleDeepLink(event.uri) + } + } + } + } + + private fun handleDeepLink(uri: Uri) { + interactorScope.launch { + val request = PONativeAlternativePaymentUrlResolutionRequest( + redirect = PONativeAlternativePaymentUrlResolutionRequest.Redirect( + result = PONativeAlternativePaymentUrlResolutionRequest.Redirect.Result( + url = uri.toString() + ) + ) + ) + invoicesService.resolveUrl(request) + .onSuccess { response -> + if (response.state == PENDING && _state.value is Pending) { + return@onSuccess + } + handlePaymentState( + stateValue = initNextStepStateValue( + paymentMethod = response.paymentMethod, + invoice = response.invoice?.map() + ), + paymentState = response.state, + elements = response.elements, + redirect = response.redirect + ) + }.onFailure { failure -> + _completion.update { Failure(failure) } + } + } + } + + private fun PONativeAlternativePaymentInvoice.map() = Invoice( + id = id, + amount = amount, + currency = currency + ) + + //endregion + fun onEvent(event: NativeAlternativePaymentEvent) { when (event) { is FieldValueChanged -> updateFieldValue(event.id, event.value) @@ -997,6 +1059,7 @@ internal class NativeAlternativePaymentInteractor( uuid = uuid, paymentMethod = paymentMethod, invoice = invoice, + redirect = redirect, stepper = null, elements = elements, primaryActionId = ActionId.CONFIRM_PAYMENT, @@ -1152,7 +1215,9 @@ internal class NativeAlternativePaymentInteractor( POLogger.info("Success: payment completed.") dispatch(DidCompletePayment) val successConfiguration = configuration.success - if (successConfiguration == null) { + if (successConfiguration == null || + configuration.redirect?.enableHeadlessMode == true + ) { _completion.update { Success } } else { _state.update { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt index b857db71..1e147e46 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt @@ -63,6 +63,7 @@ internal sealed interface NativeAlternativePaymentInteractorState { val uuid: String, val paymentMethod: PONativeAlternativePaymentMethodDetails, val invoice: Invoice?, + val redirect: PONativeAlternativePaymentRedirect?, val stepper: Stepper?, val elements: List?, val primaryActionId: String?,