diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt index cfc434891..d04aeb8fb 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmFragment.kt @@ -119,7 +119,9 @@ class NativeApmFragment : BaseFragment( gatewayConfigurationId = uiModel.gatewayConfigurationId ), cancelButton = CancelButton(), - returnUrl = Constants.RETURN_URL, + redirect = RedirectConfiguration( + returnUrl = Constants.RETURN_URL + ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), cancelButton = CancelButton(disabledForSeconds = 3) @@ -134,11 +136,15 @@ class NativeApmFragment : BaseFragment( customerTokenId = uiModel.customerTokenId ), cancelButton = CancelButton(), - returnUrl = Constants.RETURN_URL, + redirect = RedirectConfiguration( + returnUrl = Constants.RETURN_URL, + enableHeadlessMode = true + ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), cancelButton = CancelButton(disabledForSeconds = 3) - ) + ), + success = null ) ) AUTHORIZE_LEGACY -> launcherLegacy.launch( @@ -155,7 +161,9 @@ class NativeApmFragment : BaseFragment( gatewayConfigurationId = uiModel.gatewayConfigurationId ), cancelButton = CancelButton(), - returnUrl = Constants.RETURN_URL, + redirect = RedirectConfiguration( + returnUrl = Constants.RETURN_URL + ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), cancelButton = CancelButton(disabledForSeconds = 3) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POImageResource.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POImageResource.kt index 894b3b51c..92ae06fa4 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POImageResource.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POImageResource.kt @@ -1,7 +1,9 @@ package com.processout.sdk.api.model.response +import android.os.Parcelable import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize /** * Image resource for light/dark themes. @@ -9,21 +11,23 @@ import com.squareup.moshi.JsonClass * @param[lightUrl] URL to image for light theme. * @param[darkUrl] URL to image for dark theme. */ +@Parcelize @JsonClass(generateAdapter = true) data class POImageResource( @Json(name = "light_url") val lightUrl: ResourceUrl, @Json(name = "dark_url") val darkUrl: ResourceUrl? -) { +) : Parcelable { /** * Image resource URL. * * @param[raster] URL to raster image. */ + @Parcelize @JsonClass(generateAdapter = true) data class ResourceUrl( val raster: String - ) + ) : Parcelable } 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 b2ba4adc8..fa29e3d0e 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 @@ -1,8 +1,10 @@ package com.processout.sdk.api.model.response.napm.v2 +import android.os.Parcelable import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse.Invoice import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize /** * Specifies details of native alternative payment. @@ -13,13 +15,14 @@ import com.squareup.moshi.JsonClass * @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. */ +@Parcelize data class PONativeAlternativePaymentAuthorizationResponse( val state: PONativeAlternativePaymentState, val invoice: Invoice, val paymentMethod: PONativeAlternativePaymentMethodDetails, val elements: List?, val redirect: PONativeAlternativePaymentRedirect? -) { +) : Parcelable { /** * Invoice details. @@ -27,11 +30,12 @@ data class PONativeAlternativePaymentAuthorizationResponse( * @param[amount] Invoice amount. * @param[currency] Invoice currency. */ + @Parcelize @JsonClass(generateAdapter = true) data class Invoice( val amount: String, val currency: String - ) + ) : Parcelable } @JsonClass(generateAdapter = true) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerInstruction.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerInstruction.kt index 1e8d341ba..be6e6f758 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerInstruction.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerInstruction.kt @@ -1,15 +1,17 @@ package com.processout.sdk.api.model.response.napm.v2 +import android.os.Parcelable import com.processout.sdk.api.model.response.POBarcode import com.processout.sdk.api.model.response.POImageResource import com.processout.sdk.core.annotation.ProcessOutInternalApi import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize /** * Specifies instruction for the customer, providing additional information and/or describing required actions. */ -sealed class PONativeAlternativePaymentCustomerInstruction { +sealed class PONativeAlternativePaymentCustomerInstruction : Parcelable { /** * Customer instruction provided as a markdown text. @@ -17,6 +19,7 @@ sealed class PONativeAlternativePaymentCustomerInstruction { * @param[label] Optional label. * @param[value] Markdown text. */ + @Parcelize @JsonClass(generateAdapter = true) data class Message( val label: String?, @@ -28,6 +31,7 @@ sealed class PONativeAlternativePaymentCustomerInstruction { * * @param[value] Image resource. */ + @Parcelize @JsonClass(generateAdapter = true) data class Image( val value: POImageResource @@ -39,6 +43,7 @@ sealed class PONativeAlternativePaymentCustomerInstruction { * @param[rawSubtype] Raw barcode subtype. * @param[rawValue] Base64 encoded value. */ + @Parcelize @JsonClass(generateAdapter = true) data class Barcode( @Json(name = "subtype") @@ -59,6 +64,7 @@ sealed class PONativeAlternativePaymentCustomerInstruction { * Placeholder that allows adding additional cases while staying backward compatible. * __Warning:__ Do not match this case directly, use _when-else_ instead. */ + @Parcelize @ProcessOutInternalApi data object Unknown : PONativeAlternativePaymentCustomerInstruction() } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentElement.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentElement.kt index 7665dee08..9f5e39962 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentElement.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentElement.kt @@ -1,19 +1,23 @@ package com.processout.sdk.api.model.response.napm.v2 +import android.os.Parcelable import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter 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.IgnoredOnParcel +import kotlinx.parcelize.Parcelize /** * Specifies an element that needs to be rendered on the UI during native alternative payment flow. */ -sealed class PONativeAlternativePaymentElement { +sealed class PONativeAlternativePaymentElement : Parcelable { /** * A form element with the specified [parameterDefinitions]. */ + @Parcelize data class Form( val parameterDefinitions: List ) : PONativeAlternativePaymentElement() { @@ -21,7 +25,7 @@ sealed class PONativeAlternativePaymentElement { /** * Payment parameter definition. */ - sealed class Parameter { + sealed class Parameter : Parcelable { /** Parameter key. */ abstract val key: String @@ -41,6 +45,7 @@ sealed class PONativeAlternativePaymentElement { * @param[minLength] Optional minimum length. * @param[maxLength] Optional maximum length. */ + @Parcelize @JsonClass(generateAdapter = true) data class Text( override val key: String, @@ -60,6 +65,7 @@ sealed class PONativeAlternativePaymentElement { * @param[required] Indicates whether the parameter is required. * @param[availableValues] Available values. */ + @Parcelize @JsonClass(generateAdapter = true) data class SingleSelect( override val key: String, @@ -80,12 +86,13 @@ sealed class PONativeAlternativePaymentElement { * @param[label] Value display label. * @param[preselected] Indicates whether the value should be preselected by default. */ + @Parcelize @JsonClass(generateAdapter = true) data class AvailableValue( val value: String, val label: String, val preselected: Boolean - ) + ) : Parcelable } /** @@ -95,6 +102,7 @@ sealed class PONativeAlternativePaymentElement { * @param[label] Parameter display label. * @param[required] Indicates whether the parameter is required. */ + @Parcelize @JsonClass(generateAdapter = true) data class Bool( override val key: String, @@ -111,6 +119,7 @@ sealed class PONativeAlternativePaymentElement { * @param[minLength] Optional minimum length. * @param[maxLength] Optional maximum length. */ + @Parcelize @JsonClass(generateAdapter = true) data class Digits( override val key: String, @@ -130,6 +139,7 @@ sealed class PONativeAlternativePaymentElement { * @param[required] Indicates whether the parameter is required. * @param[dialingCodes] Supported international dialing codes. */ + @Parcelize @JsonClass(generateAdapter = true) data class PhoneNumber( override val key: String, @@ -146,12 +156,13 @@ sealed class PONativeAlternativePaymentElement { * Corresponds to a two-letter ISO 3166-1 alpha-2 country code. * @param[value] Dialing code value. */ + @Parcelize @JsonClass(generateAdapter = true) data class DialingCode( @Json(name = "region_code") val regionCode: String, val value: String - ) + ) : Parcelable } /** @@ -161,6 +172,7 @@ sealed class PONativeAlternativePaymentElement { * @param[label] Parameter display label. * @param[required] Indicates whether the parameter is required. */ + @Parcelize @JsonClass(generateAdapter = true) data class Email( override val key: String, @@ -177,6 +189,7 @@ sealed class PONativeAlternativePaymentElement { * @param[minLength] Optional minimum length. * @param[maxLength] Optional maximum length. */ + @Parcelize @JsonClass(generateAdapter = true) data class Card( override val key: String, @@ -198,6 +211,7 @@ sealed class PONativeAlternativePaymentElement { * @param[minLength] Optional minimum length. * @param[maxLength] Optional maximum length. */ + @Parcelize @JsonClass(generateAdapter = true) data class Otp( override val key: String, @@ -238,10 +252,16 @@ sealed class PONativeAlternativePaymentElement { * Placeholder that allows adding additional cases while staying backward compatible. * __Warning:__ Do not match this case directly, use _when-else_ instead. */ + @Parcelize @ProcessOutInternalApi data object Unknown : Parameter() { + @IgnoredOnParcel override val key = String() + + @IgnoredOnParcel override val label = String() + + @IgnoredOnParcel override val required = false } } @@ -250,6 +270,7 @@ sealed class PONativeAlternativePaymentElement { /** * Specifies instruction for the customer, providing additional information and/or describing required actions. */ + @Parcelize data class CustomerInstruction( val instruction: PONativeAlternativePaymentCustomerInstruction ) : PONativeAlternativePaymentElement() @@ -260,6 +281,7 @@ sealed class PONativeAlternativePaymentElement { * @param[label] Optional group display label. * @param[instructions] Grouped instructions for the customer that provide additional information and/or describe required actions. */ + @Parcelize data class CustomerInstructionGroup( val label: String?, val instructions: List @@ -269,6 +291,7 @@ sealed class PONativeAlternativePaymentElement { * Placeholder that allows adding additional cases while staying backward compatible. * __Warning:__ Do not match this case directly, use _when-else_ instead. */ + @Parcelize @ProcessOutInternalApi data object Unknown : PONativeAlternativePaymentElement() } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt index 83f65c3c2..52d48cfd7 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentMethodDetails.kt @@ -1,8 +1,10 @@ package com.processout.sdk.api.model.response.napm.v2 +import android.os.Parcelable import com.processout.sdk.api.model.response.POImageResource import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize /** * Specifies payment method details. @@ -10,9 +12,10 @@ import com.squareup.moshi.JsonClass * @param[displayName] Payment method display name. * @param[logo] Image resource for light/dark themes. */ +@Parcelize @JsonClass(generateAdapter = true) data class PONativeAlternativePaymentMethodDetails( @Json(name = "display_name") val displayName: String, val logo: POImageResource -) +) : Parcelable 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 e86643acf..38787b5d5 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,8 @@ package com.processout.sdk.api.model.response.napm.v2 +import android.os.Parcelable import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize /** * Specifies native alternative payment redirect parameters. @@ -8,8 +10,9 @@ import com.squareup.moshi.JsonClass * @param[url] Redirect URL. * @param[hint] A hint or description associated with the redirect URL. */ +@Parcelize @JsonClass(generateAdapter = true) data class PONativeAlternativePaymentRedirect( val url: String, val hint: String -) +) : Parcelable 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 a58e781e5..2ac5cf539 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 @@ -1,7 +1,9 @@ package com.processout.sdk.api.model.response.napm.v2 +import android.os.Parcelable import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize /** * Specifies details of native alternative payment. @@ -11,12 +13,13 @@ import com.squareup.moshi.JsonClass * @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. */ +@Parcelize data class PONativeAlternativePaymentTokenizationResponse( val state: PONativeAlternativePaymentState, val paymentMethod: PONativeAlternativePaymentMethodDetails, val elements: List?, val redirect: PONativeAlternativePaymentRedirect? -) +) : Parcelable @JsonClass(generateAdapter = true) internal data class NativeAlternativePaymentTokenizationResponseBody( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt index 1468f42bf..fa00ea4cb 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutActivity.kt @@ -49,8 +49,7 @@ import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.googlepay.POGooglePayCardTokenizationLauncher import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration -import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow -import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration +import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.* import com.processout.sdk.ui.savedpaymentmethods.POSavedPaymentMethodsLauncher import com.processout.sdk.ui.savedpaymentmethods.delegate.POSavedPaymentMethodsDelegate import com.processout.sdk.ui.savedpaymentmethods.delegate.POSavedPaymentMethodsEvent @@ -134,6 +133,7 @@ class PODynamicCheckoutActivity : POBaseTransparentPortraitActivity() { } private fun nativeAlternativePaymentConfiguration(): PONativeAlternativePaymentConfiguration { + val returnUrl = configuration.alternativePayment.returnUrl val paymentConfirmation = configuration.alternativePayment.paymentConfirmation return PONativeAlternativePaymentConfiguration( flow = Flow.Authorization( @@ -142,6 +142,8 @@ class PODynamicCheckoutActivity : POBaseTransparentPortraitActivity() { ), submitButton = configuration.submitButton.map(), cancelButton = configuration.cancelButton?.map(), + redirect = if (!returnUrl.isNullOrBlank()) + RedirectConfiguration(returnUrl = returnUrl) else null, paymentConfirmation = PaymentConfirmationConfiguration( timeoutSeconds = paymentConfirmation.timeoutSeconds, confirmButton = paymentConfirmation.confirmButton?.map(), diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/LogAttributes.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/LogAttributes.kt new file mode 100644 index 000000000..73cd2c9ed --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/LogAttributes.kt @@ -0,0 +1,21 @@ +package com.processout.sdk.ui.napm + +import com.processout.sdk.core.logger.POLogAttribute.CUSTOMER_ID +import com.processout.sdk.core.logger.POLogAttribute.CUSTOMER_TOKEN_ID +import com.processout.sdk.core.logger.POLogAttribute.GATEWAY_CONFIGURATION_ID +import com.processout.sdk.core.logger.POLogAttribute.INVOICE_ID +import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Authorization +import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow.Tokenization + +internal val PONativeAlternativePaymentConfiguration.logAttributes: Map + get() = when (flow) { + is Authorization -> mapOf( + INVOICE_ID to flow.invoiceId, + GATEWAY_CONFIGURATION_ID to flow.gatewayConfigurationId + ) + is Tokenization -> mapOf( + CUSTOMER_ID to flow.customerId, + CUSTOMER_TOKEN_ID to flow.customerTokenId, + GATEWAY_CONFIGURATION_ID to flow.gatewayConfigurationId + ) + } 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 67dcb8983..cdbabde13 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 @@ -76,7 +76,8 @@ internal class NativeAlternativePaymentBottomSheet : BaseBottomSheetDialogFragme flow = Flow.Authorization( invoiceId = String(), gatewayConfigurationId = String() - ) + ), + redirect = null ) alternativePaymentLauncher = POAlternativePaymentMethodCustomTabLauncher.create( from = this, 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 1aeb453dc..ad3271fc4 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 @@ -38,7 +38,6 @@ import com.processout.sdk.core.POFailure.InvalidField import com.processout.sdk.core.POFailure.ValidationCode 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 com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess @@ -53,7 +52,6 @@ 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.PONativeAlternativePaymentConfiguration.CancelButton -import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Flow 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 @@ -82,25 +80,9 @@ internal class NativeAlternativePaymentInteractor( private val barcodeBitmapProvider: BarcodeBitmapProvider, private val mediaStorageProvider: MediaStorageProvider, private val captureRetryStrategy: PORetryStrategy, - private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance, - private var logAttributes: Map = logAttributes(configuration.flow) + private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance ) : BaseInteractor() { - private companion object { - fun logAttributes(flow: Flow): Map = - when (flow) { - is Authorization -> mapOf( - POLogAttribute.INVOICE_ID to flow.invoiceId, - POLogAttribute.GATEWAY_CONFIGURATION_ID to flow.gatewayConfigurationId - ) - is Tokenization -> mapOf( - POLogAttribute.CUSTOMER_ID to flow.customerId, - POLogAttribute.CUSTOMER_TOKEN_ID to flow.customerTokenId, - POLogAttribute.GATEWAY_CONFIGURATION_ID to flow.gatewayConfigurationId - ) - } - } - private val _completion = MutableStateFlow(Awaiting) val completion = _completion.asStateFlow() @@ -135,7 +117,6 @@ internal class NativeAlternativePaymentInteractor( return } this.configuration = configuration - logAttributes = logAttributes(configuration.flow) start() } @@ -159,6 +140,19 @@ internal class NativeAlternativePaymentInteractor( } private suspend fun fetchAuthorizationDetails(flow: Authorization) { + val initialResponse = flow.initialResponse + if (initialResponse != null) { + handlePaymentState( + stateValue = initNextStepStateValue( + paymentMethod = initialResponse.paymentMethod, + invoice = initialResponse.invoice + ), + paymentState = initialResponse.state, + elements = initialResponse.elements, + redirect = initialResponse.redirect + ) + return + } val request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, gatewayConfigurationId = flow.gatewayConfigurationId, @@ -182,6 +176,19 @@ internal class NativeAlternativePaymentInteractor( } private suspend fun fetchTokenizationDetails(flow: Tokenization) { + val initialResponse = flow.initialResponse + if (initialResponse != null) { + handlePaymentState( + stateValue = initNextStepStateValue( + paymentMethod = initialResponse.paymentMethod, + invoice = null + ), + paymentState = initialResponse.state, + elements = initialResponse.elements, + redirect = initialResponse.redirect + ) + return + } val request = PONativeAlternativePaymentTokenizationRequest( customerId = flow.customerId, customerTokenId = flow.customerTokenId, @@ -252,7 +259,7 @@ internal class NativeAlternativePaymentInteractor( ) POLogger.error( message = "%s", failure, - attributes = logAttributes + attributes = configuration.logAttributes ) _completion.update { Failure(failure) } } @@ -272,7 +279,7 @@ internal class NativeAlternativePaymentInteractor( if (parameters.isEmpty()) { POLogger.warn( message = "Parameters is empty in response.", - attributes = logAttributes + attributes = configuration.logAttributes ) } if (failWithUnknownParameter(parameters)) { @@ -363,7 +370,7 @@ internal class NativeAlternativePaymentInteractor( ) POLogger.error( message = "Unexpected response: %s", failure, - attributes = logAttributes + attributes = configuration.logAttributes ) _completion.update { Failure(failure) } return true @@ -620,7 +627,7 @@ internal class NativeAlternativePaymentInteractor( stateValue: NextStepStateValue, redirectUrl: String ) { - val returnUrl = configuration.returnUrl + val returnUrl = configuration.redirect?.returnUrl if (returnUrl.isNullOrBlank()) { val failure = ProcessOutResult.Failure( code = Generic(), @@ -628,7 +635,7 @@ internal class NativeAlternativePaymentInteractor( ) POLogger.warn( message = "Failed redirect: %s", failure, - attributes = logAttributes + attributes = configuration.logAttributes ) _completion.update { Failure(failure) } return @@ -1258,7 +1265,10 @@ internal class NativeAlternativePaymentInteractor( interactorScope.launch { _completion.collect { if (it is Failure) { - POLogger.warn("%s", it.failure, attributes = logAttributes) + POLogger.warn( + message = "%s", it.failure, + attributes = configuration.logAttributes + ) dispatch(DidFail(it.failure, paymentState)) } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt index 162c48c11..6e6ba0db7 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt @@ -4,6 +4,8 @@ import android.os.Parcelable import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.IntRange +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentTokenizationResponse import com.processout.sdk.ui.core.shared.image.PODrawableImage import com.processout.sdk.ui.core.style.* import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.CancelButton @@ -26,7 +28,7 @@ import kotlinx.parcelize.Parcelize * displayed inline for parameters where user should select a single option (e.g. radio buttons). * Default value is _5_. * @param[barcode] Barcode configuration. - * @param[returnUrl] Deep link return URL. Required for the flows that include web redirect. + * @param[redirect] Redirect configuration. Required for the flows that include web redirect. * @param[paymentConfirmation] Payment confirmation configuration. * @param[success] Success screen configuration. Pass _null_ to skip the success screen. * @param[bottomSheet] Bottom sheet configuration. @@ -40,7 +42,7 @@ data class PONativeAlternativePaymentConfiguration( val cancelButton: CancelButton? = null, val inlineSingleSelectValuesLimit: Int = 5, val barcode: POBarcodeConfiguration = POBarcodeConfiguration(saveButton = POBarcodeConfiguration.Button()), - val returnUrl: String? = null, + val redirect: RedirectConfiguration? = null, val paymentConfirmation: PaymentConfirmationConfiguration = PaymentConfirmationConfiguration(), val success: SuccessConfiguration? = SuccessConfiguration(), val bottomSheet: POBottomSheetConfiguration = POBottomSheetConfiguration( @@ -65,7 +67,8 @@ data class PONativeAlternativePaymentConfiguration( data class Authorization( val invoiceId: String, val gatewayConfigurationId: String, - val customerTokenId: String? = null + val customerTokenId: String? = null, + internal val initialResponse: PONativeAlternativePaymentAuthorizationResponse? = null ) : Flow() /** @@ -79,10 +82,59 @@ data class PONativeAlternativePaymentConfiguration( data class Tokenization( val customerId: String, val customerTokenId: String, - val gatewayConfigurationId: String + val gatewayConfigurationId: String, + internal val initialResponse: PONativeAlternativePaymentTokenizationResponse? = null ) : Flow() } + /** + * Native alternative payment configuration. + * + * @param[flow] Payment flow configuration. + * @param[title] Custom title. + * @param[submitButton] Submit button configuration. + * @param[cancelButton] Cancel button configuration. Use _null_ to hide, this is a default behaviour. + * @param[inlineSingleSelectValuesLimit] Defines the maximum number of options that will be + * displayed inline for parameters where user should select a single option (e.g. radio buttons). + * Default value is _5_. + * @param[barcode] Barcode configuration. + * @param[returnUrl] Deep link return URL. Required for the flows that include web redirect. + * @param[paymentConfirmation] Payment confirmation configuration. + * @param[success] Success screen configuration. Pass _null_ to skip the success screen. + * @param[bottomSheet] Bottom sheet configuration. + * @param[style] Custom style. + */ + @Deprecated(message = "Use alternative constructor.") + constructor( + flow: Flow, + title: String? = null, + submitButton: Button = Button(), + cancelButton: CancelButton? = null, + inlineSingleSelectValuesLimit: Int = 5, + barcode: POBarcodeConfiguration = POBarcodeConfiguration(saveButton = POBarcodeConfiguration.Button()), + returnUrl: String? = null, + paymentConfirmation: PaymentConfirmationConfiguration = PaymentConfirmationConfiguration(), + success: SuccessConfiguration? = SuccessConfiguration(), + bottomSheet: POBottomSheetConfiguration = POBottomSheetConfiguration( + height = WrapContent, + expandable = true + ), + style: Style? = null + ) : this( + flow = flow, + title = title, + submitButton = submitButton, + cancelButton = cancelButton, + inlineSingleSelectValuesLimit = inlineSingleSelectValuesLimit, + barcode = barcode, + redirect = if (!returnUrl.isNullOrBlank()) + RedirectConfiguration(returnUrl = returnUrl) else null, + paymentConfirmation = paymentConfirmation, + success = success, + bottomSheet = bottomSheet, + style = style + ) + /** * Native alternative payment configuration. * @@ -125,6 +177,7 @@ data class PONativeAlternativePaymentConfiguration( cancelButton = cancelButton, inlineSingleSelectValuesLimit = inlineSingleSelectValuesLimit, barcode = barcode, + returnUrl = null, paymentConfirmation = paymentConfirmation, success = if (!skipSuccessScreen) SuccessConfiguration(message = successMessage) else null, @@ -279,6 +332,20 @@ data class PONativeAlternativePaymentConfiguration( val touchOutside: Boolean = true ) : Parcelable + /** + * Redirect configuration. + * + * @param[returnUrl] Deep link return URL. Required for the flows that include web redirect. + * @param[enableHeadlessMode] Enables headless mode. + * The web redirect will be handled directly when it's the first step in the flow, + * and if it's the only required step it will complete the flow without starting the bottom sheet. + */ + @Parcelize + data class RedirectConfiguration( + val returnUrl: String, + val enableHeadlessMode: Boolean = false + ) : Parcelable + /** * Payment confirmation configuration. * 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 276c83868..172815de6 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 @@ -4,15 +4,31 @@ import android.content.Context import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.core.app.ActivityOptionsCompat +import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.processout.sdk.R +import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.POEventDispatcher -import com.processout.sdk.core.POUnit -import com.processout.sdk.core.ProcessOutActivityResult +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.POAlternativePaymentMethodResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect +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 +import com.processout.sdk.api.service.POInvoicesService +import com.processout.sdk.core.* +import com.processout.sdk.core.POFailure.Code.Generic +import com.processout.sdk.core.POFailure.Code.Internal +import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.ui.apm.POAlternativePaymentMethodCustomTabLauncher +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.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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -25,9 +41,18 @@ class PONativeAlternativePaymentLauncher private constructor( private val launcher: ActivityResultLauncher, private val activityOptions: ActivityOptionsCompat, private val delegate: PONativeAlternativePaymentDelegate, - private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance + private val callback: (ProcessOutActivityResult) -> Unit, + private val eventDispatcher: POEventDispatcher = POEventDispatcher.instance, + private val invoicesService: POInvoicesService = ProcessOut.instance.invoices, + private val customerTokensService: POCustomerTokensService = ProcessOut.instance.customerTokens ) { + private lateinit var customTabLauncher: POAlternativePaymentMethodCustomTabLauncher + + private object ConfigurationCache { + var value: PONativeAlternativePaymentConfiguration? = null + } + companion object { /** * Creates the launcher from Fragment. @@ -44,8 +69,14 @@ class PONativeAlternativePaymentLauncher private constructor( callback ), activityOptions = createActivityOptions(from.requireContext()), - delegate = delegate - ) + delegate = delegate, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleRedirect + ) + } /** * Creates the launcher from Fragment. @@ -65,8 +96,14 @@ class PONativeAlternativePaymentLauncher private constructor( callback ), activityOptions = createActivityOptions(from.requireContext()), - delegate = object : PONativeAlternativePaymentDelegate {} - ) + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleRedirect + ) + } /** * Creates the launcher from Fragment. @@ -83,8 +120,14 @@ class PONativeAlternativePaymentLauncher private constructor( callback ), activityOptions = createActivityOptions(from.requireContext()), - delegate = object : PONativeAlternativePaymentDelegate {} - ) + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleRedirect + ) + } /** * Creates the launcher from Activity. @@ -102,8 +145,14 @@ class PONativeAlternativePaymentLauncher private constructor( callback ), activityOptions = createActivityOptions(from), - delegate = delegate - ) + delegate = delegate, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleRedirect + ) + } /** * Creates the launcher from Activity. @@ -124,8 +173,14 @@ class PONativeAlternativePaymentLauncher private constructor( callback ), activityOptions = createActivityOptions(from), - delegate = object : PONativeAlternativePaymentDelegate {} - ) + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleRedirect + ) + } /** * Creates the launcher from Activity. @@ -143,8 +198,14 @@ class PONativeAlternativePaymentLauncher private constructor( callback ), activityOptions = createActivityOptions(from), - delegate = object : PONativeAlternativePaymentDelegate {} - ) + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleRedirect + ) + } private fun createActivityOptions(context: Context) = ActivityOptionsCompat.makeCustomAnimation( @@ -181,6 +242,182 @@ class PONativeAlternativePaymentLauncher private constructor( * Launches the activity. */ fun launch(configuration: PONativeAlternativePaymentConfiguration) { - launcher.launch(configuration, activityOptions) + if (configuration.redirect?.enableHeadlessMode == true) { + launchHeadlessMode(configuration) + } else { + launchActivity(configuration) + } + } + + private fun launchActivity(configuration: PONativeAlternativePaymentConfiguration) { + launcher.launch( + input = configuration, + options = activityOptions + ) + } + + private fun launchHeadlessMode(configuration: PONativeAlternativePaymentConfiguration) { + POLogger.info("Starting native alternative payment in headless mode.") + ConfigurationCache.value = configuration + scope.launch { + when (val flow = configuration.flow) { + is Authorization -> authorize(flow, configuration) + is Tokenization -> tokenize(flow, configuration) + } + } + } + + private suspend fun authorize( + flow: Authorization, + configuration: PONativeAlternativePaymentConfiguration + ) { + val request = PONativeAlternativePaymentAuthorizationRequest( + invoiceId = flow.invoiceId, + gatewayConfigurationId = flow.gatewayConfigurationId, + source = flow.customerTokenId + ) + invoicesService.authorize(request) + .onSuccess { response -> + val updatedConfiguration = configuration.copy( + flow = flow.copy(initialResponse = response) + ) + ConfigurationCache.value = updatedConfiguration + handlePaymentState( + state = response.state, + redirect = response.redirect, + configuration = updatedConfiguration + ) + }.onFailure { failure -> + POLogger.info("Failed to fetch authorization details: %s", failure) + completeHeadlessMode(result = failure) + } + } + + private suspend fun tokenize( + flow: Tokenization, + configuration: PONativeAlternativePaymentConfiguration + ) { + val request = PONativeAlternativePaymentTokenizationRequest( + customerId = flow.customerId, + customerTokenId = flow.customerTokenId, + gatewayConfigurationId = flow.gatewayConfigurationId + ) + customerTokensService.tokenize(request) + .onSuccess { response -> + val updatedConfiguration = configuration.copy( + flow = flow.copy(initialResponse = response) + ) + ConfigurationCache.value = updatedConfiguration + handlePaymentState( + state = response.state, + redirect = response.redirect, + configuration = updatedConfiguration + ) + }.onFailure { failure -> + POLogger.info("Failed to fetch tokenization details: %s", failure) + completeHeadlessMode(result = failure) + } + } + + private fun handlePaymentState( + state: PONativeAlternativePaymentState, + redirect: PONativeAlternativePaymentRedirect?, + configuration: PONativeAlternativePaymentConfiguration + ) { + when (state) { + NEXT_STEP_REQUIRED -> handleNextStep(redirect, configuration) + PENDING -> launchActivity(configuration) + SUCCESS -> + if (configuration.success != null) { + launchActivity(configuration) + } else { + POLogger.info("Success: payment completed.") + completeHeadlessMode(result = ProcessOutResult.Success(value = POUnit)) + } + UNKNOWN -> { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Unsupported payment state." + ) + POLogger.error( + message = "%s", failure, + attributes = configuration.logAttributes + ) + completeHeadlessMode(result = failure) + } + } + } + + private fun handleNextStep( + redirect: PONativeAlternativePaymentRedirect?, + configuration: PONativeAlternativePaymentConfiguration + ) { + if (redirect == null) { + launchActivity(configuration) + return + } + val returnUrl = configuration.redirect?.returnUrl + if (returnUrl.isNullOrBlank()) { + val failure = ProcessOutResult.Failure( + code = Generic(), + message = "Return URL is missing in configuration during redirect flow." + ) + POLogger.warn( + message = "Failed headless redirect: %s", failure, + attributes = configuration.logAttributes + ) + completeHeadlessMode(result = failure) + return + } + customTabLauncher.launch( + uri = redirect.url.toUri(), + returnUrl = returnUrl + ) + } + + private fun handleRedirect(result: ProcessOutResult) { + val configuration = ConfigurationCache.value + result.onSuccess { + if (configuration == null) { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Configuration is not cached when handling a redirect result." + ) + POLogger.error(message = "Failed headless redirect: %s", failure) + completeHeadlessMode(result = failure) + return + } + scope.launch { + when (val flow = configuration.flow) { + is Authorization -> authorize(flow, configuration) + is Tokenization -> tokenize(flow, configuration) + } + } + }.onFailure { failure -> + POLogger.warn( + message = "Failed headless redirect: %s", failure, + attributes = configuration?.logAttributes + ) + completeHeadlessMode(result = failure) + } + } + + private fun completeHeadlessMode(result: ProcessOutResult) { + result.onFailure { failure -> + scope.launch { + eventDispatcher.send( + event = DidFail( + failure = failure, + paymentState = when (val flow = ConfigurationCache.value?.flow) { + is Authorization -> flow.initialResponse?.state ?: UNKNOWN + is Tokenization -> flow.initialResponse?.state ?: UNKNOWN + null -> UNKNOWN + } + ) + ) + } + } + ConfigurationCache.value = null + callback(result.toActivityResult()) } }