diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt index 8d4f2df07..43500d7d3 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/NativeAlternativePaymentRequestBody.kt @@ -9,7 +9,9 @@ internal data class NativeAlternativePaymentRequestBody( val gatewayConfigurationId: String, val source: String?, @Json(name = "submit_data") - val submitData: SubmitData? + val submitData: SubmitData?, + @Json(name = "redirect") + val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? ) { @JsonClass(generateAdapter = true) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt index b3eeeb85e..cbc813fdd 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentAuthorizationRequest.kt @@ -7,10 +7,12 @@ package com.processout.sdk.api.model.request.napm.v2 * @param[gatewayConfigurationId] Gateway configuration identifier. * @param[source] Payment source. * @param[submitData] Payment payload. + * @param[redirectConfirmation] Redirect confirmation. */ data class PONativeAlternativePaymentAuthorizationRequest( val invoiceId: String, val gatewayConfigurationId: String, val source: String? = null, - val submitData: PONativeAlternativePaymentSubmitData? = null + val submitData: PONativeAlternativePaymentSubmitData? = null, + val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRedirectConfirmation.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRedirectConfirmation.kt new file mode 100644 index 000000000..32084346a --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRedirectConfirmation.kt @@ -0,0 +1,13 @@ +package com.processout.sdk.api.model.request.napm.v2 + +import com.squareup.moshi.JsonClass + +/** + * Specifies native alternative payment redirect confirmation. + * + * @param[success] Indicates whether the redirection was successful. + */ +@JsonClass(generateAdapter = true) +data class PONativeAlternativePaymentRedirectConfirmation( + val success: Boolean +) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt index 5fa42bd5a..7266e7170 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentTokenizationRequest.kt @@ -7,10 +7,12 @@ package com.processout.sdk.api.model.request.napm.v2 * @param[customerTokenId] Customer token identifier. * @param[gatewayConfigurationId] Gateway configuration identifier. * @param[submitData] Payment payload. + * @param[redirectConfirmation] Redirect confirmation. */ data class PONativeAlternativePaymentTokenizationRequest( val customerId: String, val customerTokenId: String, val gatewayConfigurationId: String, - val submitData: PONativeAlternativePaymentSubmitData? = null + val submitData: PONativeAlternativePaymentSubmitData? = null, + val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt index 38787b5d5..2ed975752 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt @@ -1,6 +1,12 @@ package com.processout.sdk.api.model.response.napm.v2 import android.os.Parcelable +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRedirectConfirmation +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.processout.sdk.core.util.findBy +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import kotlinx.parcelize.Parcelize @@ -9,10 +15,43 @@ import kotlinx.parcelize.Parcelize * * @param[url] Redirect URL. * @param[hint] A hint or description associated with the redirect URL. + * @param[rawType] Raw redirect type. + * @param[confirmationRequired] Indicates whether it is required to notify the backend if the redirection was successful + * by sending [PONativeAlternativePaymentRedirectConfirmation] + * in the [PONativeAlternativePaymentAuthorizationRequest.redirectConfirmation] + * or the [PONativeAlternativePaymentTokenizationRequest.redirectConfirmation], + * depending on the flow. */ @Parcelize @JsonClass(generateAdapter = true) data class PONativeAlternativePaymentRedirect( val url: String, - val hint: String -) : Parcelable + val hint: String, + @Json(name = "type") + val rawType: String, + @Json(name = "confirmation_required") + val confirmationRequired: Boolean +) : Parcelable { + + /** Redirect type. */ + val type: RedirectType + get() = RedirectType::rawType.findBy(rawType) ?: RedirectType.UNKNOWN + + /** + * Redirect type. + */ + enum class RedirectType(val rawType: String) { + /** Web redirect. */ + WEB("web"), + + /** Deep link redirect. */ + DEEP_LINK("deep_link"), + + /** + * Placeholder that allows adding additional cases while staying backward compatible. + * __Warning:__ Do not match this case directly, use _when-else_ instead. + */ + @ProcessOutInternalApi + UNKNOWN(String()) + } +} 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 f27a4278d..6e09402a0 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 @@ -81,7 +81,8 @@ internal class DefaultCustomerTokensRepository( NativeAlternativePaymentRequestBody( gatewayConfigurationId = gatewayConfigurationId, source = null, - submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) } + submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) }, + redirectConfirmation = redirectConfirmation ) private fun Map.map() = 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 3c57e7081..ad8864ee8 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 @@ -153,7 +153,8 @@ internal class DefaultInvoicesRepository( NativeAlternativePaymentRequestBody( gatewayConfigurationId = gatewayConfigurationId, source = source, - submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) } + submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) }, + redirectConfirmation = redirectConfirmation ) private fun Map.map() = 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 24e8a37f9..b1e7976df 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 @@ -852,7 +852,7 @@ internal class DynamicCheckoutInteractor( } when (paymentMethod) { is NativeAlternativePayment -> nativeAlternativePayment.onEvent( - NativeAlternativePaymentEvent.RedirectResult(result) + NativeAlternativePaymentEvent.WebRedirectResult(result) ) else -> result.onSuccess { response -> authorizeInvoice( @@ -1143,7 +1143,7 @@ internal class DynamicCheckoutInteractor( _sideEffects.send(permissionRequest) POLogger.info("System permission requested: %s", permissionRequest) } - is NativeAlternativePaymentSideEffect.Redirect -> + is NativeAlternativePaymentSideEffect.WebRedirect -> activePaymentMethod()?.let { paymentMethod -> _state.update { it.copy(processingPaymentMethod = paymentMethod) } _sideEffects.send( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt index 01b271aa3..3da34761c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt @@ -736,7 +736,7 @@ private fun NativeAlternativePaymentEvent.map( is NativeAlternativePaymentEvent.ActionConfirmationRequested -> ActionConfirmationRequested(id = id) is NativeAlternativePaymentEvent.Dismiss -> Dismiss(failure = failure) is NativeAlternativePaymentEvent.PermissionRequestResult, - is NativeAlternativePaymentEvent.RedirectResult -> null // Ignore, handled by dynamic checkout events. + is NativeAlternativePaymentEvent.WebRedirectResult -> null // Ignore, handled by dynamic checkout events. } internal object DynamicCheckoutScreen { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentBottomSheet.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentBottomSheet.kt index ba3c0e9e5..5c7a65de0 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentBottomSheet.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentBottomSheet.kt @@ -30,7 +30,7 @@ import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Failure import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Success import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.* import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.PermissionRequest -import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.Redirect +import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.WebRedirect import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.Loaded import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.Stage import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow @@ -83,7 +83,7 @@ internal class NativeAlternativePaymentBottomSheet : BaseBottomSheetDialogFragme alternativePaymentLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = this, callback = { result -> - viewModel.onEvent(RedirectResult(result)) + viewModel.onEvent(WebRedirectResult(result)) } ) viewModel.start() @@ -137,7 +137,7 @@ internal class NativeAlternativePaymentBottomSheet : BaseBottomSheetDialogFragme private fun handle(sideEffect: NativeAlternativePaymentSideEffect) { when (sideEffect) { is PermissionRequest -> requestPermission(sideEffect.permission) - is Redirect -> alternativePaymentLauncher.launch( + is WebRedirect -> alternativePaymentLauncher.launch( uri = sideEffect.redirectUrl.toUri(), returnUrl = sideEffect.returnUrl ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentEvent.kt index f29139e3e..b1c23094f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentEvent.kt @@ -12,14 +12,14 @@ internal sealed interface NativeAlternativePaymentEvent { data class ActionConfirmationRequested(val id: String) : NativeAlternativePaymentEvent data class Dismiss(val failure: ProcessOutResult.Failure) : NativeAlternativePaymentEvent data class PermissionRequestResult(val permission: String, val isGranted: Boolean) : NativeAlternativePaymentEvent - data class RedirectResult( + data class WebRedirectResult( val result: ProcessOutResult ) : NativeAlternativePaymentEvent } internal sealed interface NativeAlternativePaymentSideEffect { data class PermissionRequest(val permission: String) : NativeAlternativePaymentSideEffect - data class Redirect( + data class WebRedirect( val redirectUrl: String, val returnUrl: String ) : NativeAlternativePaymentSideEffect 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 ad3271fc4..501025b29 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 @@ -20,6 +20,7 @@ import coil.request.ImageResult import com.processout.sdk.R import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRedirectConfirmation import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Companion.phoneNumber import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Companion.string @@ -30,6 +31,7 @@ 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.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.RedirectType import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.* import com.processout.sdk.api.service.POCustomerTokensService import com.processout.sdk.api.service.POInvoicesService @@ -50,7 +52,7 @@ import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.* import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.Action import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.PermissionRequest -import com.processout.sdk.ui.napm.NativeAlternativePaymentSideEffect.Redirect +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 @@ -61,6 +63,7 @@ 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 import com.processout.sdk.ui.shared.provider.BarcodeBitmapProvider import com.processout.sdk.ui.shared.provider.MediaStorageProvider import com.processout.sdk.ui.shared.state.FieldValue @@ -285,6 +288,9 @@ internal class NativeAlternativePaymentInteractor( if (failWithUnknownParameter(parameters)) { return } + if (failWithUnknownRedirect(redirect)) { + return + } val fields = parameters.toFields() val updatedStateValue = stateValue.copy( uuid = UUID.randomUUID().toString(), @@ -378,6 +384,24 @@ internal class NativeAlternativePaymentInteractor( return false } + private fun failWithUnknownRedirect( + redirect: PONativeAlternativePaymentRedirect? + ): Boolean { + if (redirect?.type == RedirectType.UNKNOWN) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Unknown redirect type: ${redirect.rawType}" + ) + POLogger.error( + message = "Unexpected response: %s", failure, + attributes = configuration.logAttributes + ) + _completion.update { Failure(failure) } + return true + } + return false + } + private fun List.toFields() = map { parameter -> val defaultValue = when (parameter) { @@ -525,7 +549,7 @@ internal class NativeAlternativePaymentInteractor( } } is PermissionRequestResult -> handlePermission(event) - is RedirectResult -> handleRedirect(event.result) + is WebRedirectResult -> handleWebRedirect(event.result) is Dismiss -> { POLogger.info("Dismissed: %s", event.failure) dispatch(DidFail(event.failure, paymentState)) @@ -592,7 +616,7 @@ internal class NativeAlternativePaymentInteractor( if (stateValue.redirect != null) { redirect( stateValue = stateValue, - redirectUrl = stateValue.redirect.url + redirect = stateValue.redirect ) return@whenNextStep } @@ -616,14 +640,37 @@ internal class NativeAlternativePaymentInteractor( ) ) } - when (val flow = configuration.flow) { - is Authorization -> authorize(flow) - is Tokenization -> tokenize(flow) - } + continuePayment() + } + } + + private fun continuePayment( + redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null + ) { + when (val flow = configuration.flow) { + is Authorization -> authorize(flow, redirectConfirmation) + is Tokenization -> tokenize(flow, redirectConfirmation) } } private fun redirect( + stateValue: NextStepStateValue, + redirect: PONativeAlternativePaymentRedirect + ) { + when (redirect.type) { + RedirectType.WEB -> webRedirect( + stateValue = stateValue, + redirectUrl = redirect.url + ) + RedirectType.DEEP_LINK -> deepLinkRedirect( + stateValue = stateValue, + redirectUrl = redirect.url + ) + RedirectType.UNKNOWN -> failWithUnknownRedirect(redirect) + } + } + + private fun webRedirect( stateValue: NextStepStateValue, redirectUrl: String ) { @@ -631,10 +678,10 @@ internal class NativeAlternativePaymentInteractor( if (returnUrl.isNullOrBlank()) { val failure = ProcessOutResult.Failure( code = Generic(), - message = "Return URL is missing in configuration during redirect flow." + message = "Return URL is missing in configuration during web redirect flow." ) POLogger.warn( - message = "Failed redirect: %s", failure, + message = "Failed web redirect: %s", failure, attributes = configuration.logAttributes ) _completion.update { Failure(failure) } @@ -650,7 +697,7 @@ internal class NativeAlternativePaymentInteractor( } interactorScope.launch { _sideEffects.send( - Redirect( + WebRedirect( redirectUrl = redirectUrl, returnUrl = returnUrl ) @@ -658,17 +705,36 @@ internal class NativeAlternativePaymentInteractor( } } - private fun handleRedirect(result: ProcessOutResult) { + private fun handleWebRedirect(result: ProcessOutResult) { result.onSuccess { - when (val flow = configuration.flow) { - is Authorization -> authorize(flow) - is Tokenization -> tokenize(flow) + _state.whenNextStep { stateValue -> + val redirectConfirmation = if (stateValue.redirect?.confirmationRequired == true) + PONativeAlternativePaymentRedirectConfirmation(success = true) else null + continuePayment(redirectConfirmation) } }.onFailure { failure -> _completion.update { Failure(failure) } } } + private fun deepLinkRedirect( + stateValue: NextStepStateValue, + redirectUrl: String + ) { + _state.update { + NextStep( + stateValue.copy( + submitAllowed = true, + submitting = true + ) + ) + } + val didOpenUrl = app.openDeepLink(url = redirectUrl) + val redirectConfirmation = if (stateValue.redirect?.confirmationRequired == true) + PONativeAlternativePaymentRedirectConfirmation(success = didOpenUrl) else null + continuePayment(redirectConfirmation) + } + private fun Field.validate(): InvalidField? { when (parameter) { is Parameter.PhoneNumber -> if (value is FieldValue.PhoneNumber) { @@ -753,15 +819,17 @@ internal class NativeAlternativePaymentInteractor( private fun NextStepStateValue.areAllFieldsValid() = fields.all { it.isValid } - private fun authorize(flow: Authorization) { + private fun authorize( + flow: Authorization, + redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null + ) { _state.whenNextStep { stateValue -> interactorScope.launch { val request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, gatewayConfigurationId = flow.gatewayConfigurationId, - submitData = PONativeAlternativePaymentSubmitData( - parameters = stateValue.fields.values() - ) + submitData = stateValue.fields.toSubmitData(), + redirectConfirmation = redirectConfirmation ) invoicesService.authorize(request) .onSuccess { response -> @@ -778,16 +846,18 @@ internal class NativeAlternativePaymentInteractor( } } - private fun tokenize(flow: Tokenization) { + private fun tokenize( + flow: Tokenization, + redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null + ) { _state.whenNextStep { stateValue -> interactorScope.launch { val request = PONativeAlternativePaymentTokenizationRequest( customerId = flow.customerId, customerTokenId = flow.customerTokenId, gatewayConfigurationId = flow.gatewayConfigurationId, - submitData = PONativeAlternativePaymentSubmitData( - parameters = stateValue.fields.values() - ) + submitData = stateValue.fields.toSubmitData(), + redirectConfirmation = redirectConfirmation ) customerTokensService.tokenize(request) .onSuccess { response -> @@ -804,9 +874,9 @@ internal class NativeAlternativePaymentInteractor( } } - private fun List.values() = + private fun List.toSubmitData(): PONativeAlternativePaymentSubmitData? = associate { field -> - field.id to when (val value = field.value) { + val parameter = when (val value = field.value) { is FieldValue.Text -> string(value = value.value.text) is FieldValue.PhoneNumber -> { val dialingCode = when (field.parameter) { @@ -821,6 +891,11 @@ internal class NativeAlternativePaymentInteractor( ) } } + field.id to parameter + }.let { parameters -> + if (parameters.isNotEmpty()) + PONativeAlternativePaymentSubmitData(parameters) + else null } //endregion 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 172815de6..e8e4d57f6 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 @@ -1,5 +1,6 @@ package com.processout.sdk.ui.napm +import android.app.Application import android.content.Context import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher @@ -11,9 +12,11 @@ import com.processout.sdk.R import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.POEventDispatcher import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRedirectConfirmation import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest import com.processout.sdk.api.model.response.POAlternativePaymentMethodResponse import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.RedirectType import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.* import com.processout.sdk.api.service.POCustomerTokensService @@ -30,6 +33,7 @@ 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.PONativeAlternativePaymentEvent.DidFail import com.processout.sdk.ui.napm.delegate.v2.toResponse +import com.processout.sdk.ui.shared.extension.openDeepLink import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -37,6 +41,7 @@ import kotlinx.coroutines.launch * Launcher that starts [NativeAlternativePaymentActivity] and provides the result. */ class PONativeAlternativePaymentLauncher private constructor( + private val app: Application, private val scope: CoroutineScope, private val launcher: ActivityResultLauncher, private val activityOptions: ActivityOptionsCompat, @@ -49,8 +54,8 @@ class PONativeAlternativePaymentLauncher private constructor( private lateinit var customTabLauncher: POAlternativePaymentMethodCustomTabLauncher - private object ConfigurationCache { - var value: PONativeAlternativePaymentConfiguration? = null + private object LocalCache { + var configuration: PONativeAlternativePaymentConfiguration? = null } companion object { @@ -63,6 +68,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + app = from.requireActivity().application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), @@ -74,7 +80,7 @@ class PONativeAlternativePaymentLauncher private constructor( ).apply { customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = from, - callback = ::handleRedirect + callback = ::handleWebRedirect ) } @@ -90,6 +96,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + app = from.requireActivity().application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), @@ -101,7 +108,7 @@ class PONativeAlternativePaymentLauncher private constructor( ).apply { customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = from, - callback = ::handleRedirect + callback = ::handleWebRedirect ) } @@ -114,6 +121,7 @@ class PONativeAlternativePaymentLauncher private constructor( from: Fragment, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + app = from.requireActivity().application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), @@ -125,7 +133,7 @@ class PONativeAlternativePaymentLauncher private constructor( ).apply { customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = from, - callback = ::handleRedirect + callback = ::handleWebRedirect ) } @@ -138,6 +146,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + app = from.application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), @@ -150,7 +159,7 @@ class PONativeAlternativePaymentLauncher private constructor( ).apply { customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = from, - callback = ::handleRedirect + callback = ::handleWebRedirect ) } @@ -166,6 +175,7 @@ class PONativeAlternativePaymentLauncher private constructor( delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + app = from.application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), @@ -178,7 +188,7 @@ class PONativeAlternativePaymentLauncher private constructor( ).apply { customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = from, - callback = ::handleRedirect + callback = ::handleWebRedirect ) } @@ -191,6 +201,7 @@ class PONativeAlternativePaymentLauncher private constructor( from: ComponentActivity, callback: (ProcessOutActivityResult) -> Unit ) = PONativeAlternativePaymentLauncher( + app = from.application, scope = from.lifecycleScope, launcher = from.registerForActivityResult( NativeAlternativePaymentActivityContract(), @@ -203,7 +214,7 @@ class PONativeAlternativePaymentLauncher private constructor( ).apply { customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = from, - callback = ::handleRedirect + callback = ::handleWebRedirect ) } @@ -258,30 +269,39 @@ class PONativeAlternativePaymentLauncher private constructor( private fun launchHeadlessMode(configuration: PONativeAlternativePaymentConfiguration) { POLogger.info("Starting native alternative payment in headless mode.") - ConfigurationCache.value = configuration + LocalCache.configuration = configuration + continuePayment(configuration) + } + + private fun continuePayment( + configuration: PONativeAlternativePaymentConfiguration, + redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null + ) { scope.launch { when (val flow = configuration.flow) { - is Authorization -> authorize(flow, configuration) - is Tokenization -> tokenize(flow, configuration) + is Authorization -> authorize(flow, configuration, redirectConfirmation) + is Tokenization -> tokenize(flow, configuration, redirectConfirmation) } } } private suspend fun authorize( flow: Authorization, - configuration: PONativeAlternativePaymentConfiguration + configuration: PONativeAlternativePaymentConfiguration, + redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null ) { val request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, gatewayConfigurationId = flow.gatewayConfigurationId, - source = flow.customerTokenId + source = flow.customerTokenId, + redirectConfirmation = redirectConfirmation ) invoicesService.authorize(request) .onSuccess { response -> val updatedConfiguration = configuration.copy( flow = flow.copy(initialResponse = response) ) - ConfigurationCache.value = updatedConfiguration + LocalCache.configuration = updatedConfiguration handlePaymentState( state = response.state, redirect = response.redirect, @@ -295,19 +315,21 @@ class PONativeAlternativePaymentLauncher private constructor( private suspend fun tokenize( flow: Tokenization, - configuration: PONativeAlternativePaymentConfiguration + configuration: PONativeAlternativePaymentConfiguration, + redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null ) { val request = PONativeAlternativePaymentTokenizationRequest( customerId = flow.customerId, customerTokenId = flow.customerTokenId, - gatewayConfigurationId = flow.gatewayConfigurationId + gatewayConfigurationId = flow.gatewayConfigurationId, + redirectConfirmation = redirectConfirmation ) customerTokensService.tokenize(request) .onSuccess { response -> val updatedConfiguration = configuration.copy( flow = flow.copy(initialResponse = response) ) - ConfigurationCache.value = updatedConfiguration + LocalCache.configuration = updatedConfiguration handlePaymentState( state = response.state, redirect = response.redirect, @@ -356,59 +378,101 @@ class PONativeAlternativePaymentLauncher private constructor( launchActivity(configuration) return } + when (redirect.type) { + RedirectType.WEB -> webRedirect( + redirectUrl = redirect.url, + configuration = configuration + ) + RedirectType.DEEP_LINK -> deepLinkRedirect( + redirectUrl = redirect.url, + configuration = configuration + ) + RedirectType.UNKNOWN -> { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Unknown redirect type: ${redirect.rawType}" + ) + POLogger.error( + message = "Unexpected response: %s", failure, + attributes = configuration.logAttributes + ) + completeHeadlessMode(result = failure) + } + } + } + + private fun webRedirect( + redirectUrl: String, + configuration: PONativeAlternativePaymentConfiguration + ) { val returnUrl = configuration.redirect?.returnUrl if (returnUrl.isNullOrBlank()) { val failure = ProcessOutResult.Failure( code = Generic(), - message = "Return URL is missing in configuration during redirect flow." + message = "Return URL is missing in configuration during web redirect flow." ) POLogger.warn( - message = "Failed headless redirect: %s", failure, + message = "Failed headless web redirect: %s", failure, attributes = configuration.logAttributes ) completeHeadlessMode(result = failure) return } customTabLauncher.launch( - uri = redirect.url.toUri(), + uri = redirectUrl.toUri(), returnUrl = returnUrl ) } - private fun handleRedirect(result: ProcessOutResult) { - val configuration = ConfigurationCache.value + private fun handleWebRedirect(result: ProcessOutResult) { + val configuration = LocalCache.configuration result.onSuccess { if (configuration == null) { val failure = ProcessOutResult.Failure( code = Internal(), - message = "Configuration is not cached when handling a redirect result." + message = "Configuration is not cached when handling web redirect result." ) - POLogger.error(message = "Failed headless redirect: %s", failure) + POLogger.error(message = "Failed headless web redirect: %s", failure) completeHeadlessMode(result = failure) return } - scope.launch { - when (val flow = configuration.flow) { - is Authorization -> authorize(flow, configuration) - is Tokenization -> tokenize(flow, configuration) - } + val confirmationRequired = when (val flow = configuration.flow) { + is Authorization -> flow.initialResponse?.redirect?.confirmationRequired + is Tokenization -> flow.initialResponse?.redirect?.confirmationRequired } + val redirectConfirmation = if (confirmationRequired == true) + PONativeAlternativePaymentRedirectConfirmation(success = true) else null + continuePayment(configuration, redirectConfirmation) }.onFailure { failure -> POLogger.warn( - message = "Failed headless redirect: %s", failure, + message = "Failed headless web redirect: %s", failure, attributes = configuration?.logAttributes ) completeHeadlessMode(result = failure) } } + private fun deepLinkRedirect( + redirectUrl: String, + configuration: PONativeAlternativePaymentConfiguration + ) { + val didOpenUrl = app.openDeepLink(url = redirectUrl) + val confirmationRequired = when (val flow = configuration.flow) { + is Authorization -> flow.initialResponse?.redirect?.confirmationRequired + is Tokenization -> flow.initialResponse?.redirect?.confirmationRequired + } + val redirectConfirmation = if (confirmationRequired == true) + PONativeAlternativePaymentRedirectConfirmation(success = didOpenUrl) else null + continuePayment(configuration, redirectConfirmation) + } + private fun completeHeadlessMode(result: ProcessOutResult) { result.onFailure { failure -> scope.launch { eventDispatcher.send( event = DidFail( failure = failure, - paymentState = when (val flow = ConfigurationCache.value?.flow) { + paymentState = when (val flow = LocalCache.configuration?.flow) { is Authorization -> flow.initialResponse?.state ?: UNKNOWN is Tokenization -> flow.initialResponse?.state ?: UNKNOWN null -> UNKNOWN @@ -417,7 +481,7 @@ class PONativeAlternativePaymentLauncher private constructor( ) } } - ConfigurationCache.value = null + LocalCache.configuration = null callback(result.toActivityResult()) } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/ApplicationExtensions.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/ApplicationExtensions.kt new file mode 100644 index 000000000..8c787d609 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/ApplicationExtensions.kt @@ -0,0 +1,27 @@ +package com.processout.sdk.ui.shared.extension + +import android.app.Application +import android.content.ActivityNotFoundException +import android.content.Intent +import androidx.core.net.toUri +import androidx.core.os.ConfigurationCompat +import androidx.core.os.LocaleListCompat +import com.processout.sdk.core.logger.POLogger +import java.util.Locale + +internal fun Application.currentAppLocale(): Locale = + ConfigurationCompat.getLocales(resources.configuration)[0] + ?: LocaleListCompat.getAdjustedDefault()[0] + ?: Locale.getDefault() + +internal fun Application.openDeepLink(url: String): Boolean { + try { + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + return true + } catch (e: ActivityNotFoundException) { + POLogger.warn("Failed to open deep link [%s] with exception: %s", url, e) + return false + } +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/LocaleExtensions.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/LocaleExtensions.kt deleted file mode 100644 index e8af6e4db..000000000 --- a/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/LocaleExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.processout.sdk.ui.shared.extension - -import android.app.Application -import androidx.core.os.ConfigurationCompat -import androidx.core.os.LocaleListCompat -import java.util.Locale - -internal fun Application.currentAppLocale(): Locale = - ConfigurationCompat.getLocales(resources.configuration)[0] - ?: LocaleListCompat.getAdjustedDefault()[0] - ?: Locale.getDefault()