From 434b8f4351927b95c23107c6a6f54610f3e2ba32 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 25 Feb 2026 15:36:07 +0200 Subject: [PATCH 1/3] Support full headless mode --- .../NativeAlternativePaymentInteractor.kt | 3 +- .../PONativeAlternativePaymentLauncher.kt | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) 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 421a6908..35752614 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 @@ -965,7 +965,8 @@ internal class NativeAlternativePaymentInteractor( ) _state.update { Pending(pendingStateValue) } enablePendingSecondaryAction() - if (pendingStateValue.elements.isNullOrEmpty() || + if (configuration.redirect?.enableHeadlessMode == true || + pendingStateValue.elements.isNullOrEmpty() || configuration.paymentConfirmation.confirmButton == null ) { capture() 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 e8e4d57f..7616667c 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 @@ -4,10 +4,13 @@ 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.lifecycle.Lifecycle 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 @@ -41,6 +44,7 @@ import kotlinx.coroutines.launch * Launcher that starts [NativeAlternativePaymentActivity] and provides the result. */ class PONativeAlternativePaymentLauncher private constructor( + private val hostActivity: ComponentActivity, private val app: Application, private val scope: CoroutineScope, private val launcher: ActivityResultLauncher, @@ -52,6 +56,19 @@ class PONativeAlternativePaymentLauncher private constructor( private val customerTokensService: POCustomerTokensService = ProcessOut.instance.customerTokens ) { + 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 { @@ -68,6 +85,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + hostActivity = from.requireActivity(), app = from.requireActivity().application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( @@ -96,6 +114,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + hostActivity = from.requireActivity(), app = from.requireActivity().application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( @@ -121,6 +140,7 @@ class PONativeAlternativePaymentLauncher private constructor( from: Fragment, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + hostActivity = from.requireActivity(), app = from.requireActivity().application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( @@ -146,6 +166,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + hostActivity = from, app = from.application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( @@ -175,6 +196,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + hostActivity = from, app = from.application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( @@ -201,6 +223,7 @@ class PONativeAlternativePaymentLauncher private constructor( from: ComponentActivity, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + hostActivity = from, app = from.application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( @@ -225,10 +248,27 @@ class PONativeAlternativePaymentLauncher private constructor( } init { + collectViewModelCompletion() dispatchEvents() dispatchDefaultValues() } + private fun collectViewModelCompletion() { + hostActivity.lifecycleScope.launch { + hostActivity.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) + else -> {} + } + } + } + } + } + private fun dispatchEvents() { eventDispatcher.subscribe( coroutineScope = scope @@ -348,14 +388,11 @@ class PONativeAlternativePaymentLauncher private constructor( ) { when (state) { NEXT_STEP_REQUIRED -> handleNextStep(redirect, configuration) - PENDING -> launchActivity(configuration) - SUCCESS -> - if (configuration.success != null) { - launchActivity(configuration) - } else { - POLogger.info("Success: payment completed.") - completeHeadlessMode(result = ProcessOutResult.Success(value = POUnit)) - } + PENDING -> viewModel.start(configuration) + SUCCESS -> { + POLogger.info("Success: payment completed.") + completeHeadlessMode(result = ProcessOutResult.Success(value = POUnit)) + } UNKNOWN -> { val failure = ProcessOutResult.Failure( code = Internal(), @@ -482,6 +519,7 @@ class PONativeAlternativePaymentLauncher private constructor( } } LocalCache.configuration = null + viewModel.reset() callback(result.toActivityResult()) } } From ab41443ce1ae58b8ee7146cce368bcff7d9d49cb Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 25 Feb 2026 16:08:26 +0200 Subject: [PATCH 2/3] Updated doc --- .../sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 aaabe67c..25aceadd 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 @@ -392,8 +392,9 @@ data class PONativeAlternativePaymentConfiguration( * * @param[returnUrl] Deep link return URL. Required for the flows that include web redirect. * @param[enableHeadlessMode] Enables headless mode. - * The web redirect will be handled directly when it's the first step in the flow, - * and if it's the only required step it will complete the flow without starting the bottom sheet. + * The redirect (web or deep link) will be handled directly when it's the first step in the flow, without starting the bottom sheet. + * It will also capture the payment in the background when it's required by the flow. + * __Note:__ use only with flows that do not require user input or instructions in the native UI. */ @Parcelize data class RedirectConfiguration( From 7e0b3fda46fa1a92e2256e0a38055c9adae1e151 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 25 Feb 2026 17:10:22 +0200 Subject: [PATCH 3/3] Simplify factory methods of launcher --- .../PONativeAlternativePaymentLauncher.kt | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) 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 7616667c..280aee0c 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,7 +1,6 @@ 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 @@ -45,10 +44,7 @@ import kotlinx.coroutines.launch */ class PONativeAlternativePaymentLauncher private constructor( private val hostActivity: ComponentActivity, - private val app: Application, - private val scope: CoroutineScope, private val launcher: ActivityResultLauncher, - private val activityOptions: ActivityOptionsCompat, private val delegate: PONativeAlternativePaymentDelegate, private val callback: (ProcessOutActivityResult) -> Unit, private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance, @@ -56,6 +52,13 @@ class PONativeAlternativePaymentLauncher private constructor( private val customerTokensService: POCustomerTokensService = ProcessOut.instance.customerTokens ) { + 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, @@ -86,13 +89,10 @@ class PONativeAlternativePaymentLauncher private constructor( callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( hostActivity = from.requireActivity(), - app = from.requireActivity().application, - scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), callback ), - activityOptions = createActivityOptions(from.requireContext()), delegate = delegate, callback = callback ).apply { @@ -115,13 +115,10 @@ class PONativeAlternativePaymentLauncher private constructor( callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( hostActivity = from.requireActivity(), - app = from.requireActivity().application, - scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), callback ), - activityOptions = createActivityOptions(from.requireContext()), delegate = object : PONativeAlternativePaymentDelegate {}, callback = callback ).apply { @@ -141,13 +138,10 @@ class PONativeAlternativePaymentLauncher private constructor( callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( hostActivity = from.requireActivity(), - app = from.requireActivity().application, - scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), callback ), - activityOptions = createActivityOptions(from.requireContext()), delegate = object : PONativeAlternativePaymentDelegate {}, callback = callback ).apply { @@ -167,14 +161,11 @@ class PONativeAlternativePaymentLauncher private constructor( callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( hostActivity = from, - app = from.application, - scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), from.activityResultRegistry, callback ), - activityOptions = createActivityOptions(from), delegate = delegate, callback = callback ).apply { @@ -197,14 +188,11 @@ class PONativeAlternativePaymentLauncher private constructor( callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( hostActivity = from, - app = from.application, - scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), from.activityResultRegistry, callback ), - activityOptions = createActivityOptions(from), delegate = object : PONativeAlternativePaymentDelegate {}, callback = callback ).apply { @@ -224,14 +212,11 @@ class PONativeAlternativePaymentLauncher private constructor( callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( hostActivity = from, - app = from.application, - scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), from.activityResultRegistry, callback ), - activityOptions = createActivityOptions(from), delegate = object : PONativeAlternativePaymentDelegate {}, callback = callback ).apply { @@ -240,11 +225,6 @@ class PONativeAlternativePaymentLauncher private constructor( callback = ::handleWebRedirect ) } - - private fun createActivityOptions(context: Context) = - ActivityOptionsCompat.makeCustomAnimation( - context, R.anim.po_slide_in_vertical, 0 - ) } init {