From 892da19c25fc85306f995bb63d401ad1338dd6d0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 30 Jul 2026 18:51:55 +0300 Subject: [PATCH 01/15] NativeAlternativePaymentCapturePoller --- .../NativeAlternativePaymentCapturePoller.kt | 133 ++++++++++++++++++ .../NativeAlternativePaymentInteractor.kt | 129 ++++------------- .../napm/NativeAlternativePaymentViewModel.kt | 10 +- 3 files changed, 158 insertions(+), 114 deletions(-) create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt new file mode 100644 index 00000000..2dfd99a7 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -0,0 +1,133 @@ +package com.processout.sdk.ui.napm + +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest +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.PONativeAlternativePaymentState +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.SUCCESS +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentTokenizationResponse +import com.processout.sdk.api.service.POCustomerTokensService +import com.processout.sdk.api.service.POInvoicesService +import com.processout.sdk.core.POFailure.Code.* +import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.fold +import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.core.retry.PORetryStrategy +import com.processout.sdk.core.retry.PORetryStrategy.Exponential +import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Authorization +import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Tokenization +import kotlinx.coroutines.delay + +internal class NativeAlternativePaymentCapturePoller( + private val configuration: PONativeAlternativePaymentConfiguration, + private val invoicesService: POInvoicesService, + private val customerTokensService: POCustomerTokensService, + private val retryStrategy: PORetryStrategy = Exponential( + maxRetries = Int.MAX_VALUE, + initialDelay = 150, + minDelay = 3 * 1000, + maxDelay = 90 * 1000, + factor = 1.45 + ) +) { + + data class CaptureResponse( + val state: PONativeAlternativePaymentState, + val elements: List? + ) + + private var startTimeMillis = 0L + private var elapsedTimeMillis = 0L + + val isStarted: Boolean + get() = startTimeMillis != 0L + + suspend fun start(): ProcessOutResult { + try { + return poll() + } finally { + startTimeMillis = 0L + elapsedTimeMillis = 0L + } + } + + private suspend fun poll(): ProcessOutResult { + startTimeMillis = System.currentTimeMillis() + val iterator = retryStrategy.iterator + while (elapsedTimeMillis <= configuration.paymentConfirmation.timeoutSeconds * 1000) { + val result = call() + POLogger.debug("Attempted to confirm the payment.") + if (!isRetryable(result)) { + return result + } + delay(timeMillis = iterator.next()) + elapsedTimeMillis = System.currentTimeMillis() - startTimeMillis + } + return ProcessOutResult.Failure( + code = Timeout(), + message = "Payment confirmation has timed out." + ) + } + + private suspend fun call(): ProcessOutResult = + when (val flow = configuration.flow) { + is Authorization -> invoicesService.authorize( + request = PONativeAlternativePaymentAuthorizationRequest( + invoiceId = flow.invoiceId, + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = flow.configuration + ) + ).map() + is Tokenization -> customerTokensService.tokenize( + request = PONativeAlternativePaymentTokenizationRequest( + customerId = flow.customerId, + customerTokenId = flow.customerTokenId, + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = flow.configuration + ) + ).map() + } + + private fun isRetryable( + result: ProcessOutResult + ): Boolean = result.fold( + onSuccess = { it.state != SUCCESS }, + onFailure = { + val retryableCodes = listOf( + NetworkUnreachable, + Timeout(), + Internal() + ) + retryableCodes.contains(it.code) + } + ) + + @JvmName(name = "mapFromAuthorizationResult") + private fun ProcessOutResult.map() = + fold( + onSuccess = { + ProcessOutResult.Success( + CaptureResponse( + state = it.state, + elements = it.elements + ) + ) + }, + onFailure = { it } + ) + + @JvmName(name = "mapFromTokenizationResult") + private fun ProcessOutResult.map() = + fold( + onSuccess = { + ProcessOutResult.Success( + CaptureResponse( + state = it.state, + elements = it.elements + ) + ) + }, + onFailure = { it } + ) +} 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 d1625e91..0893d4c6 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 @@ -43,7 +43,6 @@ import com.processout.sdk.core.fold import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess -import com.processout.sdk.core.retry.PORetryStrategy import com.processout.sdk.ui.base.BaseInteractor import com.processout.sdk.ui.core.component.stepper.POStepper import com.processout.sdk.ui.core.state.POImmutableList @@ -82,8 +81,13 @@ internal class NativeAlternativePaymentInteractor( private val customerTokensService: POCustomerTokensService, private val barcodeBitmapProvider: BarcodeBitmapProvider, private val mediaStorageProvider: MediaStorageProvider, - private val captureRetryStrategy: PORetryStrategy, - private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance + private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance, + private var capturePoller: NativeAlternativePaymentCapturePoller = + NativeAlternativePaymentCapturePoller( + configuration = configuration, + invoicesService = invoicesService, + customerTokensService = customerTokensService + ) ) : BaseInteractor() { private val _completion = MutableStateFlow(Awaiting) @@ -101,9 +105,6 @@ internal class NativeAlternativePaymentInteractor( private var latestDefaultValuesRequest: NativeAlternativePaymentDefaultValuesRequest? = null private var latestWillSubmitParametersEvent: WillSubmitParameters? = null - private var captureStartTimestamp = 0L - private var capturePassedTimestamp = 0L - fun start() { if (_state.value !is Idle) { return @@ -122,6 +123,11 @@ internal class NativeAlternativePaymentInteractor( return } this.configuration = configuration + capturePoller = NativeAlternativePaymentCapturePoller( + configuration = configuration, + invoicesService = invoicesService, + customerTokensService = customerTokensService + ) start() } @@ -129,8 +135,6 @@ internal class NativeAlternativePaymentInteractor( interactorScope.coroutineContext.cancelChildren() handler.removeCallbacksAndMessages(null) latestDefaultValuesRequest = null - captureStartTimestamp = 0L - capturePassedTimestamp = 0L _completion.update { Awaiting } _state.update { Idle } } @@ -1078,63 +1082,25 @@ internal class NativeAlternativePaymentInteractor( } private fun capture() { - if (captureStartTimestamp != 0L) { + if (capturePoller.isStarted) { return } updateStepper(activeStepIndex = 1) - captureStartTimestamp = System.currentTimeMillis() interactorScope.launch { - val iterator = captureRetryStrategy.iterator - while (capturePassedTimestamp <= configuration.paymentConfirmation.timeoutSeconds * 1000) { - val result = when (val flow = configuration.flow) { - is Authorization -> invoicesService.authorize( - request = PONativeAlternativePaymentAuthorizationRequest( - invoiceId = flow.invoiceId, - gatewayConfigurationId = flow.gatewayConfigurationId, - configuration = flow.configuration - ) - ).map() - is Tokenization -> customerTokensService.tokenize( - request = PONativeAlternativePaymentTokenizationRequest( - customerId = flow.customerId, - customerTokenId = flow.customerTokenId, - gatewayConfigurationId = flow.gatewayConfigurationId, - configuration = flow.configuration - ) - ).map() - } - POLogger.debug("Attempted to confirm the payment.") - if (isCaptureRetryable(result)) { - delay(iterator.next()) - capturePassedTimestamp = System.currentTimeMillis() - captureStartTimestamp - } else { - captureStartTimestamp = 0L - capturePassedTimestamp = 0L - result.onSuccess { - _state.whenPending { stateValue -> - handleSuccess( - stateValue.copy( - uuid = UUID.randomUUID().toString(), - elements = it.elements - ) + capturePoller.start() + .onSuccess { + val elements = it.elements?.map() + _state.whenPending { stateValue -> + handleSuccess( + stateValue.copy( + uuid = UUID.randomUUID().toString(), + elements = elements ) - } - }.onFailure { failure -> - _completion.update { Failure(failure) } + ) } - return@launch + }.onFailure { failure -> + _completion.update { Failure(failure) } } - } - captureStartTimestamp = 0L - capturePassedTimestamp = 0L - _completion.update { - Failure( - ProcessOutResult.Failure( - code = Timeout(), - message = "Payment confirmation timed out." - ) - ) - } } } @@ -1167,48 +1133,6 @@ internal class NativeAlternativePaymentInteractor( } } - @JvmName(name = "mapFromAuthorizationResult") - private suspend fun ProcessOutResult.map() = - fold( - onSuccess = { - ProcessOutResult.Success( - ProcessingResponse( - state = it.state, - elements = it.elements?.map() - ) - ) - }, - onFailure = { it } - ) - - @JvmName(name = "mapFromTokenizationResult") - private suspend fun ProcessOutResult.map() = - fold( - onSuccess = { - ProcessOutResult.Success( - ProcessingResponse( - state = it.state, - elements = it.elements?.map() - ) - ) - }, - onFailure = { it } - ) - - private fun isCaptureRetryable( - result: ProcessOutResult - ): Boolean = result.fold( - onSuccess = { it.state != SUCCESS }, - onFailure = { - val retryableCodes = listOf( - NetworkUnreachable, - Timeout(), - Internal() - ) - retryableCodes.contains(it.code) - } - ) - private fun handleSuccess(stateValue: PendingStateValue) { POLogger.info("Success: payment completed.") dispatch(DidCompletePayment) @@ -1469,9 +1393,4 @@ internal class NativeAlternativePaymentInteractor( override fun clear() { handler.removeCallbacksAndMessages(null) } - - private data class ProcessingResponse( - val state: PONativeAlternativePaymentState, - val elements: List? - ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt index 71e900ff..a2de55e3 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt @@ -16,7 +16,6 @@ import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentA import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter.* import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter.Otp.Subtype -import com.processout.sdk.core.retry.PORetryStrategy.Exponential import com.processout.sdk.ui.core.state.* import com.processout.sdk.ui.core.state.POActionState.Confirmation import com.processout.sdk.ui.core.transformation.POPhoneNumberVisualTransformation @@ -61,14 +60,7 @@ internal class NativeAlternativePaymentViewModel private constructor( invoicesService = ProcessOut.instance.invoices, customerTokensService = ProcessOut.instance.customerTokens, barcodeBitmapProvider = BarcodeBitmapProvider(), - mediaStorageProvider = MediaStorageProvider(app), - captureRetryStrategy = Exponential( - maxRetries = Int.MAX_VALUE, - initialDelay = 150, - minDelay = 3 * 1000, - maxDelay = 90 * 1000, - factor = 1.45 - ) + mediaStorageProvider = MediaStorageProvider(app) ) ) as T } From 9493cb4d8e7d9c77b79b4e7efb29212ad7641315 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 30 Jul 2026 19:01:30 +0300 Subject: [PATCH 02/15] Update reset() in nAPM interactor --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 2 ++ 1 file changed, 2 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 0893d4c6..4ffc1a23 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 @@ -134,7 +134,9 @@ internal class NativeAlternativePaymentInteractor( fun reset() { interactorScope.coroutineContext.cancelChildren() handler.removeCallbacksAndMessages(null) + paymentState = UNKNOWN latestDefaultValuesRequest = null + latestWillSubmitParametersEvent = null _completion.update { Awaiting } _state.update { Idle } } From d641253ea3d9fc93cc9bc89dd98c2eed41eaae1b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 30 Jul 2026 19:18:10 +0300 Subject: [PATCH 03/15] retryStrategy.newIterator() --- .../api/network/interceptor/RetryInterceptor.kt | 2 +- .../processout/sdk/core/retry/PORetryStrategy.kt | 15 +++++++-------- .../NativeAlternativePaymentMethodViewModel.kt | 2 +- .../napm/NativeAlternativePaymentCapturePoller.kt | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt b/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt index 4096b1ac..906d79b2 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt @@ -19,7 +19,7 @@ internal class RetryInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().addIdempotencyKey() - val iterator = retryStrategy.iterator + val iterator = retryStrategy.newIterator() repeat(retryStrategy.maxRetries - 1) { var response: Response? = null try { diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt b/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt index eb1043da..ba99ba81 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt @@ -53,12 +53,11 @@ sealed class PORetryStrategy( } } - val iterator: Iterator - get() = Iterator( - iterator = generateSequence(initialDelay.toDouble()) { previous -> - previous * factor - }.iterator(), - minDelay = minDelay, - maxDelay = maxDelay - ) + fun newIterator() = Iterator( + iterator = generateSequence(initialDelay.toDouble()) { previous -> + previous * factor + }.iterator(), + minDelay = minDelay, + maxDelay = maxDelay + ) } diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index 2b9e374a..9ae00468 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -531,7 +531,7 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( ) } viewModelScope.launch { - val iterator = captureRetryStrategy.iterator + val iterator = captureRetryStrategy.newIterator() while (capturePassedTimestamp <= options.paymentConfirmationTimeoutSeconds * 1000) { val result = invoicesService.captureNativeAlternativePayment(invoiceId, gatewayConfigurationId) POLogger.debug("Attempted to capture invoice.") diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt index 2dfd99a7..58cb85f1 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -54,7 +54,7 @@ internal class NativeAlternativePaymentCapturePoller( private suspend fun poll(): ProcessOutResult { startTimeMillis = System.currentTimeMillis() - val iterator = retryStrategy.iterator + val iterator = retryStrategy.newIterator() while (elapsedTimeMillis <= configuration.paymentConfirmation.timeoutSeconds * 1000) { val result = call() POLogger.debug("Attempted to confirm the payment.") From 765c1a416c5e2cf844e7812d4acd796277cb703f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 31 Jul 2026 16:10:19 +0300 Subject: [PATCH 04/15] Refactored NativeAlternativePaymentCapturePoller --- .../NativeAlternativePaymentCapturePoller.kt | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt index 58cb85f1..a731cfbf 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -1,5 +1,6 @@ package com.processout.sdk.ui.napm +import android.os.SystemClock import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse @@ -18,6 +19,7 @@ import com.processout.sdk.core.retry.PORetryStrategy.Exponential import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Authorization import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Tokenization import kotlinx.coroutines.delay +import kotlin.math.min internal class NativeAlternativePaymentCapturePoller( private val configuration: PONativeAlternativePaymentConfiguration, @@ -37,32 +39,23 @@ internal class NativeAlternativePaymentCapturePoller( val elements: List? ) - private var startTimeMillis = 0L - private var elapsedTimeMillis = 0L - - val isStarted: Boolean - get() = startTimeMillis != 0L - - suspend fun start(): ProcessOutResult { - try { - return poll() - } finally { - startTimeMillis = 0L - elapsedTimeMillis = 0L - } - } - - private suspend fun poll(): ProcessOutResult { - startTimeMillis = System.currentTimeMillis() + suspend fun poll(): ProcessOutResult { + val timeout = configuration.paymentConfirmation.timeoutSeconds * 1000L + val startTime = SystemClock.elapsedRealtime() val iterator = retryStrategy.newIterator() - while (elapsedTimeMillis <= configuration.paymentConfirmation.timeoutSeconds * 1000) { + while (true) { val result = call() POLogger.debug("Attempted to confirm the payment.") if (!isRetryable(result)) { return result } - delay(timeMillis = iterator.next()) - elapsedTimeMillis = System.currentTimeMillis() - startTimeMillis + val elapsedTime = SystemClock.elapsedRealtime() - startTime + val remainingTime = timeout - elapsedTime + if (remainingTime <= 0) { + break + } + val nextDelay = iterator.next() + delay(timeMillis = min(nextDelay, remainingTime)) } return ProcessOutResult.Failure( code = Timeout(), @@ -93,13 +86,13 @@ internal class NativeAlternativePaymentCapturePoller( result: ProcessOutResult ): Boolean = result.fold( onSuccess = { it.state != SUCCESS }, - onFailure = { + onFailure = { failure -> val retryableCodes = listOf( NetworkUnreachable, Timeout(), Internal() ) - retryableCodes.contains(it.code) + retryableCodes.contains(failure.code) } ) From 4bdbe8f0fedc68ea40147667375472d81f806a74 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 31 Jul 2026 17:06:16 +0300 Subject: [PATCH 05/15] Refactor capture() in interactor --- .../NativeAlternativePaymentInteractor.kt | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 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 4ffc1a23..abd5f23d 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 @@ -104,6 +104,7 @@ internal class NativeAlternativePaymentInteractor( private var paymentState: PONativeAlternativePaymentState = UNKNOWN private var latestDefaultValuesRequest: NativeAlternativePaymentDefaultValuesRequest? = null private var latestWillSubmitParametersEvent: WillSubmitParameters? = null + private var isPollingForCapture = false fun start() { if (_state.value !is Idle) { @@ -1084,25 +1085,28 @@ internal class NativeAlternativePaymentInteractor( } private fun capture() { - if (capturePoller.isStarted) { - return - } + if (isPollingForCapture) return + isPollingForCapture = true updateStepper(activeStepIndex = 1) interactorScope.launch { - capturePoller.start() - .onSuccess { - val elements = it.elements?.map() - _state.whenPending { stateValue -> - handleSuccess( - stateValue.copy( - uuid = UUID.randomUUID().toString(), - elements = elements + try { + capturePoller.poll() + .onSuccess { response -> + val elements = response.elements?.map() + _state.whenPending { stateValue -> + handleSuccess( + stateValue.copy( + uuid = UUID.randomUUID().toString(), + elements = elements + ) ) - ) + } + }.onFailure { failure -> + _completion.update { Failure(failure) } } - }.onFailure { failure -> - _completion.update { Failure(failure) } - } + } finally { + isPollingForCapture = false + } } } From c966f6b64f70dca8e9db4c1c7ad6269b79da2d13 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 3 Aug 2026 13:30:14 +0300 Subject: [PATCH 06/15] Fix POCountdownTimerText --- .../ui/core/component/POCountdownTimerText.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt index 56893a08..ec87f035 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt @@ -2,6 +2,7 @@ package com.processout.sdk.ui.core.component +import android.os.SystemClock import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -22,17 +23,19 @@ fun POCountdownTimerText( textStyle = typography.s15(FontWeight.Medium) ) ) { - var secondsLeft by remember { mutableIntStateOf(timeoutSeconds) } - val formattedText = remember(secondsLeft) { - val minutes = secondsLeft / 60 - val seconds = secondsLeft % 60 + val startTimeMillis = remember { SystemClock.elapsedRealtime() } + var remainingSeconds by remember { mutableIntStateOf(timeoutSeconds) } + val formattedText = remember(remainingSeconds) { + val minutes = remainingSeconds / 60 + val seconds = remainingSeconds % 60 val formattedTime = String.format("%02d:%02d", minutes, seconds) String.format(textFormat, formattedTime) } - LaunchedEffect(secondsLeft) { - if (secondsLeft > 0) { + LaunchedEffect(Unit) { + while (remainingSeconds > 0) { delay(timeMillis = 1000) - secondsLeft -= 1 + val elapsedSeconds = ((SystemClock.elapsedRealtime() - startTimeMillis) / 1000L).toInt() + remainingSeconds = (timeoutSeconds - elapsedSeconds).coerceAtLeast(minimumValue = 0) } } POText( From 9d09de84e03f73ad2d020813b5a5bb2dfbe51d50 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 3 Aug 2026 13:50:35 +0300 Subject: [PATCH 07/15] Use SystemClock.elapsedRealtime() in CardRecognitionSession and legacy nAPM --- ...NativeAlternativePaymentMethodViewModel.kt | 21 ++++++++++--------- .../recognition/CardRecognitionSession.kt | 11 +++++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index 9ae00468..a26f49f5 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -3,6 +3,7 @@ package com.processout.sdk.ui.nativeapm import android.app.Application import android.os.Handler import android.os.Looper +import android.os.SystemClock import android.util.Patterns import android.view.View import android.view.inputmethod.EditorInfo @@ -104,8 +105,8 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( var animateViewTransition = true - private var captureStartTimestamp = 0L - private var capturePassedTimestamp = 0L + private var captureStartTime = 0L + private var captureElapsedTime = 0L private val handler by lazy { Handler(Looper.getMainLooper()) } @@ -521,10 +522,10 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( } private fun capture() { - if (captureStartTimestamp != 0L) { + if (captureStartTime != 0L) { return } - captureStartTimestamp = System.currentTimeMillis() + captureStartTime = SystemClock.elapsedRealtime() options.showPaymentConfirmationProgressIndicatorAfterSeconds?.let { afterSeconds -> showPaymentConfirmationProgressIndicator( afterMillis = TimeUnit.SECONDS.toMillis(afterSeconds.toLong()) @@ -532,15 +533,15 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( } viewModelScope.launch { val iterator = captureRetryStrategy.newIterator() - while (capturePassedTimestamp <= options.paymentConfirmationTimeoutSeconds * 1000) { + while (captureElapsedTime <= options.paymentConfirmationTimeoutSeconds * 1000L) { val result = invoicesService.captureNativeAlternativePayment(invoiceId, gatewayConfigurationId) POLogger.debug("Attempted to capture invoice.") if (isCaptureRetryable(result)) { delay(iterator.next()) - capturePassedTimestamp = System.currentTimeMillis() - captureStartTimestamp + captureElapsedTime = SystemClock.elapsedRealtime() - captureStartTime } else { - captureStartTimestamp = 0L - capturePassedTimestamp = 0L + captureStartTime = 0L + captureElapsedTime = 0L when (result) { is ProcessOutResult.Success -> _uiState.value.doWhenCapture { uiModel -> @@ -552,8 +553,8 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( return@launch } } - captureStartTimestamp = 0L - capturePassedTimestamp = 0L + captureStartTime = 0L + captureElapsedTime = 0L _uiState.value = Failure( ProcessOutResult.Failure( Timeout(), "Payment confirmation timed out." diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/recognition/CardRecognitionSession.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/recognition/CardRecognitionSession.kt index 4c580267..ecb2c5f5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/recognition/CardRecognitionSession.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/recognition/CardRecognitionSession.kt @@ -2,6 +2,7 @@ package com.processout.sdk.ui.card.scanner.recognition import android.app.Application import android.graphics.Bitmap +import android.os.SystemClock import androidx.camera.core.ImageProxy import com.google.android.gms.common.moduleinstall.InstallStatusListener import com.google.android.gms.common.moduleinstall.ModuleInstall @@ -50,7 +51,7 @@ internal class CardRecognitionSession( private val moduleInstallClient = ModuleInstall.getClient(app) private val textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) - private var startTimestamp = 0L + private var startTime = 0L private val recognizedCards = mutableListOf() init { @@ -163,8 +164,8 @@ internal class CardRecognitionSession( val candidates = text.candidates(MIN_CONFIDENCE) val number = numberDetector.firstMatch(candidates) if (number != null) { - if (startTimestamp == 0L) { - startTimestamp = System.currentTimeMillis() + if (startTime == 0L) { + startTime = SystemClock.elapsedRealtime() } val card = POScannedCard( number = number, @@ -178,12 +179,12 @@ internal class CardRecognitionSession( _currentCard.send(card) } } - if (System.currentTimeMillis() - startTimestamp > RECOGNITION_DURATION_MS) { + if (SystemClock.elapsedRealtime() - startTime > RECOGNITION_DURATION_MS) { if (recognizedCards.isNotEmpty()) { sendMostFrequentCard() recognizedCards.clear() } - startTimestamp = 0L + startTime = 0L } imageProxy.close() } From 13b8b235de495add647b69d6caa2e13cb914277c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 3 Aug 2026 13:55:11 +0300 Subject: [PATCH 08/15] Remove unused `redirect` from PendingStateValue --- .../processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 1 - .../sdk/ui/napm/NativeAlternativePaymentInteractorState.kt | 1 - 2 files changed, 2 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 abd5f23d..72617298 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 @@ -1068,7 +1068,6 @@ internal class NativeAlternativePaymentInteractor( uuid = uuid, paymentMethod = paymentMethod, invoice = invoice, - redirect = redirect, stepper = null, elements = elements, primaryActionId = ActionId.CONFIRM_PAYMENT, 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 86514c14..f3e0e8d6 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,7 +63,6 @@ internal sealed interface NativeAlternativePaymentInteractorState { val uuid: String, val paymentMethod: PONativeAlternativePaymentMethodDetails, val invoice: Invoice?, - val redirect: Redirect?, val stepper: Stepper?, val elements: List?, val primaryActionId: String?, From ed7115afc93239b78bbb0d22bef160883e639429 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 4 Aug 2026 15:28:09 +0300 Subject: [PATCH 09/15] NativeAlternativePaymentCapturePoller: resetBackoff() --- .../NativeAlternativePaymentCapturePoller.kt | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt index a731cfbf..3ac5c784 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.processout.sdk.ui.napm import android.os.SystemClock @@ -18,7 +20,10 @@ import com.processout.sdk.core.retry.PORetryStrategy import com.processout.sdk.core.retry.PORetryStrategy.Exponential import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Authorization import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Tokenization -import kotlinx.coroutines.delay +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.selects.onTimeout +import kotlinx.coroutines.selects.select import kotlin.math.min internal class NativeAlternativePaymentCapturePoller( @@ -39,13 +44,19 @@ internal class NativeAlternativePaymentCapturePoller( val elements: List? ) + private var backoffIterator = retryStrategy.newIterator() + private val backoffResetSignal = Channel(capacity = Channel.CONFLATED) + suspend fun poll(): ProcessOutResult { val timeout = configuration.paymentConfirmation.timeoutSeconds * 1000L val startTime = SystemClock.elapsedRealtime() - val iterator = retryStrategy.newIterator() + backoffIterator = retryStrategy.newIterator() + while (backoffResetSignal.tryReceive().isSuccess) { + // Discard stale signals. + } while (true) { val result = call() - POLogger.debug("Attempted to confirm the payment.") + POLogger.debug("Attempted to capture the payment.") if (!isRetryable(result)) { return result } @@ -54,8 +65,14 @@ internal class NativeAlternativePaymentCapturePoller( if (remainingTime <= 0) { break } - val nextDelay = iterator.next() - delay(timeMillis = min(nextDelay, remainingTime)) + val waitTime = min(backoffIterator.next(), remainingTime) + select { + onTimeout(timeMillis = waitTime) {} + backoffResetSignal.onReceive { + backoffIterator = retryStrategy.newIterator() + POLogger.debug("Capture polling backoff has been reset.") + } + } } return ProcessOutResult.Failure( code = Timeout(), @@ -63,6 +80,10 @@ internal class NativeAlternativePaymentCapturePoller( ) } + fun resetBackoff() { + backoffResetSignal.trySend(Unit) + } + private suspend fun call(): ProcessOutResult = when (val flow = configuration.flow) { is Authorization -> invoicesService.authorize( From 77f613757ad063e3a6965f54565e257d69bfcefb Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 4 Aug 2026 15:29:16 +0300 Subject: [PATCH 10/15] Added "androidx.lifecycle:lifecycle-process" to UI module --- ui/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/build.gradle b/ui/build.gradle index f30cdb05..19fb9381 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -92,6 +92,7 @@ dependencies { api "androidx.activity:activity-compose:$androidxActivityVersion" api "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion" + implementation "androidx.lifecycle:lifecycle-process:$androidxLifecycleVersion" implementation "androidx.camera:camera-camera2:$androidxCameraVersion" implementation "androidx.camera:camera-lifecycle:$androidxCameraVersion" From 4ae2e8a5e705c20fdfcfd5815a461e7df4847036 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 4 Aug 2026 15:59:52 +0300 Subject: [PATCH 11/15] reorder --- .../sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt index 3ac5c784..3e614537 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -48,8 +48,8 @@ internal class NativeAlternativePaymentCapturePoller( private val backoffResetSignal = Channel(capacity = Channel.CONFLATED) suspend fun poll(): ProcessOutResult { - val timeout = configuration.paymentConfirmation.timeoutSeconds * 1000L val startTime = SystemClock.elapsedRealtime() + val timeout = configuration.paymentConfirmation.timeoutSeconds * 1000L backoffIterator = retryStrategy.newIterator() while (backoffResetSignal.tryReceive().isSuccess) { // Discard stale signals. From a31d51c89c0fab3d9088a8282c0893646cdc82f7 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 4 Aug 2026 16:03:30 +0300 Subject: [PATCH 12/15] Reset capture polling backoff when app returned to foreground --- .../NativeAlternativePaymentInteractor.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 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 72617298..c2ae4338 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 @@ -14,6 +14,9 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.core.os.postDelayed import androidx.core.text.isDigitsOnly +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner import coil.imageLoader import coil.request.CachePolicy import coil.request.ImageRequest @@ -88,7 +91,7 @@ internal class NativeAlternativePaymentInteractor( invoicesService = invoicesService, customerTokensService = customerTokensService ) -) : BaseInteractor() { +) : BaseInteractor(), DefaultLifecycleObserver { private val _completion = MutableStateFlow(Awaiting) val completion = _completion.asStateFlow() @@ -104,7 +107,11 @@ internal class NativeAlternativePaymentInteractor( private var paymentState: PONativeAlternativePaymentState = UNKNOWN private var latestDefaultValuesRequest: NativeAlternativePaymentDefaultValuesRequest? = null private var latestWillSubmitParametersEvent: WillSubmitParameters? = null - private var isPollingForCapture = false + private var isCapturePolling = false + + init { + ProcessLifecycleOwner.get().lifecycle.addObserver(this) + } fun start() { if (_state.value !is Idle) { @@ -1084,8 +1091,10 @@ internal class NativeAlternativePaymentInteractor( } private fun capture() { - if (isPollingForCapture) return - isPollingForCapture = true + if (isCapturePolling) { + return + } + isCapturePolling = true updateStepper(activeStepIndex = 1) interactorScope.launch { try { @@ -1104,11 +1113,18 @@ internal class NativeAlternativePaymentInteractor( _completion.update { Failure(failure) } } } finally { - isPollingForCapture = false + isCapturePolling = false } } } + override fun onStart(owner: LifecycleOwner) { + if (isCapturePolling) { + POLogger.debug("App returned to foreground: resetting capture polling backoff.") + capturePoller.resetBackoff() + } + } + private fun updateStepper(activeStepIndex: Int) { _state.whenPending { stateValue -> _state.update { @@ -1396,6 +1412,7 @@ internal class NativeAlternativePaymentInteractor( } override fun clear() { + ProcessLifecycleOwner.get().lifecycle.removeObserver(this) handler.removeCallbacksAndMessages(null) } } From 6ecabcc7d6e81a9cebec11238e23f4371b4ca25b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 4 Aug 2026 16:18:24 +0300 Subject: [PATCH 13/15] BackoffIterator --- .../sdk/api/network/interceptor/RetryInterceptor.kt | 4 ++-- .../com/processout/sdk/core/retry/PORetryStrategy.kt | 10 +++++----- .../NativeAlternativePaymentMethodViewModel.kt | 4 ++-- .../ui/napm/NativeAlternativePaymentCapturePoller.kt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt b/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt index 906d79b2..090ca76a 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt @@ -19,7 +19,7 @@ internal class RetryInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().addIdempotencyKey() - val iterator = retryStrategy.newIterator() + val backoffIterator = retryStrategy.newBackoffIterator() repeat(retryStrategy.maxRetries - 1) { var response: Response? = null try { @@ -34,7 +34,7 @@ internal class RetryInterceptor( // network issue, retry } response?.body?.close() - Thread.sleep(iterator.next()) + Thread.sleep(backoffIterator.next()) } return chain.proceed(request) } diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt b/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt index ba99ba81..8ba1e696 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt @@ -38,11 +38,11 @@ sealed class PORetryStrategy( factor = factor ) - class Iterator( - private val iterator: kotlin.collections.Iterator, + class BackoffIterator( + private val iterator: Iterator, private val minDelay: Long, private val maxDelay: Long - ) : kotlin.collections.Iterator { + ) : Iterator { override fun hasNext(): Boolean = iterator.hasNext() @@ -53,8 +53,8 @@ sealed class PORetryStrategy( } } - fun newIterator() = Iterator( - iterator = generateSequence(initialDelay.toDouble()) { previous -> + fun newBackoffIterator() = BackoffIterator( + iterator = generateSequence(seed = initialDelay.toDouble()) { previous -> previous * factor }.iterator(), minDelay = minDelay, diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index a26f49f5..495e4eb8 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -532,12 +532,12 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( ) } viewModelScope.launch { - val iterator = captureRetryStrategy.newIterator() + val backoffIterator = captureRetryStrategy.newBackoffIterator() while (captureElapsedTime <= options.paymentConfirmationTimeoutSeconds * 1000L) { val result = invoicesService.captureNativeAlternativePayment(invoiceId, gatewayConfigurationId) POLogger.debug("Attempted to capture invoice.") if (isCaptureRetryable(result)) { - delay(iterator.next()) + delay(timeMillis = backoffIterator.next()) captureElapsedTime = SystemClock.elapsedRealtime() - captureStartTime } else { captureStartTime = 0L diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt index 3e614537..f157a07c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -44,13 +44,13 @@ internal class NativeAlternativePaymentCapturePoller( val elements: List? ) - private var backoffIterator = retryStrategy.newIterator() + private var backoffIterator = retryStrategy.newBackoffIterator() private val backoffResetSignal = Channel(capacity = Channel.CONFLATED) suspend fun poll(): ProcessOutResult { val startTime = SystemClock.elapsedRealtime() val timeout = configuration.paymentConfirmation.timeoutSeconds * 1000L - backoffIterator = retryStrategy.newIterator() + backoffIterator = retryStrategy.newBackoffIterator() while (backoffResetSignal.tryReceive().isSuccess) { // Discard stale signals. } @@ -69,7 +69,7 @@ internal class NativeAlternativePaymentCapturePoller( select { onTimeout(timeMillis = waitTime) {} backoffResetSignal.onReceive { - backoffIterator = retryStrategy.newIterator() + backoffIterator = retryStrategy.newBackoffIterator() POLogger.debug("Capture polling backoff has been reset.") } } From 8be7e32d7a905b31d8cbb40c0d8398428577ff7b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 4 Aug 2026 16:34:33 +0300 Subject: [PATCH 14/15] seedDelay --- .../sdk/api/network/interceptor/RetryInterceptor.kt | 2 +- .../com/processout/sdk/core/retry/PORetryStrategy.kt | 12 ++++++------ .../NativeAlternativePaymentMethodViewModel.kt | 2 +- .../ui/napm/NativeAlternativePaymentCapturePoller.kt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt b/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt index 090ca76a..58327b3f 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/network/interceptor/RetryInterceptor.kt @@ -11,7 +11,7 @@ import java.util.UUID internal class RetryInterceptor( private val retryStrategy: PORetryStrategy = Exponential( maxRetries = 4, - initialDelay = 100, + seedDelay = 100, maxDelay = 1000, factor = 3.0 ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt b/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt index 8ba1e696..7e450263 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/core/retry/PORetryStrategy.kt @@ -7,7 +7,7 @@ import kotlin.math.roundToLong @ProcessOutInternalApi sealed class PORetryStrategy( val maxRetries: Int, - private val initialDelay: Long, + private val seedDelay: Long, private val minDelay: Long, private val maxDelay: Long, private val factor: Double @@ -18,7 +18,7 @@ sealed class PORetryStrategy( delay: Long ) : PORetryStrategy( maxRetries = maxRetries, - initialDelay = delay, + seedDelay = delay, minDelay = delay, maxDelay = delay, factor = 1.0 @@ -26,13 +26,13 @@ sealed class PORetryStrategy( class Exponential( maxRetries: Int, - initialDelay: Long, - minDelay: Long = initialDelay, + seedDelay: Long, + minDelay: Long = seedDelay, maxDelay: Long, factor: Double ) : PORetryStrategy( maxRetries = maxRetries, - initialDelay = initialDelay, + seedDelay = seedDelay, minDelay = minDelay, maxDelay = maxDelay, factor = factor @@ -54,7 +54,7 @@ sealed class PORetryStrategy( } fun newBackoffIterator() = BackoffIterator( - iterator = generateSequence(seed = initialDelay.toDouble()) { previous -> + iterator = generateSequence(seed = seedDelay.toDouble()) { previous -> previous * factor }.iterator(), minDelay = minDelay, diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index 495e4eb8..d599d950 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -80,7 +80,7 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( eventDispatcher = PODefaultEventDispatchers.defaultNativeAlternativePaymentMethod, captureRetryStrategy = Exponential( maxRetries = Int.MAX_VALUE, - initialDelay = 150, + seedDelay = 150, minDelay = 3 * 1000, maxDelay = 90 * 1000, factor = 1.45 diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt index f157a07c..82e879f5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -32,7 +32,7 @@ internal class NativeAlternativePaymentCapturePoller( private val customerTokensService: POCustomerTokensService, private val retryStrategy: PORetryStrategy = Exponential( maxRetries = Int.MAX_VALUE, - initialDelay = 150, + seedDelay = 150, minDelay = 3 * 1000, maxDelay = 90 * 1000, factor = 1.45 From d2583e67e2247f33478dcc2d7dca94639c400337 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 5 Aug 2026 15:43:20 +0300 Subject: [PATCH 15/15] Pass `initialElapsedRealtime` to `POCountdownTimerText` from interactor to sync the timer with actual polling instead of UI recomposition --- .../sdk/ui/core/component/POCountdownTimerText.kt | 6 +++--- .../processout/sdk/ui/core/component/stepper/POStepper.kt | 4 +++- .../sdk/ui/core/component/stepper/POVerticalStepper.kt | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt index ec87f035..6fdbb74e 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POCountdownTimerText.kt @@ -17,13 +17,13 @@ import kotlinx.coroutines.delay fun POCountdownTimerText( textFormat: String, timeoutSeconds: Int, + initialElapsedRealtime: Long, modifier: Modifier = Modifier, style: POText.Style = POText.Style( color = colors.text.primary, textStyle = typography.s15(FontWeight.Medium) ) ) { - val startTimeMillis = remember { SystemClock.elapsedRealtime() } var remainingSeconds by remember { mutableIntStateOf(timeoutSeconds) } val formattedText = remember(remainingSeconds) { val minutes = remainingSeconds / 60 @@ -33,9 +33,9 @@ fun POCountdownTimerText( } LaunchedEffect(Unit) { while (remainingSeconds > 0) { - delay(timeMillis = 1000) - val elapsedSeconds = ((SystemClock.elapsedRealtime() - startTimeMillis) / 1000L).toInt() + val elapsedSeconds = ((SystemClock.elapsedRealtime() - initialElapsedRealtime) / 1000L).toInt() remainingSeconds = (timeoutSeconds - elapsedSeconds).coerceAtLeast(minimumValue = 0) + delay(timeMillis = 1000) } } POText( diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POStepper.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POStepper.kt index 0a83ed1c..04a0e937 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POStepper.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POStepper.kt @@ -1,5 +1,6 @@ package com.processout.sdk.ui.core.component.stepper +import android.os.SystemClock import androidx.compose.runtime.Composable import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -22,7 +23,8 @@ object POStepper { data class CountdownTimerText( val textFormat: String, - val timeoutSeconds: Int + val timeoutSeconds: Int, + val initialElapsedRealtime: Long = SystemClock.elapsedRealtime() ) } diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POVerticalStepper.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POVerticalStepper.kt index 86f5d572..ee62d5f0 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POVerticalStepper.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/stepper/POVerticalStepper.kt @@ -104,6 +104,7 @@ fun POVerticalStepper( POCountdownTimerText( textFormat = description.textFormat, timeoutSeconds = description.timeoutSeconds, + initialElapsedRealtime = description.initialElapsedRealtime, modifier = Modifier .fillMaxWidth() .padding(vertical = spacing.space4),