diff --git a/build.gradle b/build.gradle index 3e563df3..de0b7774 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension buildscript { ext { - androidGradlePluginVersion = '9.3.1' + androidGradlePluginVersion = '9.4.1' kotlinVersion = '2.1.20' kspVersion = '2.1.20-1.0.32' dokkaVersion = '1.9.20' diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCreateInvoiceRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCreateInvoiceRequest.kt index 41b460ef..6f9abeb2 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCreateInvoiceRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/POCreateInvoiceRequest.kt @@ -16,6 +16,26 @@ data class POCreateInvoiceRequest( @Json(name = "return_url") val returnUrl: String? = null, val shipping: POContact? = null, + @Json(name = "payment_configuration") + val paymentConfiguration: PaymentConfiguration? = null, val device: Map = mapOf("channel" to "android"), val metadata: Map? = null -) +) { + + @JsonClass(generateAdapter = true) + data class PaymentConfiguration( + val apm: AlternativePaymentConfiguration + ) + + @JsonClass(generateAdapter = true) + data class AlternativePaymentConfiguration( + @Json(name = "preferred_finalization_mode") + val preferredFinalizationMode: PreferredFinalizationMode + ) + + @Suppress("EnumEntryName") + @JsonClass(generateAdapter = false) + enum class PreferredFinalizationMode { + automatic, manual + } +} diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt index 119c2581..af5526b2 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAuthorizationResponse.kt @@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize * @param[paymentMethod] Payment method details. * @param[elements] An ordered list of elements that needs to be rendered on the UI during native alternative payment flow. * @param[redirect] Indicates required redirect. + * @param[availableActions] Available actions to advance the payment. */ @Parcelize data class PONativeAlternativePaymentAuthorizationResponse( @@ -21,7 +22,8 @@ data class PONativeAlternativePaymentAuthorizationResponse( val invoice: Invoice, val paymentMethod: PONativeAlternativePaymentMethodDetails, val elements: List?, - val redirect: PONativeAlternativePaymentRedirect? + val redirect: PONativeAlternativePaymentRedirect?, + val availableActions: List? ) : Parcelable { /** @@ -47,5 +49,7 @@ internal data class NativeAlternativePaymentAuthorizationResponseBody( @Json(name = "payment_method") val paymentMethod: PONativeAlternativePaymentMethodDetails, val elements: List?, - val redirect: PONativeAlternativePaymentRedirect? + val redirect: PONativeAlternativePaymentRedirect?, + @Json(name = "available_actions") + val availableActions: List? ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAvailableAction.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAvailableAction.kt new file mode 100644 index 00000000..54d0fa58 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentAvailableAction.kt @@ -0,0 +1,24 @@ +package com.processout.sdk.api.model.response.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.JsonClass + +/** + * Available action to advance the payment. + */ +@JsonClass(generateAdapter = false) +enum class PONativeAlternativePaymentAvailableAction { + + /** Authorization is possible. */ + AUTHORIZE, + + /** Capture is possible. */ + CAPTURE, + + /** + * Placeholder that allows adding additional cases while staying backward compatible. + * __Warning:__ Do not match this case directly, use _when-else_ instead. + */ + @ProcessOutInternalApi + UNKNOWN +} diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentState.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentState.kt index 245ad23a..c5366787 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentState.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentState.kt @@ -8,9 +8,19 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = false) enum class PONativeAlternativePaymentState { + /** Next step is required to proceed. */ NEXT_STEP_REQUIRED, + /** Customer interaction completed. Explicit authorization and/or capture is expected to advance the payment. */ + CUSTOMER_ACTIONS_COMPLETED, + + /** Authorization was requested and it’s still pending. No further actions are expected, payment will advance to next state automatically. */ + AUTHORIZATION_PENDING, + + /** Authorization was requested and completed. */ + AUTHORIZED, + /** Pending payment processing. */ PENDING, diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentTokenizationResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentTokenizationResponse.kt index 2ac5cf53..f2cb9d14 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentTokenizationResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentTokenizationResponse.kt @@ -12,13 +12,15 @@ import kotlinx.parcelize.Parcelize * @param[paymentMethod] Payment method details. * @param[elements] An ordered list of elements that needs to be rendered on the UI during native alternative payment flow. * @param[redirect] Indicates required redirect. + * @param[availableActions] Available actions to advance the payment. */ @Parcelize data class PONativeAlternativePaymentTokenizationResponse( val state: PONativeAlternativePaymentState, val paymentMethod: PONativeAlternativePaymentMethodDetails, val elements: List?, - val redirect: PONativeAlternativePaymentRedirect? + val redirect: PONativeAlternativePaymentRedirect?, + val availableActions: List? ) : Parcelable @JsonClass(generateAdapter = true) @@ -27,5 +29,7 @@ internal data class NativeAlternativePaymentTokenizationResponseBody( @Json(name = "payment_method") val paymentMethod: PONativeAlternativePaymentMethodDetails, val elements: List?, - val redirect: PONativeAlternativePaymentRedirect? + val redirect: PONativeAlternativePaymentRedirect?, + @Json(name = "available_actions") + val availableActions: List? ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt index 49a6e5e4..68ceb76f 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt @@ -13,6 +13,7 @@ import com.squareup.moshi.JsonClass * @param[customerToken] Customer token details if any. * @param[elements] An ordered list of elements that needs to be rendered on the UI during native alternative payment flow. * @param[redirect] Indicates required redirect. + * @param[availableActions] Available actions to advance the payment. */ @ProcessOutInternalApi data class PONativeAlternativePaymentUrlResolutionResponse( @@ -21,7 +22,8 @@ data class PONativeAlternativePaymentUrlResolutionResponse( val invoice: PONativeAlternativePaymentInvoice?, val customerToken: PONativeAlternativePaymentCustomerToken?, val elements: List?, - val redirect: PONativeAlternativePaymentRedirect? + val redirect: PONativeAlternativePaymentRedirect?, + val availableActions: List? ) @JsonClass(generateAdapter = true) @@ -33,5 +35,7 @@ internal data class NativeAlternativePaymentUrlResolutionResponseBody( @Json(name = "customer_token") val customerToken: PONativeAlternativePaymentCustomerToken?, val elements: List?, - val redirect: PONativeAlternativePaymentRedirect? + val redirect: PONativeAlternativePaymentRedirect?, + @Json(name = "available_actions") + val availableActions: List? ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt index 753fbe97..3a5f08fe 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultCustomerTokensRepository.kt @@ -118,6 +118,7 @@ internal class DefaultCustomerTokensRepository( PONativeAlternativePaymentElement.Unknown } }, - redirect = redirect + redirect = redirect, + availableActions = availableActions ) } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt index 30e86d1f..89fa24a1 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/DefaultInvoicesRepository.kt @@ -176,7 +176,8 @@ internal class DefaultInvoicesRepository( invoice = invoice, paymentMethod = paymentMethod, elements = elements?.map { it.toModel() }, - redirect = redirect + redirect = redirect, + availableActions = availableActions ) private fun NativeAlternativePaymentUrlResolutionResponseBody.toModel() = @@ -186,7 +187,8 @@ internal class DefaultInvoicesRepository( invoice = invoice, customerToken = customerToken, elements = elements?.map { it.toModel() }, - redirect = redirect + redirect = redirect, + availableActions = availableActions ) private fun NativeAlternativePaymentElement.toModel() = diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt b/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt index 0651d78f..acdbdb48 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt @@ -149,6 +149,7 @@ class POFailure private constructor() { invalidUrl("request.validation.invalid-url"), invalidUser("request.validation.invalid-user"), invalidRedirectResult("request.validation.redirect-result-invalid"), + unexpectedRedirectResult("request.validation.redirect-result-unexpected"), missingCurrency("request.validation.missing-currency"), missingCustomerInput("gateway.missing-customer-input"), missingDescription("request.validation.missing-description"), diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt index 82e879f5..69cdabbd 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentCapturePoller.kt @@ -5,11 +5,10 @@ package com.processout.sdk.ui.napm import android.os.SystemClock import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.SUCCESS -import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentTokenizationResponse +import com.processout.sdk.api.model.response.napm.v2.* +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse.Invoice +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.AUTHORIZATION_PENDING +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.PENDING import com.processout.sdk.api.service.POCustomerTokensService import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure.Code.* @@ -41,7 +40,11 @@ internal class NativeAlternativePaymentCapturePoller( data class CaptureResponse( val state: PONativeAlternativePaymentState, - val elements: List? + val invoice: Invoice?, + val paymentMethod: PONativeAlternativePaymentMethodDetails, + val elements: List?, + val redirect: PONativeAlternativePaymentRedirect?, + val availableActions: List? ) private var backoffIterator = retryStrategy.newBackoffIterator() @@ -106,7 +109,7 @@ internal class NativeAlternativePaymentCapturePoller( private fun isRetryable( result: ProcessOutResult ): Boolean = result.fold( - onSuccess = { it.state != SUCCESS }, + onSuccess = { it.state == PENDING || it.state == AUTHORIZATION_PENDING }, onFailure = { failure -> val retryableCodes = listOf( NetworkUnreachable, @@ -124,7 +127,11 @@ internal class NativeAlternativePaymentCapturePoller( ProcessOutResult.Success( CaptureResponse( state = it.state, - elements = it.elements + invoice = it.invoice, + paymentMethod = it.paymentMethod, + elements = it.elements, + redirect = it.redirect, + availableActions = it.availableActions ) ) }, @@ -138,7 +145,11 @@ internal class NativeAlternativePaymentCapturePoller( ProcessOutResult.Success( CaptureResponse( state = it.state, - elements = it.elements + invoice = null, + paymentMethod = it.paymentMethod, + elements = it.elements, + redirect = it.redirect, + availableActions = it.availableActions ) ) }, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index c73f744d..7ee9dc9f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -31,6 +31,7 @@ import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSu import com.processout.sdk.api.model.response.POImageResource import com.processout.sdk.api.model.response.napm.v2.* import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse.Invoice +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAvailableAction.CAPTURE import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter.Otp.Subtype import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.DeepLinkConfiguration @@ -42,6 +43,8 @@ import com.processout.sdk.core.POFailure.Code.* import com.processout.sdk.core.POFailure.GenericCode.mobileHeadlessModeNotSupported import com.processout.sdk.core.POFailure.InvalidField import com.processout.sdk.core.POFailure.ValidationCode +import com.processout.sdk.core.POFailure.ValidationCode.invalidRedirectResult +import com.processout.sdk.core.POFailure.ValidationCode.unexpectedRedirectResult import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.fold import com.processout.sdk.core.logger.POLogger @@ -59,11 +62,8 @@ import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.WebRedirect import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.CancelButton import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Authorization import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Tokenization -import com.processout.sdk.ui.napm.delegate.v2.NativeAlternativePaymentDefaultValuesRequest -import com.processout.sdk.ui.napm.delegate.v2.NativeAlternativePaymentDefaultValuesResponse -import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent +import com.processout.sdk.ui.napm.delegate.v2.* import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent.* -import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentParameterValue import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentParameterValue.Value import com.processout.sdk.ui.shared.extension.dpToPx import com.processout.sdk.ui.shared.extension.openDeepLink @@ -104,13 +104,17 @@ internal class NativeAlternativePaymentInteractor( private val _sideEffects = Channel() val sideEffects = _sideEffects.receiveAsFlow() - private val handler = Handler(Looper.getMainLooper()) - private var paymentState: PONativeAlternativePaymentState = UNKNOWN + + private var didRequestManualFinalization = false + private var latestManualFinalizationRequest: NativeAlternativePaymentManualFinalizationRequest? = null private var latestDefaultValuesRequest: NativeAlternativePaymentDefaultValuesRequest? = null private var latestWillSubmitParametersEvent: WillSubmitParameters? = null + private var isCapturePolling = false + private val handler = Handler(Looper.getMainLooper()) + init { ProcessLifecycleOwner.get().lifecycle.addObserver(this) } @@ -124,8 +128,9 @@ internal class NativeAlternativePaymentInteractor( dispatch(WillStart) dispatchFailure() collectDefaultValues() + collectManualFinalizationResult() collectDeepLink() - fetchPaymentDetails() + fetchPaymentState() } fun start(configuration: PONativeAlternativePaymentConfiguration) { @@ -145,22 +150,24 @@ internal class NativeAlternativePaymentInteractor( interactorScope.coroutineContext.cancelChildren() handler.removeCallbacksAndMessages(null) paymentState = UNKNOWN + didRequestManualFinalization = false + latestManualFinalizationRequest = null latestDefaultValuesRequest = null latestWillSubmitParametersEvent = null _completion.update { Awaiting } _state.update { Idle } } - private fun fetchPaymentDetails() { + private fun fetchPaymentState() { interactorScope.launch { when (val flow = configuration.flow) { - is Authorization -> fetchAuthorizationDetails(flow) - is Tokenization -> fetchTokenizationDetails(flow) + is Authorization -> fetchAuthorizationState(flow) + is Tokenization -> fetchTokenizationState(flow) } } } - private suspend fun fetchAuthorizationDetails(flow: Authorization) { + private suspend fun fetchAuthorizationState(flow: Authorization) { val request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, gatewayConfigurationId = flow.gatewayConfigurationId, @@ -176,15 +183,16 @@ internal class NativeAlternativePaymentInteractor( ), paymentState = response.state, elements = response.elements, - redirect = response.redirect + redirect = response.redirect, + availableActions = response.availableActions ) }.onFailure { failure -> - POLogger.info("Failed to fetch authorization details: %s", failure) + POLogger.info("Failed to fetch authorization state: %s", failure) _completion.update { Failure(failure) } } } - private suspend fun fetchTokenizationDetails(flow: Tokenization) { + private suspend fun fetchTokenizationState(flow: Tokenization) { val request = PONativeAlternativePaymentTokenizationRequest( customerId = flow.customerId, customerTokenId = flow.customerTokenId, @@ -200,17 +208,18 @@ internal class NativeAlternativePaymentInteractor( ), paymentState = response.state, elements = response.elements, - redirect = response.redirect + redirect = response.redirect, + availableActions = response.availableActions ) }.onFailure { failure -> - POLogger.info("Failed to fetch tokenization details: %s", failure) + POLogger.info("Failed to fetch tokenization state: %s", failure) _completion.update { Failure(failure) } } } private suspend fun initNextStepStateValue( paymentMethod: PONativeAlternativePaymentMethodDetails, - invoice: Invoice?, + invoice: Invoice? ): NextStepStateValue { preloadImages(resources = listOf(paymentMethod.logo)) return NextStepStateValue( @@ -235,15 +244,19 @@ internal class NativeAlternativePaymentInteractor( stateValue: NextStepStateValue, paymentState: PONativeAlternativePaymentState, elements: List?, - redirect: PONativeAlternativePaymentRedirect? + redirect: PONativeAlternativePaymentRedirect?, + availableActions: List? ) { this.paymentState = paymentState val mappedElements = elements?.map() ?: emptyList() preloadImages(resources = mappedElements.images()) when (paymentState) { NEXT_STEP_REQUIRED -> handleNextStep(stateValue, mappedElements, redirect) - PENDING -> handlePending(stateValue, mappedElements) - SUCCESS -> handleSuccess( + PENDING, AUTHORIZATION_PENDING -> handlePending(stateValue, mappedElements) + CUSTOMER_ACTIONS_COMPLETED -> requestManualFinalization(paymentState, availableActions) + AUTHORIZED if !didRequestManualFinalization && availableActions?.contains(CAPTURE) == true -> + requestManualFinalization(paymentState, availableActions) + AUTHORIZED, SUCCESS -> handleSuccess( stateValue.toPendingStateValue( uuid = UUID.randomUUID().toString(), elements = mappedElements @@ -574,6 +587,70 @@ internal class NativeAlternativePaymentInteractor( //endregion + //region Manual Finalization + + private fun requestManualFinalization( + paymentState: PONativeAlternativePaymentState, + availableActions: List? + ) { + if (didRequestManualFinalization) { + val failure = ProcessOutResult.Failure( + code = Generic(), + message = "Payment was not advanced." + ) + POLogger.info( + message = "Unexpected state during manual finalization: %s", failure, + attributes = configuration.logAttributes + ) + _completion.update { Failure(failure) } + return + } + if (availableActions.isNullOrEmpty()) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "No available actions to finalize the payment." + ) + POLogger.error( + message = "Unexpected response during manual finalization: %s", failure, + attributes = configuration.logAttributes + ) + _completion.update { Failure(failure) } + return + } + interactorScope.launch { + val request = NativeAlternativePaymentManualFinalizationRequest( + paymentState = paymentState, + availableActions = availableActions + ) + didRequestManualFinalization = true + latestManualFinalizationRequest = request + eventDispatcher.send(request) + POLogger.info( + "Requested to finalize the payment manually. Payment state: %s. Available actions: %s.", + paymentState, availableActions + ) + } + } + + private fun collectManualFinalizationResult() { + eventDispatcher.subscribeForResponse( + coroutineScope = interactorScope + ) { response -> + if (response.uuid == latestManualFinalizationRequest?.uuid) { + latestManualFinalizationRequest = null + val result = response.result + POLogger.info("Collected manual finalization result: %s", result) + result.onSuccess { + fetchPaymentState() + }.onFailure { failure -> + _completion.update { Failure(failure) } + } + } + } + } + + //endregion + //region Deep Link private fun collectDeepLink() { @@ -602,7 +679,9 @@ internal class NativeAlternativePaymentInteractor( ) invoicesService.resolveUrl(request) .onSuccess { response -> - if (response.state == PENDING && _state.value is Pending) { + val pendingStates = setOf(PENDING, AUTHORIZATION_PENDING) + val isPending = _state.value is Pending && pendingStates.contains(response.state) + if (didRequestManualFinalization || isPending) { return@onSuccess } handlePaymentState( @@ -612,14 +691,16 @@ internal class NativeAlternativePaymentInteractor( ), paymentState = response.state, elements = response.elements, - redirect = response.redirect + redirect = response.redirect, + availableActions = response.availableActions ) }.onFailure { failure -> - val isInvalidRedirectResult = when (val code = failure.code) { - is Validation -> code.validationCode == ValidationCode.invalidRedirectResult + val ignoredFailureCodes = setOf(invalidRedirectResult, unexpectedRedirectResult) + val shouldIgnoreRedirectResult = when (val code = failure.code) { + is Validation -> ignoredFailureCodes.contains(code.validationCode) else -> false } - if (isInvalidRedirectResult) { + if (didRequestManualFinalization || shouldIgnoreRedirectResult) { return@onFailure } _completion.update { Failure(failure) } @@ -949,7 +1030,8 @@ internal class NativeAlternativePaymentInteractor( stateValue = stateValue, paymentState = response.state, elements = response.elements, - redirect = response.redirect + redirect = response.redirect, + availableActions = response.availableActions ) }.onFailure { failure -> handlePaymentFailure(failure) @@ -978,7 +1060,8 @@ internal class NativeAlternativePaymentInteractor( stateValue = stateValue, paymentState = response.state, elements = response.elements, - redirect = response.redirect + redirect = response.redirect, + availableActions = response.availableActions ) }.onFailure { failure -> handlePaymentFailure(failure) @@ -1110,15 +1193,16 @@ internal class NativeAlternativePaymentInteractor( try { capturePoller.poll() .onSuccess { response -> - val elements = response.elements?.map() - _state.whenPending { stateValue -> - handleSuccess( - stateValue.copy( - uuid = UUID.randomUUID().toString(), - elements = elements - ) - ) - } + handlePaymentState( + stateValue = initNextStepStateValue( + paymentMethod = response.paymentMethod, + invoice = response.invoice + ), + paymentState = response.state, + elements = response.elements, + redirect = response.redirect, + availableActions = response.availableActions + ) }.onFailure { failure -> _completion.update { Failure(failure) } } @@ -1272,9 +1356,7 @@ internal class NativeAlternativePaymentInteractor( // TODO(v2): use barcode ID, also inspect instructions group private fun saveBarcode() { _state.whenNextStep { stateValue -> - val instructions = stateValue.elements.mapNotNull { - it as? Element.Instruction - } + val instructions = stateValue.elements.filterIsInstance() instructions.forEach { if (it.instruction is Instruction.Barcode) { saveBarcode(barcode = it.instruction) @@ -1283,9 +1365,7 @@ internal class NativeAlternativePaymentInteractor( } } _state.whenPending { stateValue -> - val instructions = stateValue.elements?.mapNotNull { - it as? Element.Instruction - } + val instructions = stateValue.elements?.filterIsInstance() instructions?.forEach { if (it.instruction is Instruction.Barcode) { saveBarcode(barcode = it.instruction) @@ -1315,9 +1395,7 @@ internal class NativeAlternativePaymentInteractor( Manifest.permission.WRITE_EXTERNAL_STORAGE -> if (result.isGranted) { _state.whenNextStep { stateValue -> - val instructions = stateValue.elements.mapNotNull { - it as? Element.Instruction - } + val instructions = stateValue.elements.filterIsInstance() instructions.forEach { if (it.instruction is Instruction.Barcode) { interactorScope.launch { @@ -1330,9 +1408,7 @@ internal class NativeAlternativePaymentInteractor( } } _state.whenPending { stateValue -> - val instructions = stateValue.elements?.mapNotNull { - it as? Element.Instruction - } + val instructions = stateValue.elements?.filterIsInstance() instructions?.forEach { if (it.instruction is Instruction.Barcode) { interactorScope.launch { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt index d606b5f3..18ad50ac 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt @@ -25,10 +25,7 @@ import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Success import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.WebRedirectResult import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.WebRedirect import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Authorization -import com.processout.sdk.ui.napm.delegate.v2.NativeAlternativePaymentDefaultValuesRequest -import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentDelegate -import com.processout.sdk.ui.napm.delegate.v2.PONativeAlternativePaymentEvent -import com.processout.sdk.ui.napm.delegate.v2.toResponse +import com.processout.sdk.ui.napm.delegate.v2.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -321,6 +318,7 @@ class PONativeAlternativePaymentLauncher private constructor( private fun dispatchAllEvents(coroutineScope: CoroutineScope) { dispatchEvents(coroutineScope) dispatchDefaultValues(coroutineScope) + dispatchManualFinalization(coroutineScope) } private fun dispatchEvents(coroutineScope: CoroutineScope) { @@ -343,6 +341,20 @@ class PONativeAlternativePaymentLauncher private constructor( } } + private fun dispatchManualFinalization(coroutineScope: CoroutineScope) { + eventDispatcher.subscribeForRequest( + coroutineScope + ) { request -> + coroutineScope.launch { + val result = delegate.finalize( + paymentState = request.paymentState, + availableActions = request.availableActions + ) + eventDispatcher.send(request.toResponse(result)) + } + } + } + /** * Launches the payment. */ diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/NativeAlternativePaymentManualFinalization.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/NativeAlternativePaymentManualFinalization.kt new file mode 100644 index 00000000..6a57d3ce --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/NativeAlternativePaymentManualFinalization.kt @@ -0,0 +1,22 @@ +package com.processout.sdk.ui.napm.delegate.v2 + +import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAvailableAction +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState +import com.processout.sdk.core.ProcessOutResult +import java.util.UUID + +internal data class NativeAlternativePaymentManualFinalizationRequest( + override val uuid: UUID = UUID.randomUUID(), + val paymentState: PONativeAlternativePaymentState, + val availableActions: List +) : POEventDispatcher.Request + +internal data class NativeAlternativePaymentManualFinalizationResponse( + override val uuid: UUID, + val result: ProcessOutResult +) : POEventDispatcher.Response + +internal fun NativeAlternativePaymentManualFinalizationRequest.toResponse( + result: ProcessOutResult +) = NativeAlternativePaymentManualFinalizationResponse(uuid, result) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentDelegate.kt index 1309ebe3..ab985c88 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentDelegate.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/delegate/v2/PONativeAlternativePaymentDelegate.kt @@ -1,6 +1,10 @@ package com.processout.sdk.ui.napm.delegate.v2 +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAvailableAction import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState +import com.processout.sdk.core.POFailure +import com.processout.sdk.core.ProcessOutResult /** * Delegate that allows to handle events during native alternative payments. @@ -21,4 +25,18 @@ interface PONativeAlternativePaymentDelegate { gatewayConfigurationId: String, parameters: List ): Map = emptyMap() + + /** + * Asks the delegate to finalize the payment by explicitly advancing it to an authorized and/or captured state using one of the available actions. + * Method is invoked at most once, either when all customer actions are completed or when the payment is already authorized and can still be captured. + * In the latter case, the implementation may return without advancing the payment, leaving it in the authorized state. + */ + suspend fun finalize( + paymentState: PONativeAlternativePaymentState, + availableActions: List + ): ProcessOutResult = + ProcessOutResult.Failure( + code = POFailure.Code.Generic(), + message = "Manual finalization is not implemented by the delegate." + ) }