From 9d48b761557fcceac96c4b516d46cb5b3338420f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 8 Apr 2025 19:46:35 +0300 Subject: [PATCH 01/21] POCardTokenizationDelegate and propagating of POCardTokenizationEvent --- .../card/payment/CardPaymentFragment.kt | 1 + .../DefaultCardTokenizationDelegate.kt | 12 ++++ .../CardTokenizationInteractor.kt | 29 ++++---- .../tokenization/CardTokenizationViewModel.kt | 2 +- .../POCardTokenizationDelegate.kt | 14 ++++ .../POCardTokenizationLauncher.kt | 66 ++++++++++++++++++- 6 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt index 79a8bb146..38f60bc92 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt @@ -51,6 +51,7 @@ class CardPaymentFragment : BaseFragment( super.onCreate(savedInstanceState) launcher = POCardTokenizationLauncher.create( from = this, + delegate = DefaultCardTokenizationDelegate(), callback = ::handle ) customTabLauncher = PO3DSRedirectCustomTabLauncher.create(from = this) diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt new file mode 100644 index 000000000..1df5974c5 --- /dev/null +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt @@ -0,0 +1,12 @@ +package com.processout.example.ui.screen.card.payment + +import com.processout.sdk.api.model.event.POCardTokenizationEvent +import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.ui.card.tokenization.POCardTokenizationDelegate + +class DefaultCardTokenizationDelegate : POCardTokenizationDelegate { + + override fun onEvent(event: POCardTokenizationEvent) { + POLogger.info("%s", event) + } +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index 7192a2f65..dc5c978a6 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.R +import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.dispatcher.card.tokenization.PODefaultCardTokenizationEventDispatcher import com.processout.sdk.api.model.event.POCardTokenizationEvent import com.processout.sdk.api.model.event.POCardTokenizationEvent.* @@ -52,7 +53,8 @@ internal class CardTokenizationInteractor( private val cardsRepository: POCardsRepository, private val cardSchemeProvider: CardSchemeProvider, private val addressSpecificationProvider: AddressSpecificationProvider, - private val eventDispatcher: PODefaultCardTokenizationEventDispatcher + private val legacyEventDispatcher: PODefaultCardTokenizationEventDispatcher, + private val eventDispatcher: POEventDispatcher = POEventDispatcher ) : BaseInteractor() { private companion object { @@ -353,7 +355,7 @@ internal class CardTokenizationInteractor( issuerInformationJob?.cancel() issuerInformationJob = interactorScope.launch { fetchIssuerInformation(iin)?.let { issuerInformation -> - if (eventDispatcher.subscribedForPreferredSchemeRequest()) { + if (legacyEventDispatcher.subscribedForPreferredSchemeRequest()) { requestPreferredScheme(issuerInformation) } else { updateState( @@ -377,13 +379,13 @@ internal class CardTokenizationInteractor( private suspend fun requestPreferredScheme(issuerInformation: POCardIssuerInformation) { val request = POCardTokenizationPreferredSchemeRequest(issuerInformation) latestPreferredSchemeRequest = request - eventDispatcher.send(request) + legacyEventDispatcher.send(request) POLogger.info("Requested to choose preferred scheme by issuer information: %s", issuerInformation) } private fun collectPreferredScheme() { interactorScope.launch { - eventDispatcher.preferredSchemeResponse.collect { response -> + legacyEventDispatcher.preferredSchemeResponse.collect { response -> if (response.uuid == latestPreferredSchemeRequest?.uuid) { latestPreferredSchemeRequest = null updateState( @@ -647,8 +649,8 @@ internal class CardTokenizationInteractor( attributes = mapOf(POLogAttribute.CARD_ID to card.id) ) dispatch(DidTokenize(card)) - val subscribedForProcessing = eventDispatcher.subscribedForProcessTokenizedCardRequest() - val subscribedForProcessingDeprecated = eventDispatcher.subscribedForProcessTokenizedCard() + val subscribedForProcessing = legacyEventDispatcher.subscribedForProcessTokenizedCardRequest() + val subscribedForProcessingDeprecated = legacyEventDispatcher.subscribedForProcessTokenizedCard() val processingRequest = POCardTokenizationProcessingRequest( card = card, saveCard = _state.value.saveCardField.value.text.toBooleanStrictOrNull() ?: false @@ -663,7 +665,7 @@ internal class CardTokenizationInteractor( complete(Success(card)) } }.onFailure { failure -> - if (eventDispatcher.subscribedForShouldContinueRequest()) { + if (legacyEventDispatcher.subscribedForShouldContinueRequest()) { requestIfShouldContinue(failure) } else { handle(failure) @@ -680,9 +682,9 @@ internal class CardTokenizationInteractor( _state.update { it.copy(focusedFieldId = null) } if (useDeprecated) { @Suppress("DEPRECATION") - eventDispatcher.processTokenizedCard(request.card) + legacyEventDispatcher.processTokenizedCard(request.card) } else { - eventDispatcher.processTokenizedCardRequest(request) + legacyEventDispatcher.processTokenizedCardRequest(request) } POLogger.info( message = "Requested to process tokenized card.", @@ -693,7 +695,7 @@ internal class CardTokenizationInteractor( private fun handleCompletion() { interactorScope.launch { - eventDispatcher.completion.collect { result -> + legacyEventDispatcher.completion.collect { result -> result.onSuccess { _state.value.tokenizedCard?.let { card -> dispatch(DidComplete) @@ -719,7 +721,7 @@ internal class CardTokenizationInteractor( } private fun handleCompletion(failure: ProcessOutResult.Failure) { - if (eventDispatcher.subscribedForShouldContinueRequest()) { + if (legacyEventDispatcher.subscribedForShouldContinueRequest()) { requestIfShouldContinue(failure) } else { POLogger.info("Completed after the failure: %s", failure) @@ -731,14 +733,14 @@ internal class CardTokenizationInteractor( interactorScope.launch { val request = POCardTokenizationShouldContinueRequest(failure) latestShouldContinueRequest = request - eventDispatcher.send(request) + legacyEventDispatcher.send(request) POLogger.info("Requested to decide whether the flow should continue or complete after the failure: %s", failure) } } private fun shouldContinueOnFailure() { interactorScope.launch { - eventDispatcher.shouldContinueResponse.collect { response -> + legacyEventDispatcher.shouldContinueResponse.collect { response -> if (response.uuid == latestShouldContinueRequest?.uuid) { latestShouldContinueRequest = null if (response.shouldContinue) { @@ -861,6 +863,7 @@ internal class CardTokenizationInteractor( private fun dispatch(event: POCardTokenizationEvent) { interactorScope.launch { + legacyEventDispatcher.send(event) eventDispatcher.send(event) } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index e73a7976a..05fa87406 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -55,7 +55,7 @@ internal class CardTokenizationViewModel private constructor( cardsRepository = ProcessOut.instance.cards, cardSchemeProvider = CardSchemeProvider(), addressSpecificationProvider = AddressSpecificationProvider(app), - eventDispatcher = eventDispatcher + legacyEventDispatcher = eventDispatcher ) ) as T } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt new file mode 100644 index 000000000..5ff6fdf95 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt @@ -0,0 +1,14 @@ +package com.processout.sdk.ui.card.tokenization + +import com.processout.sdk.api.model.event.POCardTokenizationEvent + +/** + * Delegate that allows to handle events during card tokenization. + */ +interface POCardTokenizationDelegate { + + /** + * Invoked on card tokenization lifecycle events. + */ + fun onEvent(event: POCardTokenizationEvent) {} +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt index 1161fa3ee..dec07f488 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt @@ -5,16 +5,23 @@ 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.POCardTokenizationEvent import com.processout.sdk.api.model.response.POCard import com.processout.sdk.core.ProcessOutActivityResult +import kotlinx.coroutines.CoroutineScope /** * Launcher that starts [CardTokenizationActivity] and provides the result. */ class POCardTokenizationLauncher private constructor( + private val scope: CoroutineScope, private val launcher: ActivityResultLauncher, - private val activityOptions: ActivityOptionsCompat + private val activityOptions: ActivityOptionsCompat, + private val delegate: POCardTokenizationDelegate, + private val eventDispatcher: POEventDispatcher = POEventDispatcher ) { companion object { @@ -24,13 +31,34 @@ class POCardTokenizationLauncher private constructor( */ fun create( from: Fragment, + delegate: POCardTokenizationDelegate, callback: (ProcessOutActivityResult) -> Unit ) = POCardTokenizationLauncher( + scope = from.lifecycleScope, launcher = from.registerForActivityResult( CardTokenizationActivityContract(), 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 + ) = POCardTokenizationLauncher( + scope = from.lifecycleScope, + launcher = from.registerForActivityResult( + CardTokenizationActivityContract(), + callback + ), + activityOptions = createActivityOptions(from.requireContext()), + delegate = object : POCardTokenizationDelegate {} ) /** @@ -39,14 +67,36 @@ class POCardTokenizationLauncher private constructor( */ fun create( from: ComponentActivity, + delegate: POCardTokenizationDelegate, callback: (ProcessOutActivityResult) -> Unit ) = POCardTokenizationLauncher( + scope = from.lifecycleScope, launcher = from.registerForActivityResult( CardTokenizationActivityContract(), 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 + ) = POCardTokenizationLauncher( + scope = from.lifecycleScope, + launcher = from.registerForActivityResult( + CardTokenizationActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(from), + delegate = object : POCardTokenizationDelegate {} ) private fun createActivityOptions(context: Context) = @@ -55,6 +105,16 @@ class POCardTokenizationLauncher private constructor( ) } + init { + dispatchEvents() + } + + private fun dispatchEvents() { + eventDispatcher.subscribe( + coroutineScope = scope + ) { delegate.onEvent(it) } + } + /** * Launches the activity. */ From da4a8a290ef02eee91e3ede746555b7869a2b91c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 9 Apr 2025 12:03:46 +0300 Subject: [PATCH 02/21] Deprecation --- .../card/tokenization/POCardTokenizationEventDispatcher.kt | 1 + .../sdk/ui/card/tokenization/CardTokenizationInteractor.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt index 650ce50ac..2f8782d19 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.SharedFlow /** * Dispatcher that allows to handle events during card tokenization. */ +@Deprecated(message = "Use POCardTokenizationDelegate instead.") interface POCardTokenizationEventDispatcher { /** Allows to subscribe for card tokenization lifecycle events. */ diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index dc5c978a6..d40930e1b 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -53,7 +53,7 @@ internal class CardTokenizationInteractor( private val cardsRepository: POCardsRepository, private val cardSchemeProvider: CardSchemeProvider, private val addressSpecificationProvider: AddressSpecificationProvider, - private val legacyEventDispatcher: PODefaultCardTokenizationEventDispatcher, + private val legacyEventDispatcher: PODefaultCardTokenizationEventDispatcher, // TODO: remove before next major release. private val eventDispatcher: POEventDispatcher = POEventDispatcher ) : BaseInteractor() { From 990bcc8dfe786a0df53e2969bbf6c00204a62067 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 9 Apr 2025 12:28:30 +0300 Subject: [PATCH 03/21] Implement models as POEventDispatcher request/response --- .../model/request/POCardTokenizationProcessingRequest.kt | 8 ++++++-- .../request/POCardTokenizationShouldContinueRequest.kt | 5 +++-- .../response/POCardTokenizationShouldContinueResponse.kt | 5 +++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationProcessingRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationProcessingRequest.kt index ba0498ecf..f0ed9ad9b 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationProcessingRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationProcessingRequest.kt @@ -1,14 +1,18 @@ package com.processout.sdk.api.model.request +import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.model.response.POCard +import java.util.UUID /** * Request to process tokenized card (authorize invoice or assign customer token). * * @param[card] Tokenized card. * @param[saveCard] Indicates whether the user has chosen to save the card for future payments. + * @param[uuid] Unique identifier of request. */ data class POCardTokenizationProcessingRequest( val card: POCard, - val saveCard: Boolean -) + val saveCard: Boolean, + override val uuid: UUID = UUID.randomUUID() +) : POEventDispatcher.Request diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationShouldContinueRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationShouldContinueRequest.kt index bc20d5449..555f334ca 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationShouldContinueRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCardTokenizationShouldContinueRequest.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 @@ -12,5 +13,5 @@ import java.util.UUID */ data class POCardTokenizationShouldContinueRequest @ProcessOutInternalApi constructor( 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/POCardTokenizationShouldContinueResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationShouldContinueResponse.kt index b1c51c9aa..d6bc50df2 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationShouldContinueResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationShouldContinueResponse.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.POCardTokenizationShouldContinueRequest 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 POCardTokenizationShouldContinueResponse internal constructor( - val uuid: UUID, + override val uuid: UUID, val failure: ProcessOutResult.Failure, val shouldContinue: Boolean -) +) : POEventDispatcher.Response /** * Creates [POCardTokenizationShouldContinueResponse] from [POCardTokenizationShouldContinueRequest]. From d4725b3c244463bf1382d353890e814cba55e5c5 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 9 Apr 2025 16:51:30 +0300 Subject: [PATCH 04/21] preferredScheme and shouldContinue --- .../CardTokenizationInteractor.kt | 99 ++++++++++--------- .../POCardTokenizationDelegate.kt | 18 ++++ .../POCardTokenizationLauncher.kt | 28 ++++++ 3 files changed, 101 insertions(+), 44 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index d40930e1b..89f6b3a9c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -10,6 +10,8 @@ import com.processout.sdk.api.model.event.POCardTokenizationEvent import com.processout.sdk.api.model.event.POCardTokenizationEvent.* import com.processout.sdk.api.model.request.* import com.processout.sdk.api.model.response.POCardIssuerInformation +import com.processout.sdk.api.model.response.POCardTokenizationPreferredSchemeResponse +import com.processout.sdk.api.model.response.POCardTokenizationShouldContinueResponse import com.processout.sdk.api.repository.POCardsRepository import com.processout.sdk.core.POFailure.Code.Cancelled import com.processout.sdk.core.POFailure.Code.Generic @@ -355,14 +357,7 @@ internal class CardTokenizationInteractor( issuerInformationJob?.cancel() issuerInformationJob = interactorScope.launch { fetchIssuerInformation(iin)?.let { issuerInformation -> - if (legacyEventDispatcher.subscribedForPreferredSchemeRequest()) { - requestPreferredScheme(issuerInformation) - } else { - updateState( - issuerInformation = issuerInformation, - preferredScheme = null - ) - } + requestPreferredScheme(issuerInformation) } } } @@ -379,22 +374,35 @@ internal class CardTokenizationInteractor( private suspend fun requestPreferredScheme(issuerInformation: POCardIssuerInformation) { val request = POCardTokenizationPreferredSchemeRequest(issuerInformation) latestPreferredSchemeRequest = request - legacyEventDispatcher.send(request) + if (legacyEventDispatcher.subscribedForPreferredSchemeRequest()) { + legacyEventDispatcher.send(request) + } else { + eventDispatcher.send(request) + } POLogger.info("Requested to choose preferred scheme by issuer information: %s", issuerInformation) } private fun collectPreferredScheme() { interactorScope.launch { legacyEventDispatcher.preferredSchemeResponse.collect { response -> - if (response.uuid == latestPreferredSchemeRequest?.uuid) { - latestPreferredSchemeRequest = null - updateState( - issuerInformation = response.issuerInformation, - preferredScheme = response.preferredScheme - ) - } + handlePreferredScheme(response) } } + eventDispatcher.subscribeForResponse( + coroutineScope = interactorScope + ) { response -> + handlePreferredScheme(response) + } + } + + private fun handlePreferredScheme(response: POCardTokenizationPreferredSchemeResponse) { + if (response.uuid == latestPreferredSchemeRequest?.uuid) { + latestPreferredSchemeRequest = null + updateState( + issuerInformation = response.issuerInformation, + preferredScheme = response.preferredScheme + ) + } } private fun updateState( @@ -664,12 +672,8 @@ internal class CardTokenizationInteractor( if (!subscribedForProcessing && !subscribedForProcessingDeprecated) { complete(Success(card)) } - }.onFailure { failure -> - if (legacyEventDispatcher.subscribedForShouldContinueRequest()) { - requestIfShouldContinue(failure) - } else { - handle(failure) - } + }.onFailure { + requestIfShouldContinue(failure = it) } } } @@ -705,9 +709,11 @@ internal class CardTokenizationInteractor( code = Generic(), message = "Completion is called with Success via dispatcher before card is tokenized." ) - handleCompletion(failure) + requestIfShouldContinue(failure) } - }.onFailure { handleCompletion(it) } + }.onFailure { + requestIfShouldContinue(failure = it) + } } } } @@ -720,20 +726,15 @@ internal class CardTokenizationInteractor( _completion.update { success } } - private fun handleCompletion(failure: ProcessOutResult.Failure) { - if (legacyEventDispatcher.subscribedForShouldContinueRequest()) { - requestIfShouldContinue(failure) - } else { - POLogger.info("Completed after the failure: %s", failure) - _completion.update { Failure(failure) } - } - } - private fun requestIfShouldContinue(failure: ProcessOutResult.Failure) { interactorScope.launch { val request = POCardTokenizationShouldContinueRequest(failure) latestShouldContinueRequest = request - legacyEventDispatcher.send(request) + if (legacyEventDispatcher.subscribedForShouldContinueRequest()) { + legacyEventDispatcher.send(request) + } else { + eventDispatcher.send(request) + } POLogger.info("Requested to decide whether the flow should continue or complete after the failure: %s", failure) } } @@ -741,15 +742,24 @@ internal class CardTokenizationInteractor( private fun shouldContinueOnFailure() { interactorScope.launch { legacyEventDispatcher.shouldContinueResponse.collect { response -> - if (response.uuid == latestShouldContinueRequest?.uuid) { - latestShouldContinueRequest = null - if (response.shouldContinue) { - handle(response.failure) - } else { - POLogger.info("Completed after the failure: %s", response.failure) - _completion.update { Failure(response.failure) } - } - } + handleShouldContinue(response) + } + } + eventDispatcher.subscribeForResponse( + coroutineScope = interactorScope + ) { response -> + handleShouldContinue(response) + } + } + + private fun handleShouldContinue(response: POCardTokenizationShouldContinueResponse) { + if (response.uuid == latestShouldContinueRequest?.uuid) { + latestShouldContinueRequest = null + if (response.shouldContinue) { + handle(response.failure) + } else { + POLogger.info("Completed after the failure: %s", response.failure) + _completion.update { Failure(response.failure) } } } } @@ -808,6 +818,7 @@ internal class CardTokenizationInteractor( } else -> app.getString(R.string.po_card_tokenization_error_generic) } + Cancelled -> null else -> app.getString(R.string.po_card_tokenization_error_generic) } handle(failure, invalidFieldIds, errorMessage) @@ -816,7 +827,7 @@ internal class CardTokenizationInteractor( private fun handle( failure: ProcessOutResult.Failure, invalidFieldIds: Set, - errorMessage: String + errorMessage: String? ) { val cardFields = _state.value.cardFields.map { field -> validatedField(invalidFieldIds, field) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt index 5ff6fdf95..7135fb66a 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt @@ -1,6 +1,8 @@ package com.processout.sdk.ui.card.tokenization import com.processout.sdk.api.model.event.POCardTokenizationEvent +import com.processout.sdk.api.model.response.POCardIssuerInformation +import com.processout.sdk.core.ProcessOutResult /** * Delegate that allows to handle events during card tokenization. @@ -11,4 +13,20 @@ interface POCardTokenizationDelegate { * Invoked on card tokenization lifecycle events. */ fun onEvent(event: POCardTokenizationEvent) {} + + /** + * Allows to choose default preferred card scheme based on issuer information. + * Primary card scheme is used by default. + */ + suspend fun preferredScheme( + issuerInformation: POCardIssuerInformation + ): String? = issuerInformation.scheme + + /** + * Allows to decide whether the flow 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/tokenization/POCardTokenizationLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt index dec07f488..100d5c2ce 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt @@ -9,9 +9,13 @@ import androidx.lifecycle.lifecycleScope import com.processout.sdk.R import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.model.event.POCardTokenizationEvent +import com.processout.sdk.api.model.request.POCardTokenizationPreferredSchemeRequest +import com.processout.sdk.api.model.request.POCardTokenizationShouldContinueRequest 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 [CardTokenizationActivity] and provides the result. @@ -107,6 +111,8 @@ class POCardTokenizationLauncher private constructor( init { dispatchEvents() + dispatchPreferredScheme() + dispatchShouldContinue() } private fun dispatchEvents() { @@ -115,6 +121,28 @@ class POCardTokenizationLauncher private constructor( ) { delegate.onEvent(it) } } + private fun dispatchPreferredScheme() { + eventDispatcher.subscribeForRequest( + coroutineScope = scope + ) { request -> + scope.launch { + val preferredScheme = delegate.preferredScheme(request.issuerInformation) + eventDispatcher.send(request.toResponse(preferredScheme)) + } + } + } + + 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 8a3dec2cac34cd323a04d9734ad0c7198fa5d1a4 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 12:31:34 +0300 Subject: [PATCH 05/21] CustomerActionsService --- ...DSService.kt => CustomerActionsService.kt} | 4 +- ...ce.kt => DefaultCustomerActionsService.kt} | 30 ++++++----- .../service/DefaultCustomerTokensService.kt | 54 +++++++++---------- .../sdk/api/service/DefaultInvoicesService.kt | 54 +++++++++---------- .../com/processout/sdk/di/ServiceGraph.kt | 8 +-- 5 files changed, 74 insertions(+), 76 deletions(-) rename sdk/src/main/kotlin/com/processout/sdk/api/service/{ThreeDSService.kt => CustomerActionsService.kt} (75%) rename sdk/src/main/kotlin/com/processout/sdk/api/service/{DefaultThreeDSService.kt => DefaultCustomerActionsService.kt} (90%) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/ThreeDSService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt similarity index 75% rename from sdk/src/main/kotlin/com/processout/sdk/api/service/ThreeDSService.kt rename to sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt index 4e842da03..78d98c9a7 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/ThreeDSService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt @@ -3,11 +3,11 @@ package com.processout.sdk.api.service import com.processout.sdk.api.model.response.CustomerAction import com.processout.sdk.core.ProcessOutResult -internal interface ThreeDSService { +internal interface CustomerActionsService { fun handle( action: CustomerAction, - delegate: PO3DSService, + threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit ) } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultThreeDSService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt similarity index 90% rename from sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultThreeDSService.kt rename to sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index cd821133e..5b328e440 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultThreeDSService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -14,7 +14,9 @@ import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import java.net.MalformedURLException -internal class DefaultThreeDSService(private val moshi: Moshi) : ThreeDSService { +internal class DefaultCustomerActionsService( + private val moshi: Moshi +) : CustomerActionsService { private companion object { private const val DEVICE_CHANNEL = "app" @@ -27,19 +29,19 @@ internal class DefaultThreeDSService(private val moshi: Moshi) : ThreeDSService override fun handle( action: CustomerAction, - delegate: PO3DSService, + threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit ) { POLogger.info("Handling customer action type: %s", action.rawType) when (action.type()) { FINGERPRINT_MOBILE -> fingerprintMobile( - encodedConfiguration = action.value, delegate, callback + encodedConfiguration = action.value, threeDSService, callback ) CHALLENGE_MOBILE -> challengeMobile( - encodedChallenge = action.value, delegate, callback + encodedChallenge = action.value, threeDSService, callback ) - FINGERPRINT -> fingerprint(url = action.value, delegate, callback) - REDIRECT, URL -> redirect(url = action.value, delegate, callback) + FINGERPRINT -> fingerprint(url = action.value, threeDSService, callback) + REDIRECT, URL -> redirect(url = action.value, threeDSService, callback) UNSUPPORTED -> callback( ProcessOutResult.Failure( POFailure.Code.Internal(), @@ -51,14 +53,14 @@ internal class DefaultThreeDSService(private val moshi: Moshi) : ThreeDSService private fun fingerprintMobile( encodedConfiguration: String, - delegate: PO3DSService, + threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit ) { try { moshi.adapter(PO3DS2Configuration::class.java) .fromJson(String(Base64.decode(encodedConfiguration, Base64.NO_WRAP)))!! .let { configuration -> - delegate.authenticationRequest(configuration) { result -> + threeDSService.authenticationRequest(configuration) { result -> when (result) { is ProcessOutResult.Success -> callback( ChallengeResponse(body = encode(result.value)), callback @@ -82,14 +84,14 @@ internal class DefaultThreeDSService(private val moshi: Moshi) : ThreeDSService private fun challengeMobile( encodedChallenge: String, - delegate: PO3DSService, + threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit ) { try { moshi.adapter(PO3DS2Challenge::class.java) .fromJson(String(Base64.decode(encodedChallenge, Base64.NO_WRAP)))!! .let { challenge -> - delegate.handle(challenge) { result -> + threeDSService.handle(challenge) { result -> when (result) { is ProcessOutResult.Success -> { val body = if (result.value) @@ -116,11 +118,11 @@ internal class DefaultThreeDSService(private val moshi: Moshi) : ThreeDSService private fun fingerprint( url: String, - delegate: PO3DSService, + threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit ) { try { - delegate.handle( + threeDSService.handle( PO3DSRedirect( url = java.net.URL(url), timeoutSeconds = WEB_FINGERPRINT_TIMEOUT_SECONDS @@ -163,11 +165,11 @@ internal class DefaultThreeDSService(private val moshi: Moshi) : ThreeDSService private fun redirect( url: String, - delegate: PO3DSService, + threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit ) { try { - delegate.handle(PO3DSRedirect(url = java.net.URL(url)), callback) + threeDSService.handle(PO3DSRedirect(url = java.net.URL(url)), callback) } catch (e: MalformedURLException) { callback( ProcessOutResult.Failure( diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt index 33ddefbe3..590833aa0 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch internal class DefaultCustomerTokensService( private val scope: CoroutineScope, private val repository: CustomerTokensRepository, - private val threeDSService: ThreeDSService + private val customerActionsService: CustomerActionsService ) : POCustomerTokensService { private val _assignCustomerTokenResult = MutableSharedFlow>() @@ -33,22 +33,21 @@ internal class DefaultCustomerTokensService( when (val result = repository.assignCustomerToken(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - this@DefaultCustomerTokensService.threeDSService - .handle(action, threeDSService) { serviceResult -> - when (serviceResult) { - is ProcessOutResult.Success -> - assignCustomerToken( - request.copy(source = serviceResult.value), - threeDSService - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - scope.launch { - _assignCustomerTokenResult.emit(serviceResult) - } + customerActionsService.handle(action, threeDSService) { serviceResult -> + when (serviceResult) { + is ProcessOutResult.Success -> + assignCustomerToken( + request.copy(source = serviceResult.value), + threeDSService + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + scope.launch { + _assignCustomerTokenResult.emit(serviceResult) } } } + } } ?: run { threeDSService.cleanup() result.value.token?.let { token -> @@ -103,22 +102,21 @@ internal class DefaultCustomerTokensService( when (val result = repository.assignCustomerToken(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - this@DefaultCustomerTokensService.threeDSService - .handle(action, threeDSService) { serviceResult -> - @Suppress("DEPRECATION") - when (serviceResult) { - is ProcessOutResult.Success -> - assignCustomerToken( - request.copy(source = serviceResult.value), - threeDSService, - callback - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - callback(serviceResult) - } + customerActionsService.handle(action, threeDSService) { serviceResult -> + @Suppress("DEPRECATION") + when (serviceResult) { + is ProcessOutResult.Success -> + assignCustomerToken( + request.copy(source = serviceResult.value), + threeDSService, + callback + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + callback(serviceResult) } } + } } ?: run { threeDSService.cleanup() result.value.token?.let { token -> diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt index eba84e6d5..09f60c049 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.launch internal class DefaultInvoicesService( private val scope: CoroutineScope, private val repository: InvoicesRepository, - private val threeDSService: ThreeDSService + private val customerActionsService: CustomerActionsService ) : POInvoicesService { private val _authorizeInvoiceResult = MutableSharedFlow>() @@ -36,22 +36,21 @@ internal class DefaultInvoicesService( when (val result = repository.authorizeInvoice(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - this@DefaultInvoicesService.threeDSService - .handle(action, threeDSService) { serviceResult -> - when (serviceResult) { - is ProcessOutResult.Success -> - authorizeInvoice( - request.copy(source = serviceResult.value), - threeDSService - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - scope.launch { - _authorizeInvoiceResult.emit(serviceResult) - } + customerActionsService.handle(action, threeDSService) { serviceResult -> + when (serviceResult) { + is ProcessOutResult.Success -> + authorizeInvoice( + request.copy(source = serviceResult.value), + threeDSService + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + scope.launch { + _authorizeInvoiceResult.emit(serviceResult) } } } + } } ?: run { threeDSService.cleanup() scope.launch { @@ -84,22 +83,21 @@ internal class DefaultInvoicesService( when (val result = repository.authorizeInvoice(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - this@DefaultInvoicesService.threeDSService - .handle(action, threeDSService) { serviceResult -> - @Suppress("DEPRECATION") - when (serviceResult) { - is ProcessOutResult.Success -> - authorizeInvoice( - request.copy(source = serviceResult.value), - threeDSService, - callback - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - callback(serviceResult) - } + customerActionsService.handle(action, threeDSService) { serviceResult -> + @Suppress("DEPRECATION") + when (serviceResult) { + is ProcessOutResult.Success -> + authorizeInvoice( + request.copy(source = serviceResult.value), + threeDSService, + callback + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + callback(serviceResult) } } + } } ?: run { threeDSService.cleanup() callback(ProcessOutResult.Success(Unit)) diff --git a/sdk/src/main/kotlin/com/processout/sdk/di/ServiceGraph.kt b/sdk/src/main/kotlin/com/processout/sdk/di/ServiceGraph.kt index d4c151875..af7e57dc1 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/di/ServiceGraph.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/di/ServiceGraph.kt @@ -21,15 +21,15 @@ internal class DefaultServiceGraph( alternativePaymentMethodsBaseUrl: String ) : ServiceGraph { - private val threeDSService: ThreeDSService by lazy { - DefaultThreeDSService(networkGraph.moshi) + private val customerActionsService: CustomerActionsService by lazy { + DefaultCustomerActionsService(networkGraph.moshi) } override val invoicesService: POInvoicesService by lazy { DefaultInvoicesService( contextGraph.mainScope, repositoryGraph.invoicesRepository, - threeDSService + customerActionsService ) } @@ -37,7 +37,7 @@ internal class DefaultServiceGraph( DefaultCustomerTokensService( contextGraph.mainScope, repositoryGraph.customerTokensRepository, - threeDSService + customerActionsService ) } From 8959d88e06ca2bb9fdfd0efc6671ae958a6a1aa5 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 17:40:49 +0300 Subject: [PATCH 06/21] Re-implemented DefaultCustomerActionsService using coroutines, applied to services that use it --- .../sdk/api/service/CustomerActionsService.kt | 7 +- .../service/DefaultCustomerActionsService.kt | 270 ++++++++++-------- .../service/DefaultCustomerTokensService.kt | 46 ++- .../sdk/api/service/DefaultInvoicesService.kt | 46 ++- 4 files changed, 190 insertions(+), 179 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt index 78d98c9a7..c9bab39b4 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt @@ -5,9 +5,8 @@ import com.processout.sdk.core.ProcessOutResult internal interface CustomerActionsService { - fun handle( + suspend fun handle( action: CustomerAction, - threeDSService: PO3DSService, - callback: (ProcessOutResult) -> Unit - ) + threeDSService: PO3DSService + ): ProcessOutResult } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index 5b328e440..c915be545 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -7,12 +7,15 @@ import com.processout.sdk.api.model.threeds.PO3DS2AuthenticationRequest import com.processout.sdk.api.model.threeds.PO3DS2Challenge import com.processout.sdk.api.model.threeds.PO3DS2Configuration import com.processout.sdk.api.model.threeds.PO3DSRedirect -import com.processout.sdk.core.POFailure +import com.processout.sdk.core.POFailure.Code.Internal +import com.processout.sdk.core.POFailure.Code.Timeout import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import java.net.MalformedURLException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine internal class DefaultCustomerActionsService( private val moshi: Moshi @@ -27,158 +30,176 @@ internal class DefaultCustomerActionsService( private const val WEB_FINGERPRINT_TIMEOUT_SECONDS = 10 } - override fun handle( + override suspend fun handle( action: CustomerAction, - threeDSService: PO3DSService, - callback: (ProcessOutResult) -> Unit - ) { + threeDSService: PO3DSService + ): ProcessOutResult { POLogger.info("Handling customer action type: %s", action.rawType) - when (action.type()) { + return when (action.type()) { FINGERPRINT_MOBILE -> fingerprintMobile( - encodedConfiguration = action.value, threeDSService, callback + encodedConfiguration = action.value, + threeDSService = threeDSService ) CHALLENGE_MOBILE -> challengeMobile( - encodedChallenge = action.value, threeDSService, callback + encodedChallenge = action.value, + threeDSService = threeDSService ) - FINGERPRINT -> fingerprint(url = action.value, threeDSService, callback) - REDIRECT, URL -> redirect(url = action.value, threeDSService, callback) - UNSUPPORTED -> callback( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Unsupported 3DS customer action type: ${action.rawType}" - ).also { POLogger.error("%s", it) } + FINGERPRINT -> fingerprint( + url = action.value, + threeDSService = threeDSService ) + REDIRECT, URL -> redirect( + url = action.value, + threeDSService = threeDSService + ) + UNSUPPORTED -> { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Unsupported 3DS customer action type: ${action.rawType}" + ) + POLogger.error("%s", failure) + failure + } } } - private fun fingerprintMobile( + private suspend fun fingerprintMobile( encodedConfiguration: String, - threeDSService: PO3DSService, - callback: (ProcessOutResult) -> Unit - ) { - try { - moshi.adapter(PO3DS2Configuration::class.java) - .fromJson(String(Base64.decode(encodedConfiguration, Base64.NO_WRAP)))!! - .let { configuration -> - threeDSService.authenticationRequest(configuration) { result -> - when (result) { - is ProcessOutResult.Success -> callback( - ChallengeResponse(body = encode(result.value)), callback - ) - is ProcessOutResult.Failure -> { - POLogger.warn("Failed to create authentication request: %s", result) - callback(result) + threeDSService: PO3DSService + ): ProcessOutResult = + suspendCoroutine { continuation -> + try { + moshi.adapter(PO3DS2Configuration::class.java) + .fromJson(String(Base64.decode(encodedConfiguration, Base64.NO_WRAP)))!! + .let { configuration -> + threeDSService.authenticationRequest(configuration) { result -> + when (result) { + is ProcessOutResult.Success -> { + val token = encode(ChallengeResponse(body = encode(result.value))) + continuation.resume(ProcessOutResult.Success(token)) + } + is ProcessOutResult.Failure -> { + POLogger.warn("Failed to create authentication request: %s", result) + continuation.resume(result) + } } } } - } - } catch (e: Exception) { - callback( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Failed to decode configuration: ${e.message}", cause = e - ).also { POLogger.error("%s", it) } - ) + } catch (e: Exception) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Failed to decode configuration: ${e.message}", + cause = e + ) + POLogger.error("%s", failure) + continuation.resume(failure) + } } - } - private fun challengeMobile( + private suspend fun challengeMobile( encodedChallenge: String, - threeDSService: PO3DSService, - callback: (ProcessOutResult) -> Unit - ) { - try { - moshi.adapter(PO3DS2Challenge::class.java) - .fromJson(String(Base64.decode(encodedChallenge, Base64.NO_WRAP)))!! - .let { challenge -> - threeDSService.handle(challenge) { result -> - when (result) { - is ProcessOutResult.Success -> { - val body = if (result.value) - CHALLENGE_SUCCESS_RESPONSE_BODY - else CHALLENGE_FAILURE_RESPONSE_BODY - callback(ChallengeResponse(body = body), callback) - } - is ProcessOutResult.Failure -> { - POLogger.warn("Failed to handle challenge: %s", result) - callback(result) + threeDSService: PO3DSService + ): ProcessOutResult = + suspendCoroutine { continuation -> + try { + moshi.adapter(PO3DS2Challenge::class.java) + .fromJson(String(Base64.decode(encodedChallenge, Base64.NO_WRAP)))!! + .let { challenge -> + threeDSService.handle(challenge) { result -> + when (result) { + is ProcessOutResult.Success -> { + val body = if (result.value) + CHALLENGE_SUCCESS_RESPONSE_BODY + else CHALLENGE_FAILURE_RESPONSE_BODY + val token = encode(ChallengeResponse(body = body)) + continuation.resume(ProcessOutResult.Success(token)) + } + is ProcessOutResult.Failure -> { + POLogger.warn("Failed to handle challenge: %s", result) + continuation.resume(result) + } } } } - } - } catch (e: Exception) { - callback( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Failed to decode challenge: ${e.message}", cause = e - ).also { POLogger.error("%s", it) } - ) + } catch (e: Exception) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Failed to decode challenge: ${e.message}", + cause = e + ) + POLogger.error("%s", failure) + continuation.resume(failure) + } } - } - private fun fingerprint( + private suspend fun fingerprint( url: String, - threeDSService: PO3DSService, - callback: (ProcessOutResult) -> Unit - ) { - try { - threeDSService.handle( - PO3DSRedirect( - url = java.net.URL(url), - timeoutSeconds = WEB_FINGERPRINT_TIMEOUT_SECONDS - ) - ) { result -> - when (result) { - is ProcessOutResult.Success -> callback(result) - is ProcessOutResult.Failure -> - when (result.code == POFailure.Code.Timeout()) { - true -> callback( - ChallengeResponse( - body = WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY, - url = url - ), callback - ) - false -> { - POLogger.warn("Failed to handle URL fingerprint: %s", result) - callback(result) + threeDSService: PO3DSService + ): ProcessOutResult = + suspendCoroutine { continuation -> + try { + threeDSService.handle( + PO3DSRedirect( + url = java.net.URL(url), + timeoutSeconds = WEB_FINGERPRINT_TIMEOUT_SECONDS + ) + ) { result -> + when (result) { + is ProcessOutResult.Success -> continuation.resume(result) + is ProcessOutResult.Failure -> + when (result.code == Timeout()) { + true -> { + val token = encode( + ChallengeResponse( + body = WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY, + url = url + ) + ) + continuation.resume(ProcessOutResult.Success(token)) + } + false -> { + POLogger.warn("Failed to handle URL fingerprint: %s", result) + continuation.resume(result) + } } - } + } } - } - } catch (e: Exception) { - when (e) { - is MalformedURLException -> callback( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Failed to parse fingerprint URL: $url", cause = e - ).also { POLogger.error("%s", it) } - ) - else -> callback( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Failed to handle fingerprint with URL: $url", cause = e - ).also { POLogger.error("%s", it) } + } catch (e: Exception) { + val message = when (e) { + is MalformedURLException -> "Failed to parse fingerprint URL: $url" + else -> "Failed to handle fingerprint with URL: $url" + } + val failure = ProcessOutResult.Failure( + code = Internal(), + message = message, + cause = e ) + POLogger.error("%s", failure) + continuation.resume(failure) } } - } - private fun redirect( + private suspend fun redirect( url: String, - threeDSService: PO3DSService, - callback: (ProcessOutResult) -> Unit - ) { - try { - threeDSService.handle(PO3DSRedirect(url = java.net.URL(url)), callback) - } catch (e: MalformedURLException) { - callback( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Failed to parse redirect URL: $url", cause = e - ).also { POLogger.error("%s", it) } - ) + threeDSService: PO3DSService + ): ProcessOutResult = + suspendCoroutine { continuation -> + try { + threeDSService.handle( + PO3DSRedirect(url = java.net.URL(url)) + ) { result -> + continuation.resume(result) + } + } catch (e: MalformedURLException) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Failed to parse redirect URL: $url", + cause = e + ) + POLogger.error("%s", failure) + continuation.resume(failure) + } } - } private fun encode(request: PO3DS2AuthenticationRequest): String { val sdkEphemeralPublicKey = if (request.sdkEphemeralPublicKey.isBlank()) @@ -197,10 +218,9 @@ internal class DefaultCustomerActionsService( return moshi.adapter(ThreeDS2AuthenticationRequest::class.java).toJson(authRequest) } - private fun callback(response: ChallengeResponse, callback: (ProcessOutResult) -> Unit) { + private fun encode(response: ChallengeResponse): String { val bytes = moshi.adapter(ChallengeResponse::class.java).toJson(response).toByteArray() - val token = TOKEN_PREFIX + Base64.encodeToString(bytes, Base64.NO_WRAP) - callback(ProcessOutResult.Success(token)) + return TOKEN_PREFIX + Base64.encodeToString(bytes, Base64.NO_WRAP) } @JsonClass(generateAdapter = true) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt index 590833aa0..e267ef58f 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt @@ -33,18 +33,16 @@ internal class DefaultCustomerTokensService( when (val result = repository.assignCustomerToken(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - customerActionsService.handle(action, threeDSService) { serviceResult -> - when (serviceResult) { - is ProcessOutResult.Success -> - assignCustomerToken( - request.copy(source = serviceResult.value), - threeDSService - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - scope.launch { - _assignCustomerTokenResult.emit(serviceResult) - } + when (val serviceResult = customerActionsService.handle(action, threeDSService)) { + is ProcessOutResult.Success -> + assignCustomerToken( + request.copy(source = serviceResult.value), + threeDSService + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + scope.launch { + _assignCustomerTokenResult.emit(serviceResult) } } } @@ -102,19 +100,17 @@ internal class DefaultCustomerTokensService( when (val result = repository.assignCustomerToken(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - customerActionsService.handle(action, threeDSService) { serviceResult -> - @Suppress("DEPRECATION") - when (serviceResult) { - is ProcessOutResult.Success -> - assignCustomerToken( - request.copy(source = serviceResult.value), - threeDSService, - callback - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - callback(serviceResult) - } + when (val serviceResult = customerActionsService.handle(action, threeDSService)) { + is ProcessOutResult.Success -> + @Suppress("DEPRECATION") + assignCustomerToken( + request.copy(source = serviceResult.value), + threeDSService, + callback + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + callback(serviceResult) } } } ?: run { diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt index 09f60c049..53399b039 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt @@ -36,18 +36,16 @@ internal class DefaultInvoicesService( when (val result = repository.authorizeInvoice(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - customerActionsService.handle(action, threeDSService) { serviceResult -> - when (serviceResult) { - is ProcessOutResult.Success -> - authorizeInvoice( - request.copy(source = serviceResult.value), - threeDSService - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - scope.launch { - _authorizeInvoiceResult.emit(serviceResult) - } + when (val serviceResult = customerActionsService.handle(action, threeDSService)) { + is ProcessOutResult.Success -> + authorizeInvoice( + request.copy(source = serviceResult.value), + threeDSService + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + scope.launch { + _authorizeInvoiceResult.emit(serviceResult) } } } @@ -83,19 +81,17 @@ internal class DefaultInvoicesService( when (val result = repository.authorizeInvoice(request)) { is ProcessOutResult.Success -> result.value.customerAction?.let { action -> - customerActionsService.handle(action, threeDSService) { serviceResult -> - @Suppress("DEPRECATION") - when (serviceResult) { - is ProcessOutResult.Success -> - authorizeInvoice( - request.copy(source = serviceResult.value), - threeDSService, - callback - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - callback(serviceResult) - } + when (val serviceResult = customerActionsService.handle(action, threeDSService)) { + is ProcessOutResult.Success -> + @Suppress("DEPRECATION") + authorizeInvoice( + request.copy(source = serviceResult.value), + threeDSService, + callback + ) + is ProcessOutResult.Failure -> { + threeDSService.cleanup() + callback(serviceResult) } } } ?: run { From 1aa8df424f9ee7c7b0f1f08d3ee66f4693b3e20d Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 17:48:28 +0300 Subject: [PATCH 07/21] DefaultCustomerActionsService: removed redundant private modifier from constants --- .../sdk/api/service/DefaultCustomerActionsService.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index c915be545..72aba7352 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -22,12 +22,12 @@ internal class DefaultCustomerActionsService( ) : CustomerActionsService { private companion object { - private const val DEVICE_CHANNEL = "app" - private const val TOKEN_PREFIX = "gway_req_" - private const val CHALLENGE_SUCCESS_RESPONSE_BODY = """{ "transStatus": "Y" }""" - private const val CHALLENGE_FAILURE_RESPONSE_BODY = """{ "transStatus": "N" }""" - private const val WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY = """{ "threeDS2FingerprintTimeout": true }""" - private const val WEB_FINGERPRINT_TIMEOUT_SECONDS = 10 + const val DEVICE_CHANNEL = "app" + const val TOKEN_PREFIX = "gway_req_" + const val CHALLENGE_SUCCESS_RESPONSE_BODY = """{ "transStatus": "Y" }""" + const val CHALLENGE_FAILURE_RESPONSE_BODY = """{ "transStatus": "N" }""" + const val WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY = """{ "threeDS2FingerprintTimeout": true }""" + const val WEB_FINGERPRINT_TIMEOUT_SECONDS = 10 } override suspend fun handle( From a49772cc1687f9ecc0c14c2ffc01d4f476dea8e1 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 18:16:32 +0300 Subject: [PATCH 08/21] GATEWAY_TOKEN_PREFIX --- .../sdk/api/service/DefaultCustomerActionsService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index 72aba7352..ba71e6998 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -23,7 +23,7 @@ internal class DefaultCustomerActionsService( private companion object { const val DEVICE_CHANNEL = "app" - const val TOKEN_PREFIX = "gway_req_" + const val GATEWAY_TOKEN_PREFIX = "gway_req_" const val CHALLENGE_SUCCESS_RESPONSE_BODY = """{ "transStatus": "Y" }""" const val CHALLENGE_FAILURE_RESPONSE_BODY = """{ "transStatus": "N" }""" const val WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY = """{ "threeDS2FingerprintTimeout": true }""" @@ -220,7 +220,7 @@ internal class DefaultCustomerActionsService( private fun encode(response: ChallengeResponse): String { val bytes = moshi.adapter(ChallengeResponse::class.java).toJson(response).toByteArray() - return TOKEN_PREFIX + Base64.encodeToString(bytes, Base64.NO_WRAP) + return GATEWAY_TOKEN_PREFIX + Base64.encodeToString(bytes, Base64.NO_WRAP) } @JsonClass(generateAdapter = true) From b339202b6d5b3f261694f6b913d324fa437c9556 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 18:57:44 +0300 Subject: [PATCH 09/21] GatewayRequest --- .../sdk/api/service/DefaultCustomerActionsService.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index ba71e6998..e6fde7aa8 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -75,7 +75,7 @@ internal class DefaultCustomerActionsService( threeDSService.authenticationRequest(configuration) { result -> when (result) { is ProcessOutResult.Success -> { - val token = encode(ChallengeResponse(body = encode(result.value))) + val token = encode(GatewayRequest(body = encode(result.value))) continuation.resume(ProcessOutResult.Success(token)) } is ProcessOutResult.Failure -> { @@ -111,7 +111,7 @@ internal class DefaultCustomerActionsService( val body = if (result.value) CHALLENGE_SUCCESS_RESPONSE_BODY else CHALLENGE_FAILURE_RESPONSE_BODY - val token = encode(ChallengeResponse(body = body)) + val token = encode(GatewayRequest(body = body)) continuation.resume(ProcessOutResult.Success(token)) } is ProcessOutResult.Failure -> { @@ -150,7 +150,7 @@ internal class DefaultCustomerActionsService( when (result.code == Timeout()) { true -> { val token = encode( - ChallengeResponse( + GatewayRequest( body = WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY, url = url ) @@ -218,13 +218,13 @@ internal class DefaultCustomerActionsService( return moshi.adapter(ThreeDS2AuthenticationRequest::class.java).toJson(authRequest) } - private fun encode(response: ChallengeResponse): String { - val bytes = moshi.adapter(ChallengeResponse::class.java).toJson(response).toByteArray() + private fun encode(request: GatewayRequest): String { + val bytes = moshi.adapter(GatewayRequest::class.java).toJson(request).toByteArray() return GATEWAY_TOKEN_PREFIX + Base64.encodeToString(bytes, Base64.NO_WRAP) } @JsonClass(generateAdapter = true) - internal data class ChallengeResponse( + internal data class GatewayRequest( val body: String, val url: String? = null ) From 9a9b4d9e5eb28ef855a42817f9837fde1735b561 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 19:04:06 +0300 Subject: [PATCH 10/21] gatewayToken --- .../sdk/api/service/DefaultCustomerActionsService.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index e6fde7aa8..abd295988 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -75,8 +75,8 @@ internal class DefaultCustomerActionsService( threeDSService.authenticationRequest(configuration) { result -> when (result) { is ProcessOutResult.Success -> { - val token = encode(GatewayRequest(body = encode(result.value))) - continuation.resume(ProcessOutResult.Success(token)) + val gatewayToken = encode(GatewayRequest(body = encode(result.value))) + continuation.resume(ProcessOutResult.Success(gatewayToken)) } is ProcessOutResult.Failure -> { POLogger.warn("Failed to create authentication request: %s", result) @@ -111,8 +111,8 @@ internal class DefaultCustomerActionsService( val body = if (result.value) CHALLENGE_SUCCESS_RESPONSE_BODY else CHALLENGE_FAILURE_RESPONSE_BODY - val token = encode(GatewayRequest(body = body)) - continuation.resume(ProcessOutResult.Success(token)) + val gatewayToken = encode(GatewayRequest(body = body)) + continuation.resume(ProcessOutResult.Success(gatewayToken)) } is ProcessOutResult.Failure -> { POLogger.warn("Failed to handle challenge: %s", result) @@ -149,13 +149,13 @@ internal class DefaultCustomerActionsService( is ProcessOutResult.Failure -> when (result.code == Timeout()) { true -> { - val token = encode( + val gatewayToken = encode( GatewayRequest( body = WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY, url = url ) ) - continuation.resume(ProcessOutResult.Success(token)) + continuation.resume(ProcessOutResult.Success(gatewayToken)) } false -> { POLogger.warn("Failed to handle URL fingerprint: %s", result) From 8f8223591003c96980ce4a9b93930133b9fd831d Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 19:17:06 +0300 Subject: [PATCH 11/21] Const names --- .../sdk/api/service/DefaultCustomerActionsService.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index abd295988..3ba864cac 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -24,9 +24,9 @@ internal class DefaultCustomerActionsService( private companion object { const val DEVICE_CHANNEL = "app" const val GATEWAY_TOKEN_PREFIX = "gway_req_" - const val CHALLENGE_SUCCESS_RESPONSE_BODY = """{ "transStatus": "Y" }""" - const val CHALLENGE_FAILURE_RESPONSE_BODY = """{ "transStatus": "N" }""" - const val WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY = """{ "threeDS2FingerprintTimeout": true }""" + const val CHALLENGE_SUCCESS_GATEWAY_REQUEST_BODY = """{ "transStatus": "Y" }""" + const val CHALLENGE_FAILURE_GATEWAY_REQUEST_BODY = """{ "transStatus": "N" }""" + const val WEB_FINGERPRINT_TIMEOUT_GATEWAY_REQUEST_BODY = """{ "threeDS2FingerprintTimeout": true }""" const val WEB_FINGERPRINT_TIMEOUT_SECONDS = 10 } @@ -109,8 +109,8 @@ internal class DefaultCustomerActionsService( when (result) { is ProcessOutResult.Success -> { val body = if (result.value) - CHALLENGE_SUCCESS_RESPONSE_BODY - else CHALLENGE_FAILURE_RESPONSE_BODY + CHALLENGE_SUCCESS_GATEWAY_REQUEST_BODY + else CHALLENGE_FAILURE_GATEWAY_REQUEST_BODY val gatewayToken = encode(GatewayRequest(body = body)) continuation.resume(ProcessOutResult.Success(gatewayToken)) } @@ -151,7 +151,7 @@ internal class DefaultCustomerActionsService( true -> { val gatewayToken = encode( GatewayRequest( - body = WEB_FINGERPRINT_TIMEOUT_RESPONSE_BODY, + body = WEB_FINGERPRINT_TIMEOUT_GATEWAY_REQUEST_BODY, url = url ) ) From b9fe1e6a5859d89ace3bcfd2c3d889b666c2fe57 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 10 Apr 2025 20:14:45 +0300 Subject: [PATCH 12/21] Code improvements --- .../service/DefaultCustomerActionsService.kt | 72 +++++++++---------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index 3ba864cac..9f346a6d7 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -11,6 +11,8 @@ import com.processout.sdk.core.POFailure.Code.Internal import com.processout.sdk.core.POFailure.Code.Timeout import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.core.onFailure +import com.processout.sdk.core.onSuccess import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import java.net.MalformedURLException @@ -73,15 +75,13 @@ internal class DefaultCustomerActionsService( .fromJson(String(Base64.decode(encodedConfiguration, Base64.NO_WRAP)))!! .let { configuration -> threeDSService.authenticationRequest(configuration) { result -> - when (result) { - is ProcessOutResult.Success -> { - val gatewayToken = encode(GatewayRequest(body = encode(result.value))) - continuation.resume(ProcessOutResult.Success(gatewayToken)) - } - is ProcessOutResult.Failure -> { - POLogger.warn("Failed to create authentication request: %s", result) - continuation.resume(result) - } + result.onSuccess { authenticationRequest -> + val gatewayRequest = GatewayRequest(body = encode(authenticationRequest)) + val gatewayToken = encode(gatewayRequest) + continuation.resume(ProcessOutResult.Success(gatewayToken)) + }.onFailure { failure -> + POLogger.warn("Failed to create authentication request: %s", failure) + continuation.resume(failure) } } } @@ -106,18 +106,16 @@ internal class DefaultCustomerActionsService( .fromJson(String(Base64.decode(encodedChallenge, Base64.NO_WRAP)))!! .let { challenge -> threeDSService.handle(challenge) { result -> - when (result) { - is ProcessOutResult.Success -> { - val body = if (result.value) - CHALLENGE_SUCCESS_GATEWAY_REQUEST_BODY - else CHALLENGE_FAILURE_GATEWAY_REQUEST_BODY - val gatewayToken = encode(GatewayRequest(body = body)) - continuation.resume(ProcessOutResult.Success(gatewayToken)) - } - is ProcessOutResult.Failure -> { - POLogger.warn("Failed to handle challenge: %s", result) - continuation.resume(result) - } + result.onSuccess { didSucceed -> + val body = if (didSucceed) + CHALLENGE_SUCCESS_GATEWAY_REQUEST_BODY + else CHALLENGE_FAILURE_GATEWAY_REQUEST_BODY + + val gatewayToken = encode(GatewayRequest(body = body)) + continuation.resume(ProcessOutResult.Success(gatewayToken)) + }.onFailure { failure -> + POLogger.warn("Failed to handle challenge: %s", failure) + continuation.resume(failure) } } } @@ -147,20 +145,16 @@ internal class DefaultCustomerActionsService( when (result) { is ProcessOutResult.Success -> continuation.resume(result) is ProcessOutResult.Failure -> - when (result.code == Timeout()) { - true -> { - val gatewayToken = encode( - GatewayRequest( - body = WEB_FINGERPRINT_TIMEOUT_GATEWAY_REQUEST_BODY, - url = url - ) - ) - continuation.resume(ProcessOutResult.Success(gatewayToken)) - } - false -> { - POLogger.warn("Failed to handle URL fingerprint: %s", result) - continuation.resume(result) - } + if (result.code == Timeout()) { + val gatewayRequest = GatewayRequest( + body = WEB_FINGERPRINT_TIMEOUT_GATEWAY_REQUEST_BODY, + url = url + ) + val gatewayToken = encode(gatewayRequest) + continuation.resume(ProcessOutResult.Success(gatewayToken)) + } else { + POLogger.warn("Failed to handle URL fingerprint: %s", result) + continuation.resume(result) } } } @@ -207,7 +201,7 @@ internal class DefaultCustomerActionsService( else moshi.adapter(EphemeralPublicKey::class.java) .fromJson(request.sdkEphemeralPublicKey)!! - val authRequest = ThreeDS2AuthenticationRequest( + val authenticationRequest = ThreeDS2AuthenticationRequest( deviceChannel = DEVICE_CHANNEL, sdkAppID = request.sdkAppId, sdkEncData = request.deviceData, @@ -215,11 +209,11 @@ internal class DefaultCustomerActionsService( sdkReferenceNumber = request.sdkReferenceNumber, sdkTransID = request.sdkTransactionId ) - return moshi.adapter(ThreeDS2AuthenticationRequest::class.java).toJson(authRequest) + return moshi.adapter(ThreeDS2AuthenticationRequest::class.java).toJson(authenticationRequest) } - private fun encode(request: GatewayRequest): String { - val bytes = moshi.adapter(GatewayRequest::class.java).toJson(request).toByteArray() + private fun encode(gatewayRequest: GatewayRequest): String { + val bytes = moshi.adapter(GatewayRequest::class.java).toJson(gatewayRequest).toByteArray() return GATEWAY_TOKEN_PREFIX + Base64.encodeToString(bytes, Base64.NO_WRAP) } From 5c434aa6055621e31cc9f8ff7e4c6ab6a3d7cc66 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 11 Apr 2025 17:16:20 +0300 Subject: [PATCH 13/21] action -> customerAction --- .../sdk/api/service/CustomerActionsService.kt | 2 +- .../api/service/DefaultCustomerActionsService.kt | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt index c9bab39b4..f30541467 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/CustomerActionsService.kt @@ -6,7 +6,7 @@ import com.processout.sdk.core.ProcessOutResult internal interface CustomerActionsService { suspend fun handle( - action: CustomerAction, + customerAction: CustomerAction, threeDSService: PO3DSService ): ProcessOutResult } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt index 9f346a6d7..68c6d0872 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerActionsService.kt @@ -33,31 +33,31 @@ internal class DefaultCustomerActionsService( } override suspend fun handle( - action: CustomerAction, + customerAction: CustomerAction, threeDSService: PO3DSService ): ProcessOutResult { - POLogger.info("Handling customer action type: %s", action.rawType) - return when (action.type()) { + POLogger.info("Handling customer action type: %s", customerAction.rawType) + return when (customerAction.type()) { FINGERPRINT_MOBILE -> fingerprintMobile( - encodedConfiguration = action.value, + encodedConfiguration = customerAction.value, threeDSService = threeDSService ) CHALLENGE_MOBILE -> challengeMobile( - encodedChallenge = action.value, + encodedChallenge = customerAction.value, threeDSService = threeDSService ) FINGERPRINT -> fingerprint( - url = action.value, + url = customerAction.value, threeDSService = threeDSService ) REDIRECT, URL -> redirect( - url = action.value, + url = customerAction.value, threeDSService = threeDSService ) UNSUPPORTED -> { val failure = ProcessOutResult.Failure( code = Internal(), - message = "Unsupported 3DS customer action type: ${action.rawType}" + message = "Unsupported 3DS customer action type: ${customerAction.rawType}" ) POLogger.error("%s", failure) failure From e012f13c6139d3e4785ffe0ea4bba50dbe6f62d3 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 14 Apr 2025 20:44:54 +0300 Subject: [PATCH 14/21] Refactor invoice auth --- .../sdk/api/service/DefaultInvoicesService.kt | 175 +++++++++++------- .../sdk/api/service/POInvoicesService.kt | 20 +- 2 files changed, 129 insertions(+), 66 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt index 53399b039..39751f8a6 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt @@ -1,3 +1,5 @@ +@file:Suppress("OVERRIDE_DEPRECATION") + package com.processout.sdk.api.service import com.processout.sdk.api.model.request.POCreateInvoiceRequest @@ -9,15 +11,13 @@ import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethod import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodCapture import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.repository.InvoicesRepository -import com.processout.sdk.core.ProcessOutCallback -import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.* +import com.processout.sdk.core.POFailure.Code.Cancelled import com.processout.sdk.core.logger.POLogAttribute import com.processout.sdk.core.logger.POLogger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch internal class DefaultInvoicesService( private val scope: CoroutineScope, @@ -33,78 +33,133 @@ internal class DefaultInvoicesService( threeDSService: PO3DSService ) { scope.launch { - when (val result = repository.authorizeInvoice(request)) { - is ProcessOutResult.Success -> - result.value.customerAction?.let { action -> - when (val serviceResult = customerActionsService.handle(action, threeDSService)) { - is ProcessOutResult.Success -> - authorizeInvoice( - request.copy(source = serviceResult.value), - threeDSService - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - scope.launch { - _authorizeInvoiceResult.emit(serviceResult) - } - } - } - } ?: run { + val logAttributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) + repository.authorizeInvoice(request) + .onSuccess { response -> + if (response.customerAction == null) { threeDSService.cleanup() - scope.launch { - _authorizeInvoiceResult.emit( - ProcessOutResult.Success(request.invoiceId) + _authorizeInvoiceResult.emit( + ProcessOutResult.Success(request.invoiceId) + ) + return@onSuccess + } + customerActionsService.handle(response.customerAction, threeDSService) + .onSuccess { newSource -> + authorizeInvoice( + request.copy(source = newSource), + threeDSService + ) + }.onFailure { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes ) + threeDSService.cleanup() + _authorizeInvoiceResult.emit(failure) } - } - is ProcessOutResult.Failure -> { + }.onFailure { failure -> POLogger.warn( - message = "Failed to authorize invoice: %s", result, - attributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes ) threeDSService.cleanup() - scope.launch { _authorizeInvoiceResult.emit(result) } + _authorizeInvoiceResult.emit(failure) } - } } } - @Deprecated( - message = "Use function authorizeInvoice(request, threeDSService)", - replaceWith = ReplaceWith("authorizeInvoice(request, threeDSService)") - ) override fun authorizeInvoice( request: POInvoiceAuthorizationRequest, threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit ): Job = scope.launch { - when (val result = repository.authorizeInvoice(request)) { - is ProcessOutResult.Success -> - result.value.customerAction?.let { action -> - when (val serviceResult = customerActionsService.handle(action, threeDSService)) { - is ProcessOutResult.Success -> - @Suppress("DEPRECATION") - authorizeInvoice( - request.copy(source = serviceResult.value), - threeDSService, - callback - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - callback(serviceResult) - } - } - } ?: run { + val logAttributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) + repository.authorizeInvoice(request) + .onSuccess { response -> + if (response.customerAction == null) { threeDSService.cleanup() callback(ProcessOutResult.Success(Unit)) + return@onSuccess } - is ProcessOutResult.Failure -> { + customerActionsService.handle(response.customerAction, threeDSService) + .onSuccess { newSource -> + authorizeInvoice( + request.copy(source = newSource), + threeDSService, + callback + ) + }.onFailure { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + callback(failure) + } + }.onFailure { failure -> POLogger.warn( - message = "Failed to authorize invoice: %s", result, - attributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + callback(failure) + } + } + + override suspend fun authorize( + request: POInvoiceAuthorizationRequest, + threeDSService: PO3DSService + ): ProcessOutResult { + val logAttributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) + return try { + repository.authorizeInvoice(request) + .fold( + onSuccess = { response -> + if (response.customerAction == null) { + threeDSService.cleanup() + return@fold ProcessOutResult.Success(Unit) + } + customerActionsService.handle(response.customerAction, threeDSService) + .fold( + onSuccess = { newSource -> + authorize( + request.copy(source = newSource), + threeDSService + ) + }, + onFailure = { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + failure + } + ) + }, + onFailure = { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + failure + } + ) + } catch (e: CancellationException) { + coroutineScope { + val failure = ProcessOutResult.Failure( + code = Cancelled, + message = e.message, + cause = e + ) + POLogger.info( + message = "Invoice authorization has been cancelled: %s", failure, + attributes = logAttributes ) threeDSService.cleanup() - callback(result) + ensureActive() + failure } } } @@ -165,10 +220,6 @@ internal class DefaultInvoicesService( repository.invoice(request, callback) } - @Deprecated( - message = "Use function invoice(request)", - replaceWith = ReplaceWith("invoice(request)") - ) override suspend fun invoice( invoiceId: String ): ProcessOutResult = @@ -179,10 +230,6 @@ internal class DefaultInvoicesService( ) ) - @Deprecated( - message = "Use function invoice(request, callback)", - replaceWith = ReplaceWith("invoice(request, callback)") - ) override fun invoice( invoiceId: String, callback: ProcessOutCallback diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt index 6fb3476e2..428236e7d 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt @@ -23,20 +23,28 @@ interface POInvoicesService { * Subscribe to this flow to collect result from [authorizeInvoice] invocation. * Result contains _invoiceId_ that was used for authorization. */ + @Deprecated(message = "Use function: authorize(request, threeDSService)") val authorizeInvoiceResult: SharedFlow> /** * Authorize invoice with the given request and 3DS service implementation. * Collect result by subscribing to [authorizeInvoiceResult] flow before invoking invoice authorization. */ + @Deprecated( + message = "Use replacement function.", + replaceWith = ReplaceWith("authorize(request, threeDSService)") + ) fun authorizeInvoice( request: POInvoiceAuthorizationRequest, threeDSService: PO3DSService ) + /** + * Authorize invoice with the given request and 3DS service implementation. + */ @Deprecated( - message = "Use function authorizeInvoice(request, threeDSService)", - replaceWith = ReplaceWith("authorizeInvoice(request, threeDSService)") + message = "Use replacement function.", + replaceWith = ReplaceWith("authorize(request, threeDSService)") ) fun authorizeInvoice( request: POInvoiceAuthorizationRequest, @@ -44,6 +52,14 @@ interface POInvoicesService { callback: (ProcessOutResult) -> Unit ): Job + /** + * Authorize invoice with the given request and 3DS service implementation. + */ + suspend fun authorize( + request: POInvoiceAuthorizationRequest, + threeDSService: PO3DSService + ): ProcessOutResult + /** * Initiates native alternative payment with the given request. */ From 840f1952e68118bbb02fdc661714e71fa05514d0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 15 Apr 2025 12:38:37 +0300 Subject: [PATCH 15/21] Return Job from authorizeInvoice(), updated KDoc --- .../sdk/api/service/DefaultInvoicesService.kt | 64 +++++++++---------- .../sdk/api/service/POInvoicesService.kt | 9 ++- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt index 39751f8a6..696e33a1c 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt @@ -31,41 +31,39 @@ internal class DefaultInvoicesService( override fun authorizeInvoice( request: POInvoiceAuthorizationRequest, threeDSService: PO3DSService - ) { - scope.launch { - val logAttributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) - repository.authorizeInvoice(request) - .onSuccess { response -> - if (response.customerAction == null) { - threeDSService.cleanup() - _authorizeInvoiceResult.emit( - ProcessOutResult.Success(request.invoiceId) - ) - return@onSuccess - } - customerActionsService.handle(response.customerAction, threeDSService) - .onSuccess { newSource -> - authorizeInvoice( - request.copy(source = newSource), - threeDSService - ) - }.onFailure { failure -> - POLogger.warn( - message = "Failed to authorize invoice: %s", failure, - attributes = logAttributes - ) - threeDSService.cleanup() - _authorizeInvoiceResult.emit(failure) - } - }.onFailure { failure -> - POLogger.warn( - message = "Failed to authorize invoice: %s", failure, - attributes = logAttributes - ) + ): Job = scope.launch { + val logAttributes = mapOf(POLogAttribute.INVOICE_ID to request.invoiceId) + repository.authorizeInvoice(request) + .onSuccess { response -> + if (response.customerAction == null) { threeDSService.cleanup() - _authorizeInvoiceResult.emit(failure) + _authorizeInvoiceResult.emit( + ProcessOutResult.Success(request.invoiceId) + ) + return@onSuccess } - } + customerActionsService.handle(response.customerAction, threeDSService) + .onSuccess { newSource -> + authorizeInvoice( + request.copy(source = newSource), + threeDSService + ) + }.onFailure { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + _authorizeInvoiceResult.emit(failure) + } + }.onFailure { failure -> + POLogger.warn( + message = "Failed to authorize invoice: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + _authorizeInvoiceResult.emit(failure) + } } override fun authorizeInvoice( diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt index 428236e7d..062651f2f 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.SharedFlow interface POInvoicesService { /** - * Subscribe to this flow to collect result from [authorizeInvoice] invocation. + * Subscribe to this flow to collect the result from [authorizeInvoice] invocation. * Result contains _invoiceId_ that was used for authorization. */ @Deprecated(message = "Use function: authorize(request, threeDSService)") @@ -28,7 +28,8 @@ interface POInvoicesService { /** * Authorize invoice with the given request and 3DS service implementation. - * Collect result by subscribing to [authorizeInvoiceResult] flow before invoking invoice authorization. + * Collect the result by subscribing to [authorizeInvoiceResult] flow before invoking this function. + * Returns coroutine job. */ @Deprecated( message = "Use replacement function.", @@ -37,10 +38,12 @@ interface POInvoicesService { fun authorizeInvoice( request: POInvoiceAuthorizationRequest, threeDSService: PO3DSService - ) + ): Job /** * Authorize invoice with the given request and 3DS service implementation. + * Result provided in the callback. + * Returns coroutine job. */ @Deprecated( message = "Use replacement function.", From ea0e023ebacdbba331f47de6f84f988e1f3ef3dd Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 15 Apr 2025 13:11:12 +0300 Subject: [PATCH 16/21] Use new authorize() on DC --- .../sdk/ui/checkout/DynamicCheckoutInteractor.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt index 887dd5f50..5a704e94b 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt @@ -896,17 +896,17 @@ internal class DynamicCheckoutInteractor( } } - @Suppress("DEPRECATION") private fun collectInvoiceAuthorizationRequest() { eventDispatcher.subscribeForResponse( coroutineScope = interactorScope ) { response -> POLogger.info("Authorizing the invoice.", attributes = logAttributes) val threeDSService = PODefaultProxy3DSService() - val job = invoicesService.authorizeInvoice( - request = response.request, - threeDSService = threeDSService - ) { result -> + val job = interactorScope.launch { + val result = invoicesService.authorize( + request = response.request, + threeDSService = threeDSService + ) handleInvoiceAuthorization( state = _state.value, invoiceId = response.request.invoiceId, From 5fd0160dc7a16bc634f066e0ccaa8b96b7892c26 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 15 Apr 2025 16:00:59 +0300 Subject: [PATCH 17/21] Reimplement assigning of a customer token --- .../service/DefaultCustomerTokensService.kt | 263 +++++++++++------- .../api/service/POCustomerTokensService.kt | 34 ++- 2 files changed, 190 insertions(+), 107 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt index e267ef58f..230b3c572 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultCustomerTokensService.kt @@ -1,3 +1,5 @@ +@file:Suppress("OVERRIDE_DEPRECATION") + package com.processout.sdk.api.service import com.processout.sdk.api.model.request.POAssignCustomerTokenRequest @@ -7,14 +9,17 @@ import com.processout.sdk.api.model.request.PODeleteCustomerTokenRequest import com.processout.sdk.api.model.response.POCustomer import com.processout.sdk.api.model.response.POCustomerToken import com.processout.sdk.api.repository.CustomerTokensRepository -import com.processout.sdk.core.POFailure +import com.processout.sdk.core.POFailure.Code.Cancelled +import com.processout.sdk.core.POFailure.Code.Internal import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.fold import com.processout.sdk.core.logger.POLogAttribute import com.processout.sdk.core.logger.POLogger -import kotlinx.coroutines.CoroutineScope +import com.processout.sdk.core.onFailure +import com.processout.sdk.core.onSuccess +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch internal class DefaultCustomerTokensService( private val scope: CoroutineScope, @@ -28,121 +33,177 @@ internal class DefaultCustomerTokensService( override fun assignCustomerToken( request: POAssignCustomerTokenRequest, threeDSService: PO3DSService - ) { - scope.launch { - when (val result = repository.assignCustomerToken(request)) { - is ProcessOutResult.Success -> - result.value.customerAction?.let { action -> - when (val serviceResult = customerActionsService.handle(action, threeDSService)) { - is ProcessOutResult.Success -> - assignCustomerToken( - request.copy(source = serviceResult.value), - threeDSService - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - scope.launch { - _assignCustomerTokenResult.emit(serviceResult) - } - } - } - } ?: run { - threeDSService.cleanup() - result.value.token?.let { token -> - scope.launch { - _assignCustomerTokenResult.emit( - ProcessOutResult.Success(token) - ) - } - } ?: scope.launch { - _assignCustomerTokenResult.emit( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Customer token is 'null'." - ).also { failure -> - POLogger.warn( - message = "Failed to assign customer token: %s", failure, - attributes = mapOf( - POLogAttribute.CUSTOMER_ID to request.customerId, - POLogAttribute.CUSTOMER_TOKEN_ID to request.tokenId - ) - ) - } - ) - } - } - is ProcessOutResult.Failure -> { - POLogger.warn( - message = "Failed to assign customer token: %s", result, - attributes = mapOf( - POLogAttribute.CUSTOMER_ID to request.customerId, - POLogAttribute.CUSTOMER_TOKEN_ID to request.tokenId + ): Job = scope.launch { + val logAttributes = mapOf( + POLogAttribute.CUSTOMER_ID to request.customerId, + POLogAttribute.CUSTOMER_TOKEN_ID to request.tokenId + ) + repository.assignCustomerToken(request) + .onSuccess { response -> + if (response.customerAction == null) { + threeDSService.cleanup() + if (response.token == null) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Customer token is null." ) + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes + ) + _assignCustomerTokenResult.emit(failure) + return@onSuccess + } + _assignCustomerTokenResult.emit( + ProcessOutResult.Success(response.token) ) - threeDSService.cleanup() - scope.launch { _assignCustomerTokenResult.emit(result) } + return@onSuccess } + customerActionsService.handle(response.customerAction, threeDSService) + .onSuccess { newSource -> + assignCustomerToken( + request.copy(source = newSource), + threeDSService + ) + }.onFailure { failure -> + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + _assignCustomerTokenResult.emit(failure) + } + }.onFailure { failure -> + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + _assignCustomerTokenResult.emit(failure) } - } } - - @Deprecated( - message = "Use function assignCustomerToken(request, threeDSService)", - replaceWith = ReplaceWith("assignCustomerToken(request, threeDSService)") - ) override fun assignCustomerToken( request: POAssignCustomerTokenRequest, threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit - ) { - scope.launch { - when (val result = repository.assignCustomerToken(request)) { - is ProcessOutResult.Success -> - result.value.customerAction?.let { action -> - when (val serviceResult = customerActionsService.handle(action, threeDSService)) { - is ProcessOutResult.Success -> - @Suppress("DEPRECATION") - assignCustomerToken( - request.copy(source = serviceResult.value), - threeDSService, - callback - ) - is ProcessOutResult.Failure -> { - threeDSService.cleanup() - callback(serviceResult) - } - } - } ?: run { + ): Job = scope.launch { + val logAttributes = mapOf( + POLogAttribute.CUSTOMER_ID to request.customerId, + POLogAttribute.CUSTOMER_TOKEN_ID to request.tokenId + ) + repository.assignCustomerToken(request) + .onSuccess { response -> + if (response.customerAction == null) { + threeDSService.cleanup() + if (response.token == null) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Customer token is null." + ) + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes + ) + callback(failure) + return@onSuccess + } + callback(ProcessOutResult.Success(response.token)) + return@onSuccess + } + customerActionsService.handle(response.customerAction, threeDSService) + .onSuccess { newSource -> + assignCustomerToken( + request.copy(source = newSource), + threeDSService, + callback + ) + }.onFailure { failure -> + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes + ) threeDSService.cleanup() - result.value.token?.let { token -> - callback(ProcessOutResult.Success(token)) - } ?: callback( - ProcessOutResult.Failure( - POFailure.Code.Internal(), - "Customer token is 'null'." - ).also { failure -> + callback(failure) + } + }.onFailure { failure -> + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + callback(failure) + } + } + + override suspend fun assign( + request: POAssignCustomerTokenRequest, + threeDSService: PO3DSService + ): ProcessOutResult { + val logAttributes = mapOf( + POLogAttribute.CUSTOMER_ID to request.customerId, + POLogAttribute.CUSTOMER_TOKEN_ID to request.tokenId + ) + return try { + repository.assignCustomerToken(request) + .fold( + onSuccess = { response -> + if (response.customerAction == null) { + threeDSService.cleanup() + if (response.token == null) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Customer token is null." + ) POLogger.warn( message = "Failed to assign customer token: %s", failure, - attributes = mapOf( - POLogAttribute.CUSTOMER_ID to request.customerId, - POLogAttribute.CUSTOMER_TOKEN_ID to request.tokenId - ) + attributes = logAttributes ) + return@fold failure } + return@fold ProcessOutResult.Success(response.token) + } + customerActionsService.handle(response.customerAction, threeDSService) + .fold( + onSuccess = { newSource -> + assign( + request.copy(source = newSource), + threeDSService + ) + }, + onFailure = { failure -> + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + failure + } + ) + }, + onFailure = { failure -> + POLogger.warn( + message = "Failed to assign customer token: %s", failure, + attributes = logAttributes ) + threeDSService.cleanup() + failure } - is ProcessOutResult.Failure -> { - POLogger.warn( - message = "Failed to assign customer token: %s", result, - attributes = mapOf( - POLogAttribute.CUSTOMER_ID to request.customerId, - POLogAttribute.CUSTOMER_TOKEN_ID to request.tokenId - ) - ) - threeDSService.cleanup() - callback(result) - } + ) + } catch (e: CancellationException) { + coroutineScope { + val failure = ProcessOutResult.Failure( + code = Cancelled, + message = e.message, + cause = e + ) + POLogger.info( + message = "Customer token assigning has been cancelled: %s", failure, + attributes = logAttributes + ) + threeDSService.cleanup() + ensureActive() + failure } } } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/POCustomerTokensService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/POCustomerTokensService.kt index d5488dffb..863313407 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/POCustomerTokensService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/POCustomerTokensService.kt @@ -8,6 +8,7 @@ import com.processout.sdk.api.model.response.POCustomer import com.processout.sdk.api.model.response.POCustomerToken import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.annotation.ProcessOutInternalApi +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.SharedFlow /** @@ -18,27 +19,48 @@ interface POCustomerTokensService { /** * Subscribe to this flow to collect result from [assignCustomerToken] invocation. */ + @Deprecated(message = "Use function: assign(request, threeDSService)") val assignCustomerTokenResult: SharedFlow> /** - * Assign new source to existing customer token and optionally verify it + * Assign new source to the existing customer token and optionally verify it * with the given request and 3DS service implementation. - * Collect result by subscribing to [assignCustomerTokenResult] flow before invoking token assignment. + * Collect the result by subscribing to [assignCustomerTokenResult] flow before invoking this function. + * Returns coroutine job. */ + @Deprecated( + message = "Use replacement function.", + replaceWith = ReplaceWith("assign(request, threeDSService)") + ) fun assignCustomerToken( request: POAssignCustomerTokenRequest, threeDSService: PO3DSService - ) + ): Job + /** + * Assign new source to the existing customer token and optionally verify it + * with the given request and 3DS service implementation. + * Result provided in the callback. + * Returns coroutine job. + */ @Deprecated( - message = "Use function assignCustomerToken(request, threeDSService)", - replaceWith = ReplaceWith("assignCustomerToken(request, threeDSService)") + message = "Use replacement function.", + replaceWith = ReplaceWith("assign(request, threeDSService)") ) fun assignCustomerToken( request: POAssignCustomerTokenRequest, threeDSService: PO3DSService, callback: (ProcessOutResult) -> Unit - ) + ): Job + + /** + * Assign new source to the existing customer token and optionally verify it + * with the given request and 3DS service implementation. + */ + suspend fun assign( + request: POAssignCustomerTokenRequest, + threeDSService: PO3DSService + ): ProcessOutResult /** * Deletes customer token. From 5c8a88ddb5e7760a39ef1b7bbf32e2a75e0c6b3d Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 15 Apr 2025 18:03:19 +0300 Subject: [PATCH 18/21] processTokenizedCard() - delegate, response event, launcher --- .../POCardTokenizationProcessingResponse.kt | 28 +++++++++++++++++++ .../POCardTokenizationDelegate.kt | 17 +++++++++++ .../POCardTokenizationLauncher.kt | 16 +++++++++++ 3 files changed, 61 insertions(+) create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt new file mode 100644 index 000000000..6f2d2fbce --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt @@ -0,0 +1,28 @@ +package com.processout.sdk.api.model.response + +import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.request.POCardTokenizationProcessingRequest +import com.processout.sdk.core.ProcessOutResult +import java.util.UUID + +/** + * Response after processed tokenized card (authorized invoice or assigned customer token). + * + * @param[uuid] Unique identifier of response that must be equal to UUID of request. + * @param[card] Tokenized card. + * @param[result] Result of tokenized card processing. + */ +data class POCardTokenizationProcessingResponse internal constructor( + override val uuid: UUID, + val card: POCard, + val result: ProcessOutResult +) : POEventDispatcher.Response + +/** + * Creates [POCardTokenizationProcessingResponse] from [POCardTokenizationProcessingRequest]. + * + * @param[result] Result of tokenized card processing. + */ +fun POCardTokenizationProcessingRequest.toResponse( + result: ProcessOutResult +) = POCardTokenizationProcessingResponse(uuid, card, result) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt index 7135fb66a..8697a8e18 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt @@ -1,6 +1,7 @@ package com.processout.sdk.ui.card.tokenization import com.processout.sdk.api.model.event.POCardTokenizationEvent +import com.processout.sdk.api.model.response.POCard import com.processout.sdk.api.model.response.POCardIssuerInformation import com.processout.sdk.core.ProcessOutResult @@ -14,6 +15,22 @@ interface POCardTokenizationDelegate { */ fun onEvent(event: POCardTokenizationEvent) {} + /** + * Allows to additionally process tokenized card before completion, + * for example to authorize an invoice or assign a customer token. + * Return the result from respective ProcessOut API if it was used. + * In case of a custom implementation you can pass + * _ProcessOutResult.Success(Unit)_ or appropriate _ProcessOutResult.Failure()_. + * Failure will be propagated to [shouldContinue] function. + * + * @param[card] Tokenized card. + * @param[saveCard] Indicates whether the user has chosen to save the card for future payments. + */ + suspend fun processTokenizedCard( + card: POCard, + saveCard: Boolean + ): ProcessOutResult = ProcessOutResult.Success(Unit) + /** * Allows to choose default preferred card scheme based on issuer information. * Primary card scheme is used by default. diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt index 100d5c2ce..cce16fcba 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt @@ -10,6 +10,7 @@ import com.processout.sdk.R import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.model.event.POCardTokenizationEvent import com.processout.sdk.api.model.request.POCardTokenizationPreferredSchemeRequest +import com.processout.sdk.api.model.request.POCardTokenizationProcessingRequest import com.processout.sdk.api.model.request.POCardTokenizationShouldContinueRequest import com.processout.sdk.api.model.response.POCard import com.processout.sdk.api.model.response.toResponse @@ -111,6 +112,7 @@ class POCardTokenizationLauncher private constructor( init { dispatchEvents() + dispatchTokenizedCard() dispatchPreferredScheme() dispatchShouldContinue() } @@ -121,6 +123,20 @@ class POCardTokenizationLauncher private constructor( ) { delegate.onEvent(it) } } + private fun dispatchTokenizedCard() { + eventDispatcher.subscribeForRequest( + coroutineScope = scope + ) { request -> + scope.launch { + val result = delegate.processTokenizedCard( + card = request.card, + saveCard = request.saveCard + ) + eventDispatcher.send(request.toResponse(result)) + } + } + } + private fun dispatchPreferredScheme() { eventDispatcher.subscribeForRequest( coroutineScope = scope From 773021721ab0305bca336440c6d1ea8af5618603 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 16 Apr 2025 13:04:54 +0300 Subject: [PATCH 19/21] requestToProcessTokenizedCard() --- .../POCardTokenizationProcessingResponse.kt | 13 ++-- .../CardTokenizationInteractor.kt | 72 +++++++++---------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt index 6f2d2fbce..c4d0e472d 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardTokenizationProcessingResponse.kt @@ -3,19 +3,18 @@ package com.processout.sdk.api.model.response import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.model.request.POCardTokenizationProcessingRequest import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.fold import java.util.UUID /** * Response after processed tokenized card (authorized invoice or assigned customer token). * * @param[uuid] Unique identifier of response that must be equal to UUID of request. - * @param[card] Tokenized card. * @param[result] Result of tokenized card processing. */ data class POCardTokenizationProcessingResponse internal constructor( override val uuid: UUID, - val card: POCard, - val result: ProcessOutResult + val result: ProcessOutResult ) : POEventDispatcher.Response /** @@ -25,4 +24,10 @@ data class POCardTokenizationProcessingResponse internal constructor( */ fun POCardTokenizationProcessingRequest.toResponse( result: ProcessOutResult -) = POCardTokenizationProcessingResponse(uuid, card, result) +) = POCardTokenizationProcessingResponse( + uuid, + result.fold( + onSuccess = { ProcessOutResult.Success(card) }, + onFailure = { it } + ) +) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index 89f6b3a9c..aff48ca61 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -9,9 +9,7 @@ import com.processout.sdk.api.dispatcher.card.tokenization.PODefaultCardTokeniza import com.processout.sdk.api.model.event.POCardTokenizationEvent import com.processout.sdk.api.model.event.POCardTokenizationEvent.* import com.processout.sdk.api.model.request.* -import com.processout.sdk.api.model.response.POCardIssuerInformation -import com.processout.sdk.api.model.response.POCardTokenizationPreferredSchemeResponse -import com.processout.sdk.api.model.response.POCardTokenizationShouldContinueResponse +import com.processout.sdk.api.model.response.* import com.processout.sdk.api.repository.POCardsRepository import com.processout.sdk.core.POFailure.Code.Cancelled import com.processout.sdk.core.POFailure.Code.Generic @@ -644,57 +642,47 @@ internal class CardTokenizationInteractor( //region Tokenization - @Suppress("DEPRECATION") private fun tokenize(request: POCardTokenizationRequest) { POLogger.info(message = "Submitting card information.") dispatch(WillTokenize) interactorScope.launch { cardsRepository.tokenize(request) .onSuccess { card -> - _state.update { it.copy(tokenizedCard = card) } + _state.update { + it.copy( + tokenizedCard = card, + focusedFieldId = null + ) + } POLogger.info( message = "Card tokenized successfully.", attributes = mapOf(POLogAttribute.CARD_ID to card.id) ) dispatch(DidTokenize(card)) - val subscribedForProcessing = legacyEventDispatcher.subscribedForProcessTokenizedCardRequest() - val subscribedForProcessingDeprecated = legacyEventDispatcher.subscribedForProcessTokenizedCard() - val processingRequest = POCardTokenizationProcessingRequest( - card = card, - saveCard = _state.value.saveCardField.value.text.toBooleanStrictOrNull() ?: false - ) - if (subscribedForProcessing) { - requestToProcessTokenizedCard(request = processingRequest, useDeprecated = false) - } - if (subscribedForProcessingDeprecated) { - requestToProcessTokenizedCard(request = processingRequest, useDeprecated = true) - } - if (!subscribedForProcessing && !subscribedForProcessingDeprecated) { - complete(Success(card)) - } + requestToProcessTokenizedCard(card) }.onFailure { requestIfShouldContinue(failure = it) } } } - private fun requestToProcessTokenizedCard( - request: POCardTokenizationProcessingRequest, - useDeprecated: Boolean // TODO: remove when deprecated dispatcher method is removed. - ) { - interactorScope.launch { - _state.update { it.copy(focusedFieldId = null) } - if (useDeprecated) { - @Suppress("DEPRECATION") - legacyEventDispatcher.processTokenizedCard(request.card) - } else { - legacyEventDispatcher.processTokenizedCardRequest(request) - } - POLogger.info( - message = "Requested to process tokenized card.", - attributes = mapOf(POLogAttribute.CARD_ID to request.card.id) - ) + @Suppress("DEPRECATION") + private suspend fun requestToProcessTokenizedCard(card: POCard) { + val request = POCardTokenizationProcessingRequest( + card = card, + saveCard = _state.value.saveCardField.value.text.toBooleanStrictOrNull() ?: false + ) + if (legacyEventDispatcher.subscribedForProcessTokenizedCard()) { + legacyEventDispatcher.processTokenizedCard(card) + } else if (legacyEventDispatcher.subscribedForProcessTokenizedCardRequest()) { + legacyEventDispatcher.processTokenizedCardRequest(request) + } else { + eventDispatcher.send(request) } + POLogger.info( + message = "Requested to process tokenized card.", + attributes = mapOf(POLogAttribute.CARD_ID to card.id) + ) } private fun handleCompletion() { @@ -702,7 +690,6 @@ internal class CardTokenizationInteractor( legacyEventDispatcher.completion.collect { result -> result.onSuccess { _state.value.tokenizedCard?.let { card -> - dispatch(DidComplete) complete(Success(card)) }.orElse { val failure = ProcessOutResult.Failure( @@ -716,6 +703,16 @@ internal class CardTokenizationInteractor( } } } + eventDispatcher.subscribeForResponse( + coroutineScope = interactorScope + ) { response -> + response.result + .onSuccess { card -> + complete(Success(card)) + }.onFailure { + requestIfShouldContinue(failure = it) + } + } } private fun complete(success: Success) { @@ -723,6 +720,7 @@ internal class CardTokenizationInteractor( message = "Completed successfully.", attributes = mapOf(POLogAttribute.CARD_ID to success.card.id) ) + dispatch(DidComplete) _completion.update { success } } From 3cd03fe502f82d193bbaf3d7d6964652bda8a73a Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 16 Apr 2025 13:17:49 +0300 Subject: [PATCH 20/21] Deprecated --- .../processout/sdk/api/dispatcher/PODefaultEventDispatchers.kt | 2 ++ .../com/processout/sdk/api/dispatcher/POEventDispatchers.kt | 1 + .../card/tokenization/POCardTokenizationEventDispatcher.kt | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/PODefaultEventDispatchers.kt b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/PODefaultEventDispatchers.kt index e0a24e6b8..a0f2de2d4 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/PODefaultEventDispatchers.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/PODefaultEventDispatchers.kt @@ -1,3 +1,5 @@ +@file:Suppress("OVERRIDE_DEPRECATION") + package com.processout.sdk.api.dispatcher import com.processout.sdk.api.dispatcher.card.tokenization.POCardTokenizationEventDispatcher 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 96c688026..db54b7f57 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 @@ -12,6 +12,7 @@ interface POEventDispatchers { val nativeAlternativePaymentMethod: PONativeAlternativePaymentMethodEventDispatcher /** Dispatcher that allows to handle events during card tokenization. */ + @Deprecated(message = "Use API with POCardTokenizationDelegate instead.") val cardTokenization: POCardTokenizationEventDispatcher /** Dispatcher that allows to handle events during card updates. */ diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt index 2f8782d19..37b94e0f0 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/dispatcher/card/tokenization/POCardTokenizationEventDispatcher.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.SharedFlow /** * Dispatcher that allows to handle events during card tokenization. */ -@Deprecated(message = "Use POCardTokenizationDelegate instead.") +@Deprecated(message = "Use API with POCardTokenizationDelegate instead.") interface POCardTokenizationEventDispatcher { /** Allows to subscribe for card tokenization lifecycle events. */ From 539921ef6aece7a7fec8bb9bb961c94a69751e96 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Apr 2025 14:58:26 +0300 Subject: [PATCH 21/21] Update example app --- .../card/payment/CardPaymentFragment.kt | 77 +++++---------- .../screen/card/payment/CardPaymentUiState.kt | 25 ----- .../card/payment/CardPaymentViewModel.kt | 96 ++++++------------- .../card/payment/CardPaymentViewModelState.kt | 12 +++ .../DefaultCardTokenizationDelegate.kt | 38 +++++++- 5 files changed, 100 insertions(+), 148 deletions(-) delete mode 100644 example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentUiState.kt create mode 100644 example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModelState.kt diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt index 38f60bc92..36b12a9eb 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt @@ -17,9 +17,8 @@ import com.processout.example.shared.Constants import com.processout.example.shared.toMessage import com.processout.example.ui.screen.MainActivity import com.processout.example.ui.screen.base.BaseFragment -import com.processout.example.ui.screen.card.payment.CardPaymentUiState.* +import com.processout.example.ui.screen.card.payment.CardPaymentViewModelEvent.LaunchTokenization import com.processout.sdk.api.ProcessOut -import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest import com.processout.sdk.api.model.response.POCard import com.processout.sdk.api.service.PO3DSService import com.processout.sdk.checkout.threeds.POCheckout3DSService @@ -32,7 +31,9 @@ import com.processout.sdk.ui.card.tokenization.POCardTokenizationLauncher import com.processout.sdk.ui.shared.view.dialog.POAlertDialog import com.processout.sdk.ui.threeds.PO3DSRedirectCustomTabLauncher import com.processout.sdk.ui.threeds.POTest3DSService +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class CardPaymentFragment : BaseFragment( FragmentCardPaymentBinding::inflate @@ -42,8 +43,6 @@ class CardPaymentFragment : BaseFragment( CardPaymentViewModel.Factory() } - private val invoices = ProcessOut.instance.invoices - private val dispatcher = ProcessOut.instance.dispatchers.cardTokenization private lateinit var launcher: POCardTokenizationLauncher private lateinit var customTabLauncher: PO3DSRedirectCustomTabLauncher @@ -51,7 +50,11 @@ class CardPaymentFragment : BaseFragment( super.onCreate(savedInstanceState) launcher = POCardTokenizationLauncher.create( from = this, - delegate = DefaultCardTokenizationDelegate(), + delegate = DefaultCardTokenizationDelegate( + viewModel = viewModel, + invoices = ProcessOut.instance.invoices, + provide3DSService = ::create3DSService + ), callback = ::handle ) customTabLauncher = PO3DSRedirectCustomTabLauncher.create(from = this) @@ -60,44 +63,35 @@ class CardPaymentFragment : BaseFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setOnClickListeners() - viewLifecycleOwner.lifecycleScope.launch { + lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - viewModel.uiState.collect { handle(it) } - } - } - viewLifecycleOwner.lifecycleScope.launch { - dispatcher.processTokenizedCardRequest.collect { request -> - viewModel.onTokenized(request) - } - } - viewLifecycleOwner.lifecycleScope.launch { - invoices.authorizeInvoiceResult.collect { result -> - dispatcher.complete(result) + withContext(Dispatchers.Main.immediate) { + viewModel.events.collect { handle(it) } + } } } } private fun handle(result: ProcessOutActivityResult) { - val uiState = viewModel.uiState.value - val invoiceId = if (uiState is Authorizing) uiState.uiModel.invoiceId else null - viewModel.reset() + enableControls(isEnabled = true) result.onSuccess { card -> + val invoiceId = viewModel.state.value.invoiceId showAlert(getString(R.string.authorize_invoice_success_format, invoiceId, card.id)) - }.onFailure { showAlert(it.toMessage()) } + }.onFailure { + showAlert(it.toMessage()) + } } - private fun handle(uiState: CardPaymentUiState) { - handleControls(uiState) - when (uiState) { - is Submitted -> launchCardTokenization() - is Tokenized -> authorizeInvoice(uiState.uiModel) - is Failure -> showAlert(uiState.failure.toMessage()) - else -> {} + private fun handle(event: CardPaymentViewModelEvent) { + when (event) { + LaunchTokenization -> { + enableControls(isEnabled = false) + launchTokenization() + } } } - private fun launchCardTokenization() { - viewModel.onTokenizing() + private fun launchTokenization() { launcher.launch( POCardTokenizationConfiguration( cardScanner = CardScannerConfiguration(), @@ -106,19 +100,6 @@ class CardPaymentFragment : BaseFragment( ) } - private fun authorizeInvoice(uiModel: CardPaymentUiModel) { - invoices.authorizeInvoice( - request = POInvoiceAuthorizationRequest( - invoiceId = uiModel.invoiceId, - source = uiModel.cardId, - saveSource = uiModel.saveCard, - clientSecret = uiModel.clientSecret - ), - threeDSService = create3DSService() - ) - viewModel.onAuthorizing() - } - private fun create3DSService(): PO3DSService { val selected3DSService = with(binding.threedsServiceRadioGroup) { findViewById(checkedRadioButtonId).text.toString() @@ -170,18 +151,10 @@ class CardPaymentFragment : BaseFragment( private fun onSubmitClick() { with(binding) { - val details = InvoiceDetails( + viewModel.submit( amount = amountInput.text.toString(), currency = currencyInput.text.toString() ) - viewModel.submit(details) - } - } - - private fun handleControls(uiState: CardPaymentUiState) { - when (uiState) { - Initial, is Failure -> enableControls(true) - else -> enableControls(false) } } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentUiState.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentUiState.kt deleted file mode 100644 index 4e2f58520..000000000 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentUiState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.processout.example.ui.screen.card.payment - -import com.processout.sdk.core.ProcessOutResult - -sealed class CardPaymentUiState { - data object Initial : CardPaymentUiState() - data object Submitting : CardPaymentUiState() - data class Submitted(val uiModel: CardPaymentUiModel) : CardPaymentUiState() - data class Tokenizing(val uiModel: CardPaymentUiModel) : CardPaymentUiState() - data class Tokenized(val uiModel: CardPaymentUiModel) : CardPaymentUiState() - data class Authorizing(val uiModel: CardPaymentUiModel) : CardPaymentUiState() - data class Failure(val failure: ProcessOutResult.Failure) : CardPaymentUiState() -} - -data class CardPaymentUiModel( - val invoiceId: String, - val cardId: String = String(), - val saveCard: Boolean = false, - val clientSecret: String? = null -) - -data class InvoiceDetails( - val amount: String, - val currency: String -) diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt index 70ec6122f..84fe6286e 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt @@ -4,19 +4,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.processout.example.shared.Constants -import com.processout.example.ui.screen.card.payment.CardPaymentUiState.* +import com.processout.example.ui.screen.card.payment.CardPaymentViewModelEvent.LaunchTokenization import com.processout.sdk.api.ProcessOut -import com.processout.sdk.api.model.request.POCardTokenizationProcessingRequest import com.processout.sdk.api.model.request.POCreateCustomerRequest import com.processout.sdk.api.model.request.POCreateInvoiceRequest import com.processout.sdk.api.model.response.POCustomer +import com.processout.sdk.api.model.response.POInvoice import com.processout.sdk.api.service.POCustomerTokensService import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.getOrNull -import com.processout.sdk.core.onFailure -import com.processout.sdk.core.onSuccess +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.UUID @@ -36,40 +37,41 @@ class CardPaymentViewModel( } } - private val _uiState = MutableStateFlow(Initial) - val uiState = _uiState.asStateFlow() + private val _state = MutableStateFlow(CardPaymentViewModelState()) + val state = _state.asStateFlow() - private var customerId: String? = null + private val _events = Channel() + val events = _events.receiveAsFlow() - fun submit(details: InvoiceDetails) { - _uiState.value = Submitting + fun submit(amount: String, currency: String) { + _state.update { + it.copy( + amount = amount, + currency = currency + ) + } viewModelScope.launch { - if (customerId == null) { - customerId = createCustomer()?.id - createInvoice(details) - } else { - createInvoice(details) - } + _events.send(LaunchTokenization) } } - private suspend fun createInvoice(details: InvoiceDetails) = - invoices.createInvoice( + suspend fun createInvoice(): POInvoice? { + val state = _state.value + if (state.customerId == null) { + _state.update { it.copy(customerId = createCustomer()?.id) } + } + val invoice = invoices.createInvoice( POCreateInvoiceRequest( name = UUID.randomUUID().toString(), - amount = details.amount, - currency = details.currency, - customerId = customerId, + amount = state.amount, + currency = state.currency, + customerId = state.customerId, returnUrl = Constants.RETURN_URL ) - ).onSuccess { invoice -> - _uiState.value = Submitted( - CardPaymentUiModel( - invoiceId = invoice.id, - clientSecret = invoice.clientSecret - ) - ) - }.onFailure { _uiState.value = Failure(it) } + ).getOrNull() + _state.update { it.copy(invoiceId = invoice?.id) } + return invoice + } private suspend fun createCustomer(): POCustomer? = customerTokens.createCustomer( @@ -79,42 +81,4 @@ class CardPaymentViewModel( email = "test@email.com" ) ).getOrNull() - - fun onTokenizing() { - val uiState = _uiState.value - if (uiState is Submitted) { - _uiState.value = Tokenizing(uiState.uiModel) - } - } - - fun onTokenized(request: POCardTokenizationProcessingRequest) { - val uiState = _uiState.value - if (uiState is Tokenizing) { - _uiState.value = Tokenized( - uiState.uiModel.copy( - cardId = request.card.id, - saveCard = request.saveCard - ) - ) - } - if (uiState is Authorizing) { - _uiState.value = Tokenized( - uiState.uiModel.copy( - cardId = request.card.id, - saveCard = request.saveCard - ) - ) - } - } - - fun onAuthorizing() { - val uiState = _uiState.value - if (uiState is Tokenized) { - _uiState.value = Authorizing(uiState.uiModel) - } - } - - fun reset() { - _uiState.value = Initial - } } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModelState.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModelState.kt new file mode 100644 index 000000000..1e2d6f552 --- /dev/null +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModelState.kt @@ -0,0 +1,12 @@ +package com.processout.example.ui.screen.card.payment + +data class CardPaymentViewModelState( + val amount: String = String(), + val currency: String = String(), + val invoiceId: String? = null, + val customerId: String? = null +) + +sealed interface CardPaymentViewModelEvent { + data object LaunchTokenization : CardPaymentViewModelEvent +} diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt index 1df5974c5..1dc8fd6ff 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt @@ -1,12 +1,40 @@ +@file:Suppress("FoldInitializerAndIfToElvis") + package com.processout.example.ui.screen.card.payment -import com.processout.sdk.api.model.event.POCardTokenizationEvent -import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest +import com.processout.sdk.api.model.response.POCard +import com.processout.sdk.api.service.PO3DSService +import com.processout.sdk.api.service.POInvoicesService +import com.processout.sdk.core.POFailure.Code.Generic +import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.ui.card.tokenization.POCardTokenizationDelegate -class DefaultCardTokenizationDelegate : POCardTokenizationDelegate { +class DefaultCardTokenizationDelegate( + private val viewModel: CardPaymentViewModel, + private val invoices: POInvoicesService, + private val provide3DSService: () -> PO3DSService +) : POCardTokenizationDelegate { - override fun onEvent(event: POCardTokenizationEvent) { - POLogger.info("%s", event) + override suspend fun processTokenizedCard( + card: POCard, + saveCard: Boolean + ): ProcessOutResult { + val invoice = viewModel.createInvoice() + if (invoice == null) { + return ProcessOutResult.Failure( + code = Generic(), + message = "Failed to create an invoice." + ) + } + return invoices.authorize( + request = POInvoiceAuthorizationRequest( + invoiceId = invoice.id, + source = card.id, + saveSource = saveCard, + clientSecret = invoice.clientSecret + ), + threeDSService = provide3DSService() + ) } }