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' 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( 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. */ 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/CardUpdateViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt index 089a52035..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 @@ -11,11 +11,13 @@ 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.* 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 @@ -43,7 +45,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 +60,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,11 +350,7 @@ internal class CardUpdateViewModel private constructor( dispatch(DidComplete) _completion.update { Success(card) } }.onFailure { failure -> - if (eventDispatcher.subscribedForShouldContinueRequest()) { - requestIfShouldContinue(failure) - } else { - handle(failure) - } + requestIfShouldContinue(failure) } } } @@ -362,7 +362,11 @@ internal class CardUpdateViewModel private constructor( failure = failure ) latestShouldContinueRequest = request - eventDispatcher.send(request) + if (legacyEventDispatcher.subscribedForShouldContinueRequest()) { + 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 @@ -372,19 +376,28 @@ internal class CardUpdateViewModel private constructor( private fun shouldContinueOnFailure() { viewModelScope.launch { - eventDispatcher.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) } - } - } + legacyEventDispatcher.shouldContinueResponse.collect { response -> + 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) } } } } @@ -449,6 +462,7 @@ internal class CardUpdateViewModel private constructor( private fun dispatch(event: POCardUpdateEvent) { viewModelScope.launch { + legacyEventDispatcher.send(event) eventDispatcher.send(event) } } 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 +} 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. */