From 370f984305c96da020498178643ed6c277de69e4 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 18:52:06 +0300 Subject: [PATCH 1/8] POCardUpdateDelegate --- .../ui/card/update/POCardUpdateDelegate.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateDelegate.kt diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateDelegate.kt new file mode 100644 index 000000000..b3b4f7b13 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateDelegate.kt @@ -0,0 +1,23 @@ +package com.processout.sdk.ui.card.update + +import com.processout.sdk.api.model.event.POCardUpdateEvent +import com.processout.sdk.core.ProcessOutResult + +/** + * Delegate that allows to handle events during card update. + */ +interface POCardUpdateDelegate { + + /** + * Invoked on card update lifecycle events. + */ + fun onEvent(event: POCardUpdateEvent) {} + + /** + * Allows to decide whether the card update should continue or complete after the failure. + * Returns _true_ by default. + */ + suspend fun shouldContinue( + failure: ProcessOutResult.Failure + ): Boolean = true +} From 720571307717450650584b0328c854e69dc64e99 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 19:04:33 +0300 Subject: [PATCH 2/8] Update POCardUpdateLauncher --- .../POCardUpdateShouldContinueRequest.kt | 5 +- .../POCardUpdateShouldContinueResponse.kt | 5 +- .../ui/card/update/POCardUpdateLauncher.kt | 81 ++++++++++++++++++- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardUpdateShouldContinueRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardUpdateShouldContinueRequest.kt index 90182d859..2fe46fe81 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardUpdateShouldContinueRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardUpdateShouldContinueRequest.kt @@ -1,5 +1,6 @@ package com.processout.sdk.api.model.request +import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.annotation.ProcessOutInternalApi import java.util.UUID @@ -14,5 +15,5 @@ import java.util.UUID data class POCardUpdateShouldContinueRequest @ProcessOutInternalApi constructor( val cardId: String, val failure: ProcessOutResult.Failure, - val uuid: UUID = UUID.randomUUID() -) + override val uuid: UUID = UUID.randomUUID() +) : POEventDispatcher.Request diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardUpdateShouldContinueResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardUpdateShouldContinueResponse.kt index 7471f40ea..56e682160 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardUpdateShouldContinueResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardUpdateShouldContinueResponse.kt @@ -1,5 +1,6 @@ package com.processout.sdk.api.model.response +import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.model.request.POCardUpdateShouldContinueRequest import com.processout.sdk.core.ProcessOutResult import java.util.UUID @@ -13,10 +14,10 @@ import java.util.UUID * @param[shouldContinue] Boolean that indicates whether the flow should continue or complete after the [failure]. */ data class POCardUpdateShouldContinueResponse internal constructor( - val uuid: UUID, + override val uuid: UUID, val failure: ProcessOutResult.Failure, val shouldContinue: Boolean -) +) : POEventDispatcher.Response /** * Creates [POCardUpdateShouldContinueResponse] from [POCardUpdateShouldContinueRequest]. diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateLauncher.kt index 8102031e5..eb2860501 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/POCardUpdateLauncher.kt @@ -5,16 +5,26 @@ import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.processout.sdk.R +import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.event.POCardUpdateEvent +import com.processout.sdk.api.model.request.POCardUpdateShouldContinueRequest import com.processout.sdk.api.model.response.POCard +import com.processout.sdk.api.model.response.toResponse import com.processout.sdk.core.ProcessOutActivityResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Launcher that starts [CardUpdateActivity] and provides the result. */ class POCardUpdateLauncher private constructor( + private val scope: CoroutineScope, private val launcher: ActivityResultLauncher, - private val activityOptions: ActivityOptionsCompat + private val activityOptions: ActivityOptionsCompat, + private val delegate: POCardUpdateDelegate, + private val eventDispatcher: POEventDispatcher = POEventDispatcher ) { companion object { @@ -24,13 +34,34 @@ class POCardUpdateLauncher private constructor( */ fun create( from: Fragment, + delegate: POCardUpdateDelegate, callback: (ProcessOutActivityResult) -> Unit ) = POCardUpdateLauncher( + scope = from.lifecycleScope, launcher = from.registerForActivityResult( CardUpdateActivityContract(), callback ), - activityOptions = createActivityOptions(from.requireContext()) + activityOptions = createActivityOptions(from.requireContext()), + delegate = delegate + ) + + /** + * Creates the launcher from Fragment. + * __Note:__ Required to call in _onCreate()_ to register for activity result. + */ + @Deprecated(message = "Use alternative function.") + fun create( + from: Fragment, + callback: (ProcessOutActivityResult) -> Unit + ) = POCardUpdateLauncher( + scope = from.lifecycleScope, + launcher = from.registerForActivityResult( + CardUpdateActivityContract(), + callback + ), + activityOptions = createActivityOptions(from.requireContext()), + delegate = object : POCardUpdateDelegate {} ) /** @@ -39,14 +70,36 @@ class POCardUpdateLauncher private constructor( */ fun create( from: ComponentActivity, + delegate: POCardUpdateDelegate, callback: (ProcessOutActivityResult) -> Unit ) = POCardUpdateLauncher( + scope = from.lifecycleScope, launcher = from.registerForActivityResult( CardUpdateActivityContract(), from.activityResultRegistry, callback ), - activityOptions = createActivityOptions(from) + activityOptions = createActivityOptions(from), + delegate = delegate + ) + + /** + * Creates the launcher from Activity. + * __Note:__ Required to call in _onCreate()_ to register for activity result. + */ + @Deprecated(message = "Use alternative function.") + fun create( + from: ComponentActivity, + callback: (ProcessOutActivityResult) -> Unit + ) = POCardUpdateLauncher( + scope = from.lifecycleScope, + launcher = from.registerForActivityResult( + CardUpdateActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(from), + delegate = object : POCardUpdateDelegate {} ) private fun createActivityOptions(context: Context) = @@ -55,6 +108,28 @@ class POCardUpdateLauncher private constructor( ) } + init { + dispatchEvents() + dispatchShouldContinue() + } + + private fun dispatchEvents() { + eventDispatcher.subscribe( + coroutineScope = scope + ) { delegate.onEvent(it) } + } + + private fun dispatchShouldContinue() { + eventDispatcher.subscribeForRequest( + coroutineScope = scope + ) { request -> + scope.launch { + val shouldContinue = delegate.shouldContinue(request.failure) + eventDispatcher.send(request.toResponse(shouldContinue)) + } + } + } + /** * Launches the activity. */ From 7d3ca8f14b2ec553905658b5cc58c12a31d31e4c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 19:14:59 +0300 Subject: [PATCH 3/8] legacyEventDispatcher --- .../sdk/ui/card/update/CardUpdateViewModel.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt index 089a52035..c2441b099 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.processout.sdk.R import com.processout.sdk.api.ProcessOut +import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.dispatcher.card.update.PODefaultCardUpdateEventDispatcher import com.processout.sdk.api.model.event.POCardUpdateEvent import com.processout.sdk.api.model.event.POCardUpdateEvent.* @@ -43,7 +44,8 @@ internal class CardUpdateViewModel private constructor( private val app: Application, private val configuration: POCardUpdateConfiguration, private val cardsRepository: POCardsRepository, - private val eventDispatcher: PODefaultCardUpdateEventDispatcher, + private val legacyEventDispatcher: PODefaultCardUpdateEventDispatcher?, // TODO: remove before next major release. + private val eventDispatcher: POEventDispatcher, private val logAttributes: Map ) : ViewModel() { @@ -57,7 +59,8 @@ internal class CardUpdateViewModel private constructor( app = app, configuration = configuration, cardsRepository = ProcessOut.instance.cards, - eventDispatcher = PODefaultCardUpdateEventDispatcher, + legacyEventDispatcher = PODefaultCardUpdateEventDispatcher, + eventDispatcher = POEventDispatcher, logAttributes = mapOf(POLogAttribute.CARD_ID to configuration.cardId) ) as T } @@ -346,7 +349,7 @@ internal class CardUpdateViewModel private constructor( dispatch(DidComplete) _completion.update { Success(card) } }.onFailure { failure -> - if (eventDispatcher.subscribedForShouldContinueRequest()) { + if (legacyEventDispatcher?.subscribedForShouldContinueRequest() == true) { requestIfShouldContinue(failure) } else { handle(failure) @@ -362,7 +365,7 @@ internal class CardUpdateViewModel private constructor( failure = failure ) latestShouldContinueRequest = request - eventDispatcher.send(request) + legacyEventDispatcher?.send(request) POLogger.info( message = "Requested to decide whether the flow should continue or complete after the failure: %s", failure, attributes = logAttributes @@ -372,7 +375,7 @@ internal class CardUpdateViewModel private constructor( private fun shouldContinueOnFailure() { viewModelScope.launch { - eventDispatcher.shouldContinueResponse.collect { response -> + legacyEventDispatcher?.shouldContinueResponse?.collect { response -> if (response.uuid == latestShouldContinueRequest?.uuid) { latestShouldContinueRequest = null if (response.shouldContinue) { @@ -449,6 +452,7 @@ internal class CardUpdateViewModel private constructor( private fun dispatch(event: POCardUpdateEvent) { viewModelScope.launch { + legacyEventDispatcher?.send(event) eventDispatcher.send(event) } } From b14139bfb7ade99cb7d88df93c8beb9543ba03e3 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 19:27:52 +0300 Subject: [PATCH 4/8] Request and handle if should continue on failure --- .../sdk/ui/card/update/CardUpdateViewModel.kt | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt index c2441b099..b52226228 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt @@ -17,6 +17,7 @@ import com.processout.sdk.api.model.event.POCardUpdateEvent import com.processout.sdk.api.model.event.POCardUpdateEvent.* import com.processout.sdk.api.model.request.POCardUpdateRequest import com.processout.sdk.api.model.request.POCardUpdateShouldContinueRequest +import com.processout.sdk.api.model.response.POCardUpdateShouldContinueResponse import com.processout.sdk.api.repository.POCardsRepository import com.processout.sdk.core.POFailure.Code.Cancelled import com.processout.sdk.core.POFailure.Code.Generic @@ -349,11 +350,7 @@ internal class CardUpdateViewModel private constructor( dispatch(DidComplete) _completion.update { Success(card) } }.onFailure { failure -> - if (legacyEventDispatcher?.subscribedForShouldContinueRequest() == true) { - requestIfShouldContinue(failure) - } else { - handle(failure) - } + requestIfShouldContinue(failure) } } } @@ -365,7 +362,11 @@ internal class CardUpdateViewModel private constructor( failure = failure ) latestShouldContinueRequest = request - legacyEventDispatcher?.send(request) + if (legacyEventDispatcher?.subscribedForShouldContinueRequest() == true) { + legacyEventDispatcher.send(request) + } else { + eventDispatcher.send(request) + } POLogger.info( message = "Requested to decide whether the flow should continue or complete after the failure: %s", failure, attributes = logAttributes @@ -376,18 +377,27 @@ internal class CardUpdateViewModel private constructor( private fun shouldContinueOnFailure() { viewModelScope.launch { legacyEventDispatcher?.shouldContinueResponse?.collect { response -> - if (response.uuid == latestShouldContinueRequest?.uuid) { - latestShouldContinueRequest = null - if (response.shouldContinue) { - handle(response.failure) - } else { - POLogger.info( - message = "Completed after the failure: %s", response.failure, - attributes = logAttributes - ) - _completion.update { Failure(response.failure) } - } - } + handleShouldContinue(response) + } + } + eventDispatcher.subscribeForResponse( + coroutineScope = viewModelScope + ) { response -> + handleShouldContinue(response) + } + } + + private fun handleShouldContinue(response: POCardUpdateShouldContinueResponse) { + if (response.uuid == latestShouldContinueRequest?.uuid) { + latestShouldContinueRequest = null + if (response.shouldContinue) { + handle(response.failure) + } else { + POLogger.info( + message = "Completed after the failure: %s", response.failure, + attributes = logAttributes + ) + _completion.update { Failure(response.failure) } } } } From caee1993696f30397fedd8e96d40e2ac27e5b1c4 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 19:29:05 +0300 Subject: [PATCH 5/8] Update example app --- .../processout/example/ui/screen/features/FeaturesFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/features/FeaturesFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/features/FeaturesFragment.kt index 5509fc46c..16c51b366 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/features/FeaturesFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/features/FeaturesFragment.kt @@ -24,6 +24,7 @@ import com.processout.sdk.ui.card.scanner.POCardScannerLauncher import com.processout.sdk.ui.card.scanner.recognition.POScannedCard import com.processout.sdk.ui.card.update.POCardUpdateConfiguration import com.processout.sdk.ui.card.update.POCardUpdateConfiguration.CardInformation +import com.processout.sdk.ui.card.update.POCardUpdateDelegate import com.processout.sdk.ui.card.update.POCardUpdateLauncher import com.processout.sdk.ui.googlepay.POGooglePayCardTokenizationLauncher import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration @@ -45,6 +46,7 @@ class FeaturesFragment : BaseFragment( super.onCreate(savedInstanceState) cardUpdateLauncher = POCardUpdateLauncher.create( from = this, + delegate = object : POCardUpdateDelegate {}, callback = ::handleCardUpdateResult ) cardScannerLauncher = POCardScannerLauncher.create( From 2ef258a53e2835daa0f0c334b4d930f0eee24769 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 19:36:35 +0300 Subject: [PATCH 6/8] Deprecate dispatcher --- .../com/processout/sdk/api/dispatcher/POEventDispatchers.kt | 1 + .../api/dispatcher/card/update/POCardUpdateEventDispatcher.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatchers.kt b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatchers.kt index db54b7f57..361cd3396 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatchers.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/POEventDispatchers.kt @@ -16,5 +16,6 @@ interface POEventDispatchers { val cardTokenization: POCardTokenizationEventDispatcher /** Dispatcher that allows to handle events during card updates. */ + @Deprecated(message = "Use API with POCardUpdateDelegate instead.") val cardUpdate: POCardUpdateEventDispatcher } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/update/POCardUpdateEventDispatcher.kt b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/update/POCardUpdateEventDispatcher.kt index 5e8263bc4..4841a3a28 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/update/POCardUpdateEventDispatcher.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/update/POCardUpdateEventDispatcher.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharedFlow /** * Dispatcher that allows to handle events during card updates. */ +@Deprecated(message = "Use API with POCardUpdateDelegate instead.") interface POCardUpdateEventDispatcher { /** Allows to subscribe for card update lifecycle events. */ From 0fe658b8a406c2cb1441ea57d679b92c5a0bef8c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 19:42:30 +0300 Subject: [PATCH 7/8] Fix nullablility --- .../processout/sdk/ui/card/update/CardUpdateViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt index b52226228..5515ca6be 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt @@ -45,7 +45,7 @@ internal class CardUpdateViewModel private constructor( private val app: Application, private val configuration: POCardUpdateConfiguration, private val cardsRepository: POCardsRepository, - private val legacyEventDispatcher: PODefaultCardUpdateEventDispatcher?, // TODO: remove before next major release. + private val legacyEventDispatcher: PODefaultCardUpdateEventDispatcher, // TODO: remove before next major release. private val eventDispatcher: POEventDispatcher, private val logAttributes: Map ) : ViewModel() { @@ -362,7 +362,7 @@ internal class CardUpdateViewModel private constructor( failure = failure ) latestShouldContinueRequest = request - if (legacyEventDispatcher?.subscribedForShouldContinueRequest() == true) { + if (legacyEventDispatcher.subscribedForShouldContinueRequest()) { legacyEventDispatcher.send(request) } else { eventDispatcher.send(request) @@ -376,7 +376,7 @@ internal class CardUpdateViewModel private constructor( private fun shouldContinueOnFailure() { viewModelScope.launch { - legacyEventDispatcher?.shouldContinueResponse?.collect { response -> + legacyEventDispatcher.shouldContinueResponse.collect { response -> handleShouldContinue(response) } } @@ -462,7 +462,7 @@ internal class CardUpdateViewModel private constructor( private fun dispatch(event: POCardUpdateEvent) { viewModelScope.launch { - legacyEventDispatcher?.send(event) + legacyEventDispatcher.send(event) eventDispatcher.send(event) } } From 56cfb5045fb52ce37f240ab3bde3773b45c3a6a2 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 29 Apr 2025 19:50:25 +0300 Subject: [PATCH 8/8] AGP 8.9.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1466901d6..59a4f823b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - androidGradlePluginVersion = '8.9.1' + androidGradlePluginVersion = '8.9.2' kotlinVersion = '2.1.20' kspVersion = '2.1.20-1.0.32' dokkaVersion = '1.9.20'