diff --git a/build.gradle b/build.gradle index f1ae41b6b..3c6103c1f 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension buildscript { ext { - androidGradlePluginVersion = '8.13.2' + androidGradlePluginVersion = '9.1.0' kotlinVersion = '2.1.20' kspVersion = '2.1.20-1.0.32' dokkaVersion = '1.9.20' diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 45e3dd3cb..87f46bc2d 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -30,6 +30,17 @@ + + + + + + + + diff --git a/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt b/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt index d8f42d678..124ac73ac 100644 --- a/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt +++ b/example/src/main/kotlin/com/processout/example/redirect/MerchantRedirectActivity.kt @@ -8,10 +8,9 @@ class MerchantRedirectActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - ProcessOut.instance.processDeepLink( - activity = this, - uri = intent.data - ) + intent.data?.let { uri -> + ProcessOut.instance.processDeepLink(hostActivity = this, uri) + } finish() } } diff --git a/example/src/main/kotlin/com/processout/example/shared/Constants.kt b/example/src/main/kotlin/com/processout/example/shared/Constants.kt index 486bf5b51..914962d20 100644 --- a/example/src/main/kotlin/com/processout/example/shared/Constants.kt +++ b/example/src/main/kotlin/com/processout/example/shared/Constants.kt @@ -3,6 +3,7 @@ package com.processout.example.shared import com.processout.example.BuildConfig object Constants { - const val DEFAULT_RETURN_URL = "${BuildConfig.APPLICATION_ID}://processout/return" - const val MERCHANT_RETURN_URL = "merchant://example/return" + const val RETURN_URL_DEFAULT = "${BuildConfig.APPLICATION_ID}://processout/return" + const val RETURN_URL_APP_LINK = "https://merchant-example.com/return" + const val RETURN_URL_DEEP_LINK = "merchant://example/return" } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt index 3ad13f8ed..51bf6af76 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt @@ -117,7 +117,7 @@ class CardPaymentFragment : BaseFragment( delegate = Netcetera3DS2ServiceDelegate( provideActivity = { POCardTokenizationActivity.instance }, customTabLauncher = customTabLauncher, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ), configuration = PONetcetera3DS2ServiceConfiguration( bridgingExtensionVersion = BridgingMessageExtensionVersion.V20 @@ -130,7 +130,7 @@ class CardPaymentFragment : BaseFragment( delegate = Checkout3DSServiceDelegate( activity = requireActivity(), customTabLauncher = customTabLauncher, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ).build() diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt index 49551d20e..e7b6c89ae 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentViewModel.kt @@ -66,7 +66,7 @@ class CardPaymentViewModel( amount = state.amount, currency = state.currency, customerId = state.customerId, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ).getOrNull() _state.update { it.copy(invoiceId = invoice?.id) } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt index 99843217b..ec9c4814c 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DefaultDynamicCheckoutDelegate.kt @@ -43,7 +43,7 @@ class DefaultDynamicCheckoutDelegate( amount = details.amount, currency = details.currency, customerId = customerId, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ) } diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt index f3f772c4e..f3b33328f 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutFragment.kt @@ -98,7 +98,7 @@ class DynamicCheckoutFragment : BaseFragment( clientSecret = uiModel.clientSecret ), alternativePayment = AlternativePaymentConfiguration( - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ) ) @@ -111,7 +111,7 @@ class DynamicCheckoutFragment : BaseFragment( delegate = Netcetera3DS2ServiceDelegate( provideActivity = { PODynamicCheckoutActivity.instance }, customTabLauncher = customTabLauncher, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ), configuration = PONetcetera3DS2ServiceConfiguration( bridgingExtensionVersion = BridgingMessageExtensionVersion.V20 diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt index cc3f82d90..ef24d9b35 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/checkout/DynamicCheckoutViewModel.kt @@ -59,7 +59,7 @@ class DynamicCheckoutViewModel( amount = details.amount, currency = details.currency, customerId = customerId, - returnUrl = Constants.DEFAULT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEFAULT ) ).onSuccess { invoice -> _uiState.value = Submitted( 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 0a7433b64..933b971a4 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 @@ -120,8 +120,8 @@ class NativeApmFragment : BaseFragment( ), cancelButton = CancelButton(), redirect = RedirectConfiguration( - returnUrl = Constants.MERCHANT_RETURN_URL, - enableHeadlessMode = true + returnUrl = Constants.RETURN_URL_DEEP_LINK, + enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), @@ -138,8 +138,8 @@ class NativeApmFragment : BaseFragment( ), cancelButton = CancelButton(), redirect = RedirectConfiguration( - returnUrl = Constants.MERCHANT_RETURN_URL, - enableHeadlessMode = true + returnUrl = Constants.RETURN_URL_DEEP_LINK, + enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), @@ -163,7 +163,8 @@ class NativeApmFragment : BaseFragment( ), cancelButton = CancelButton(), redirect = RedirectConfiguration( - returnUrl = Constants.MERCHANT_RETURN_URL + returnUrl = Constants.RETURN_URL_DEEP_LINK, + enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked ), paymentConfirmation = PaymentConfirmationConfiguration( confirmButton = Button(), diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt index 6f5acae7f..af2903fac 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/nativeapm/NativeApmViewModel.kt @@ -62,7 +62,7 @@ class NativeApmViewModel( amount = amount, currency = currency, customerId = customerId, - returnUrl = Constants.MERCHANT_RETURN_URL, + returnUrl = Constants.RETURN_URL_DEEP_LINK, shipping = POContact( address1 = "6th Street", city = "Paris", diff --git a/example/src/main/res/layout/fragment_native_apm.xml b/example/src/main/res/layout/fragment_native_apm.xml index 8b29f4fb9..2f0b6ac7b 100644 --- a/example/src/main/res/layout/fragment_native_apm.xml +++ b/example/src/main/res/layout/fragment_native_apm.xml @@ -74,6 +74,15 @@ android:inputType="text|textCapCharacters" /> + + () val events = _events.asSharedFlow() + @PublishedApi + internal val subscriptions = mutableSetOf>() + private val _requests = MutableSharedFlow() val requests = _requests.asSharedFlow() @@ -53,15 +57,29 @@ class POEventDispatcher { coroutineScope: CoroutineScope, crossinline onEvent: (T) -> Unit ) { + synchronized(subscriptions) { + subscriptions.add(T::class) + } coroutineScope.launch { - events.filterIsInstance() - .collect { event -> - coroutineContext.ensureActive() - onEvent(event) + try { + events.filterIsInstance() + .collect { event -> + coroutineContext.ensureActive() + onEvent(event) + } + } finally { + synchronized(subscriptions) { + subscriptions.remove(T::class) } + } } } + inline fun hasSubscribers(): Boolean = + synchronized(subscriptions) { + subscriptions.contains(T::class) + } + inline fun subscribeForRequest( coroutineScope: CoroutineScope, crossinline onRequest: (T) -> Unit diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/event/PODeepLinkReceivedEvent.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/event/PODeepLinkReceivedEvent.kt new file mode 100644 index 000000000..738953e0b --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/event/PODeepLinkReceivedEvent.kt @@ -0,0 +1,10 @@ +package com.processout.sdk.api.model.event + +import android.net.Uri +import com.processout.sdk.core.annotation.ProcessOutInternalApi + +/** @suppress */ +@ProcessOutInternalApi +data class PODeepLinkReceivedEvent( + val uri: Uri +) 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 43500d7d3..7e6efaef3 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 @@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass internal data class NativeAlternativePaymentRequestBody( @Json(name = "gateway_configuration_id") val gatewayConfigurationId: String, + val configuration: Configuration, val source: String?, @Json(name = "submit_data") val submitData: SubmitData?, @@ -14,6 +15,12 @@ internal data class NativeAlternativePaymentRequestBody( val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? ) { + @JsonClass(generateAdapter = true) + data class Configuration( + @Json(name = "return_redirect_type") + val returnRedirectType: String + ) + @JsonClass(generateAdapter = true) data class SubmitData( val parameters: Map 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 cbc813fdd..ee27e3ade 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 @@ -5,6 +5,8 @@ package com.processout.sdk.api.model.request.napm.v2 * * @param[invoiceId] Invoice identifier. * @param[gatewayConfigurationId] Gateway configuration identifier. + * @param[configuration] Payment configuration. + * __Note:__ Configuration is respected _only on the first_ payment request and ignored on subsequent calls. * @param[source] Payment source. * @param[submitData] Payment payload. * @param[redirectConfirmation] Redirect confirmation. @@ -12,6 +14,7 @@ package com.processout.sdk.api.model.request.napm.v2 data class PONativeAlternativePaymentAuthorizationRequest( val invoiceId: String, val gatewayConfigurationId: String, + val configuration: PONativeAlternativePaymentRequestConfiguration = PONativeAlternativePaymentRequestConfiguration(), val source: String? = null, val submitData: PONativeAlternativePaymentSubmitData? = null, val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRequestConfiguration.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRequestConfiguration.kt new file mode 100644 index 000000000..29bb78ca6 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentRequestConfiguration.kt @@ -0,0 +1,26 @@ +package com.processout.sdk.api.model.request.napm.v2 + +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRequestConfiguration.ReturnRedirectType.AUTOMATIC +import com.squareup.moshi.JsonClass + +/** + * Payment configuration. + * + * @param[returnRedirectType] Return redirect type. By default [AUTOMATIC]. + */ +data class PONativeAlternativePaymentRequestConfiguration( + val returnRedirectType: ReturnRedirectType = AUTOMATIC +) { + + /** + * Return redirect type. + */ + @JsonClass(generateAdapter = false) + enum class ReturnRedirectType(val rawValue: String) { + /** Redirect result is handled automatically. */ + AUTOMATIC("automatic"), + + /** Redirect result is not processed automatically and should be resolved explicitly. */ + MANUAL("manual") + } +} 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 7266e7170..9d04abf42 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 @@ -6,6 +6,8 @@ package com.processout.sdk.api.model.request.napm.v2 * @param[customerId] Customer identifier. * @param[customerTokenId] Customer token identifier. * @param[gatewayConfigurationId] Gateway configuration identifier. + * @param[configuration] Payment configuration. + * __Note:__ Configuration is respected _only on the first_ payment request and ignored on subsequent calls. * @param[submitData] Payment payload. * @param[redirectConfirmation] Redirect confirmation. */ @@ -13,6 +15,7 @@ data class PONativeAlternativePaymentTokenizationRequest( val customerId: String, val customerTokenId: String, val gatewayConfigurationId: String, + val configuration: PONativeAlternativePaymentRequestConfiguration = PONativeAlternativePaymentRequestConfiguration(), val submitData: PONativeAlternativePaymentSubmitData? = null, val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null ) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentUrlResolutionRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentUrlResolutionRequest.kt new file mode 100644 index 000000000..0cb05a9cd --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/napm/v2/PONativeAlternativePaymentUrlResolutionRequest.kt @@ -0,0 +1,37 @@ +package com.processout.sdk.api.model.request.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.JsonClass + +/** + * Request parameters for redirect URL resolution during native alternative payment. + * + * @param[redirect] Redirect information. + */ +@ProcessOutInternalApi +@JsonClass(generateAdapter = true) +data class PONativeAlternativePaymentUrlResolutionRequest( + val redirect: Redirect +) { + + /** + * Redirect information. + * + * @param[result] Redirect result. + */ + @JsonClass(generateAdapter = true) + data class Redirect( + val result: Result + ) { + + /** + * Redirect result. + * + * @param[url] Result URL. + */ + @JsonClass(generateAdapter = true) + data class Result( + val url: String + ) + } +} 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 fa29e3d0e..119c25810 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 @@ -27,12 +27,14 @@ data class PONativeAlternativePaymentAuthorizationResponse( /** * Invoice details. * + * @param[id] Invoice identifier. * @param[amount] Invoice amount. * @param[currency] Invoice currency. */ @Parcelize @JsonClass(generateAdapter = true) data class Invoice( + val id: String, val amount: String, val currency: String ) : Parcelable diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerToken.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerToken.kt new file mode 100644 index 000000000..5961eabe9 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentCustomerToken.kt @@ -0,0 +1,15 @@ +package com.processout.sdk.api.model.response.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.JsonClass + +/** + * Customer token details. + * + * @param[id] Customer token. + */ +@ProcessOutInternalApi +@JsonClass(generateAdapter = true) +data class PONativeAlternativePaymentCustomerToken( + val id: String +) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentInvoice.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentInvoice.kt new file mode 100644 index 000000000..c46aa3de6 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentInvoice.kt @@ -0,0 +1,19 @@ +package com.processout.sdk.api.model.response.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.JsonClass + +/** + * Invoice details. + * + * @param[id] Invoice identifier. + * @param[amount] Invoice amount. + * @param[currency] Invoice currency. + */ +@ProcessOutInternalApi +@JsonClass(generateAdapter = true) +data class PONativeAlternativePaymentInvoice( + val id: String, + val amount: String, + val currency: String +) 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 52d48cfd7..e6beb1b37 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 @@ -9,12 +9,15 @@ import kotlinx.parcelize.Parcelize /** * Specifies payment method details. * + * @param[gatewayConfigurationId] Gateway configuration identifier. * @param[displayName] Payment method display name. * @param[logo] Image resource for light/dark themes. */ @Parcelize @JsonClass(generateAdapter = true) data class PONativeAlternativePaymentMethodDetails( + @Json(name = "gateway_configuration_id") + val gatewayConfigurationId: String, @Json(name = "display_name") val displayName: String, val logo: POImageResource 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 new file mode 100644 index 000000000..49a6e5e46 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentUrlResolutionResponse.kt @@ -0,0 +1,37 @@ +package com.processout.sdk.api.model.response.napm.v2 + +import com.processout.sdk.core.annotation.ProcessOutInternalApi +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Specifies details of native alternative payment after redirect URL resolution. + * + * @param[state] State of native alternative payment. + * @param[paymentMethod] Payment method details. + * @param[invoice] Invoice details if any. + * @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. + */ +@ProcessOutInternalApi +data class PONativeAlternativePaymentUrlResolutionResponse( + val state: PONativeAlternativePaymentState, + val paymentMethod: PONativeAlternativePaymentMethodDetails, + val invoice: PONativeAlternativePaymentInvoice?, + val customerToken: PONativeAlternativePaymentCustomerToken?, + val elements: List?, + val redirect: PONativeAlternativePaymentRedirect? +) + +@JsonClass(generateAdapter = true) +internal data class NativeAlternativePaymentUrlResolutionResponseBody( + val state: PONativeAlternativePaymentState, + @Json(name = "payment_method") + val paymentMethod: PONativeAlternativePaymentMethodDetails, + val invoice: PONativeAlternativePaymentInvoice?, + @Json(name = "customer_token") + val customerToken: PONativeAlternativePaymentCustomerToken?, + val elements: List?, + val redirect: PONativeAlternativePaymentRedirect? +) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt b/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt index e0bf7248b..15b058ab3 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/network/InvoicesApi.kt @@ -5,8 +5,10 @@ import com.processout.sdk.api.model.request.NativeAPMRequestBody import com.processout.sdk.api.model.request.NativeAlternativePaymentCaptureRequest import com.processout.sdk.api.model.request.POCreateInvoiceRequest import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.* import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentAuthorizationResponseBody +import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentUrlResolutionResponseBody import com.processout.sdk.api.network.HeaderConstants.CLIENT_SECRET import retrofit2.Response import retrofit2.http.* @@ -55,4 +57,9 @@ internal interface InvoicesApi { suspend fun createInvoice( @Body request: POCreateInvoiceRequest ): Response + + @POST("/apm-payments") + suspend fun resolveUrl( + @Body request: PONativeAlternativePaymentUrlResolutionRequest + ): Response } 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 6e09402a0..753fbe976 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 @@ -2,6 +2,7 @@ package com.processout.sdk.api.repository import com.processout.sdk.api.model.request.* import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody +import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.Configuration import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.SubmitData import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Value @@ -80,6 +81,7 @@ internal class DefaultCustomerTokensRepository( private fun PONativeAlternativePaymentTokenizationRequest.toBody() = NativeAlternativePaymentRequestBody( gatewayConfigurationId = gatewayConfigurationId, + configuration = Configuration(returnRedirectType = configuration.returnRedirectType.rawValue), source = null, submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) }, redirectConfirmation = redirectConfirmation 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 ad8864ee8..30e86d1f7 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 @@ -2,15 +2,14 @@ package com.processout.sdk.api.repository import com.processout.sdk.api.model.request.* import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody +import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.Configuration import com.processout.sdk.api.model.request.napm.v2.NativeAlternativePaymentRequestBody.SubmitData import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentSubmitData.Parameter.Value +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.* -import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentAuthorizationResponseBody -import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentElement -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.* import com.processout.sdk.api.network.HeaderConstants.CLIENT_SECRET import com.processout.sdk.api.network.InvoicesApi import com.processout.sdk.core.* @@ -127,6 +126,11 @@ internal class DefaultInvoicesRepository( override suspend fun createInvoice(request: POCreateInvoiceRequest) = plainApiCall { api.createInvoice(request) }.map() + override suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ) = apiCall { api.resolveUrl(request) } + .map { it.toModel() } + private fun POInvoiceAuthorizationRequest.withDeviceData() = InvoiceAuthorizationRequestWithDeviceData( source = source, @@ -152,6 +156,7 @@ internal class DefaultInvoicesRepository( private fun PONativeAlternativePaymentAuthorizationRequest.toBody() = NativeAlternativePaymentRequestBody( gatewayConfigurationId = gatewayConfigurationId, + configuration = Configuration(returnRedirectType = configuration.returnRedirectType.rawValue), source = source, submitData = submitData?.let { SubmitData(parameters = it.parameters.map()) }, redirectConfirmation = redirectConfirmation @@ -170,28 +175,39 @@ internal class DefaultInvoicesRepository( state = state, invoice = invoice, paymentMethod = paymentMethod, - elements = elements?.map { - when (it) { - is NativeAlternativePaymentElement.Form -> - PONativeAlternativePaymentElement.Form( - parameterDefinitions = it.parameters.parameterDefinitions - ) - is NativeAlternativePaymentElement.CustomerInstruction -> - PONativeAlternativePaymentElement.CustomerInstruction( - instruction = it.instruction - ) - is NativeAlternativePaymentElement.CustomerInstructionGroup -> - PONativeAlternativePaymentElement.CustomerInstructionGroup( - label = it.label, - instructions = it.instructions - ) - NativeAlternativePaymentElement.Unknown -> - PONativeAlternativePaymentElement.Unknown - } - }, + elements = elements?.map { it.toModel() }, redirect = redirect ) + private fun NativeAlternativePaymentUrlResolutionResponseBody.toModel() = + PONativeAlternativePaymentUrlResolutionResponse( + state = state, + paymentMethod = paymentMethod, + invoice = invoice, + customerToken = customerToken, + elements = elements?.map { it.toModel() }, + redirect = redirect + ) + + private fun NativeAlternativePaymentElement.toModel() = + when (this) { + is NativeAlternativePaymentElement.Form -> + PONativeAlternativePaymentElement.Form( + parameterDefinitions = parameters.parameterDefinitions + ) + is NativeAlternativePaymentElement.CustomerInstruction -> + PONativeAlternativePaymentElement.CustomerInstruction( + instruction = instruction + ) + is NativeAlternativePaymentElement.CustomerInstructionGroup -> + PONativeAlternativePaymentElement.CustomerInstructionGroup( + label = label, + instructions = instructions + ) + NativeAlternativePaymentElement.Unknown -> + PONativeAlternativePaymentElement.Unknown + } + private fun ProcessOutResult>.map() = fold( onSuccess = { response -> response.body()?.let { invoice -> diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt index 64f931cfe..6a9989ca7 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/InvoicesRepository.kt @@ -5,8 +5,10 @@ import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.* import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.core.ProcessOutCallback import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.annotation.ProcessOutInternalApi @@ -62,4 +64,10 @@ internal interface InvoicesRepository { /** @suppress */ @ProcessOutInternalApi suspend fun createInvoice(request: POCreateInvoiceRequest): ProcessOutResult + + /** @suppress */ + @ProcessOutInternalApi + suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ): ProcessOutResult } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt index c7527f619..82b320578 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/DefaultInvoicesService.kt @@ -7,11 +7,13 @@ import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.POInvoice import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethod import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodCapture import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.api.repository.InvoicesRepository import com.processout.sdk.core.* import com.processout.sdk.core.POFailure.Code.Cancelled @@ -244,4 +246,9 @@ internal class DefaultInvoicesService( request: POCreateInvoiceRequest ): ProcessOutResult = repository.createInvoice(request) + + override suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ): ProcessOutResult = + repository.resolveUrl(request) } diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt index 36d01917f..7dafd0432 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/service/POInvoicesService.kt @@ -5,11 +5,13 @@ import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentAuthorizationRequest +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentUrlResolutionRequest import com.processout.sdk.api.model.response.POInvoice import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethod import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodCapture import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentUrlResolutionResponse import com.processout.sdk.core.ProcessOutCallback import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.annotation.ProcessOutInternalApi @@ -158,4 +160,10 @@ interface POInvoicesService { /** @suppress */ @ProcessOutInternalApi suspend fun createInvoice(request: POCreateInvoiceRequest): ProcessOutResult + + /** @suppress */ + @ProcessOutInternalApi + suspend fun resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequest + ): ProcessOutResult } 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 c70ee7bf7..36a9a65d8 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/core/POFailure.kt @@ -147,6 +147,7 @@ class POFailure private constructor() { invalidType("request.validation.invalid-type"), invalidUrl("request.validation.invalid-url"), invalidUser("request.validation.invalid-user"), + invalidRedirectResult("request.validation.redirect-result-invalid"), missingCurrency("request.validation.missing-currency"), missingCustomerInput("gateway.missing-customer-input"), missingDescription("request.validation.missing-description"), @@ -208,6 +209,7 @@ class POFailure private constructor() { enum class GenericCode(val rawValue: String) : Parcelable { mobile("processout-mobile.generic.error"), mobileAppProcessKilled("processout-mobile.generic.app-process-killed"), + mobileOperationNotSupported("processout-mobile.generic.operation-not-supported"), customerCancelled("customer.cancelled"), cardExceededLimits("card.exceeded-limits"), cardFailedCvc("card.failed-cvc"), diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt index c124c87d1..19e142219 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabAuthorizationActivity.kt @@ -14,9 +14,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.processout.sdk.R +import com.processout.sdk.api.dispatcher.POEventDispatcher +import com.processout.sdk.api.model.event.PODeepLinkReceivedEvent import com.processout.sdk.api.service.POBrowserCapabilitiesService.Companion.CHROME_PACKAGE import com.processout.sdk.core.POFailure import com.processout.sdk.core.ProcessOutActivityResult +import com.processout.sdk.core.annotation.ProcessOutInternalApi import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure import com.processout.sdk.ui.web.ActivityResultDispatcher @@ -37,6 +40,17 @@ import kotlinx.coroutines.launch */ class POCustomTabAuthorizationActivity : AppCompatActivity() { + @ProcessOutInternalApi + companion object { + fun redirect(hostActivity: Activity, uri: Uri?) { + val intent = Intent(hostActivity, POCustomTabAuthorizationActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + intent.data = uri + hostActivity.startActivity(intent) + } + } + private val resultDispatcher: ActivityResultDispatcher = WebAuthorizationActivityResultDispatcher private lateinit var configuration: POCustomTabConfiguration @@ -76,6 +90,7 @@ class POCustomTabAuthorizationActivity : AppCompatActivity() { return } + collectDeepLink() lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { viewModel.uiState.collect { handleUiState(it) } @@ -83,6 +98,15 @@ class POCustomTabAuthorizationActivity : AppCompatActivity() { } } + private fun collectDeepLink() { + POEventDispatcher.instance.subscribe( + coroutineScope = lifecycleScope + ) { event -> + // Activity restarts itself with proper intent flags to close the Custom Tab. + redirect(hostActivity = this, event.uri) + } + } + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt index 24001bc95..5c5f4e812 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/POCustomTabRedirectActivity.kt @@ -1,8 +1,8 @@ package com.processout.sdk.ui.web.customtab -import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.processout.sdk.api.ProcessOut /** * Redirect activity that receives deep link and starts [POCustomTabAuthorizationActivity] providing return URL. @@ -12,10 +12,8 @@ class POCustomTabRedirectActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Intent(this, POCustomTabAuthorizationActivity::class.java).let { - it.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - intent.data?.let { uri -> it.data = uri } - startActivity(it) + intent.data?.let { uri -> + ProcessOut.instance.processDeepLink(hostActivity = this, uri) } finish() } 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 07aaf3c10..5bbd8c1f3 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 @@ -4,6 +4,7 @@ package com.processout.sdk.ui.napm import android.Manifest import android.app.Application +import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper @@ -19,12 +20,11 @@ import coil.request.ImageRequest 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.event.PODeepLinkReceivedEvent +import com.processout.sdk.api.model.request.napm.v2.* +import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRequestConfiguration.ReturnRedirectType 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 -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.POImageResource import com.processout.sdk.api.model.response.napm.v2.* @@ -37,6 +37,7 @@ import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentS import com.processout.sdk.api.service.POCustomerTokensService import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure.Code.* +import com.processout.sdk.core.POFailure.GenericCode.mobileOperationNotSupported import com.processout.sdk.core.POFailure.InvalidField import com.processout.sdk.core.POFailure.ValidationCode import com.processout.sdk.core.ProcessOutResult @@ -114,6 +115,7 @@ internal class NativeAlternativePaymentInteractor( dispatch(WillStart) dispatchFailure() collectDefaultValues() + collectDeepLink() fetchPaymentDetails() } @@ -145,22 +147,12 @@ 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, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ), source = flow.customerTokenId ) invoicesService.authorize(request) @@ -181,23 +173,13 @@ 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, - gatewayConfigurationId = flow.gatewayConfigurationId + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ) ) customerTokensService.tokenize(request) .onSuccess { response -> @@ -437,6 +419,7 @@ internal class NativeAlternativePaymentInteractor( enableNextStepSecondaryAction() POLogger.info("Started: waiting for payment parameters.") dispatch(DidStart) + handleHeadlessRedirect() } private fun continueNextStep(stateValue: NextStepStateValue) { @@ -455,6 +438,31 @@ internal class NativeAlternativePaymentInteractor( additionalParametersExpected = true ) ) + handleHeadlessRedirect() + } + + private fun handleHeadlessRedirect() { + if (configuration.redirect?.enableHeadlessMode != true) { + return + } + _state.whenNextStep { stateValue -> + if (stateValue.redirect == null) { + val failure = ProcessOutResult.Failure( + code = Generic(genericCode = mobileOperationNotSupported), + message = "Headless mode is not supported: redirect parameters are missing in the response." + ) + POLogger.error( + message = "Unsupported operation: %s", failure, + attributes = configuration.logAttributes + ) + _completion.update { Failure(failure) } + return@whenNextStep + } + redirect( + stateValue = stateValue, + redirect = stateValue.redirect + ) + } } //endregion @@ -535,6 +543,65 @@ internal class NativeAlternativePaymentInteractor( //endregion + //region Deep Link + + private fun collectDeepLink() { + eventDispatcher.subscribe( + coroutineScope = interactorScope + ) { event -> + if (_completion.value !is Awaiting) { + return@subscribe + } + _state.whenNextStep { stateValue -> + if (stateValue.redirect?.type == RedirectType.DEEP_LINK) { + handleDeepLink(event.uri) + } + } + _state.whenPending { stateValue -> + if (stateValue.redirect?.type == RedirectType.DEEP_LINK) { + handleDeepLink(event.uri) + } + } + } + } + + private fun handleDeepLink(uri: Uri) { + interactorScope.launch { + val request = PONativeAlternativePaymentUrlResolutionRequest( + redirect = PONativeAlternativePaymentUrlResolutionRequest.Redirect( + result = PONativeAlternativePaymentUrlResolutionRequest.Redirect.Result( + url = uri.toString() + ) + ) + ) + invoicesService.resolveUrl(request) + .onSuccess { response -> + if (response.state == PENDING && _state.value is Pending) { + return@onSuccess + } + handlePaymentState( + stateValue = initNextStepStateValue( + paymentMethod = response.paymentMethod, + invoice = response.invoice?.map() + ), + paymentState = response.state, + elements = response.elements, + redirect = response.redirect + ) + }.onFailure { failure -> + _completion.update { Failure(failure) } + } + } + } + + private fun PONativeAlternativePaymentInvoice.map() = Invoice( + id = id, + amount = amount, + currency = currency + ) + + //endregion + fun onEvent(event: NativeAlternativePaymentEvent) { when (event) { is FieldValueChanged -> updateFieldValue(event.id, event.value) @@ -842,6 +909,9 @@ internal class NativeAlternativePaymentInteractor( val request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ), submitData = stateValue.fields.toSubmitData(), redirectConfirmation = redirectConfirmation ) @@ -870,6 +940,9 @@ internal class NativeAlternativePaymentInteractor( customerId = flow.customerId, customerTokenId = flow.customerTokenId, gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ), submitData = stateValue.fields.toSubmitData(), redirectConfirmation = redirectConfirmation ) @@ -986,6 +1059,7 @@ internal class NativeAlternativePaymentInteractor( uuid = uuid, paymentMethod = paymentMethod, invoice = invoice, + redirect = redirect, stepper = null, elements = elements, primaryActionId = ActionId.CONFIRM_PAYMENT, @@ -1014,14 +1088,20 @@ internal class NativeAlternativePaymentInteractor( is Authorization -> invoicesService.authorize( request = PONativeAlternativePaymentAuthorizationRequest( invoiceId = flow.invoiceId, - gatewayConfigurationId = flow.gatewayConfigurationId + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ) ) ).map() is Tokenization -> customerTokensService.tokenize( request = PONativeAlternativePaymentTokenizationRequest( customerId = flow.customerId, customerTokenId = flow.customerTokenId, - gatewayConfigurationId = flow.gatewayConfigurationId + gatewayConfigurationId = flow.gatewayConfigurationId, + configuration = PONativeAlternativePaymentRequestConfiguration( + returnRedirectType = ReturnRedirectType.MANUAL + ) ) ).map() } @@ -1135,7 +1215,9 @@ internal class NativeAlternativePaymentInteractor( POLogger.info("Success: payment completed.") dispatch(DidCompletePayment) val successConfiguration = configuration.success - if (successConfiguration == null) { + if (successConfiguration == null || + configuration.redirect?.enableHeadlessMode == true + ) { _completion.update { Success } } else { _state.update { @@ -1155,6 +1237,9 @@ internal class NativeAlternativePaymentInteractor( //region Images private suspend fun preloadImages(resources: List) { + if (configuration.redirect?.enableHeadlessMode == true) { + return + } coroutineScope { val urls = resources.flatMap { it.urls() } val deferredResults = urls.map { url -> diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt index b857db717..1e147e46e 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt @@ -63,6 +63,7 @@ internal sealed interface NativeAlternativePaymentInteractorState { val uuid: String, val paymentMethod: PONativeAlternativePaymentMethodDetails, val invoice: Invoice?, + val redirect: PONativeAlternativePaymentRedirect?, val stepper: Stepper?, val elements: List?, val primaryActionId: String?, 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 25aceadda..4253ad78f 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,8 +4,6 @@ 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 @@ -69,8 +67,7 @@ data class PONativeAlternativePaymentConfiguration( data class Authorization( val invoiceId: String, val gatewayConfigurationId: String, - val customerTokenId: String? = null, - internal val initialResponse: PONativeAlternativePaymentAuthorizationResponse? = null + val customerTokenId: String? = null ) : Flow() /** @@ -84,8 +81,7 @@ data class PONativeAlternativePaymentConfiguration( data class Tokenization( val customerId: String, val customerTokenId: String, - val gatewayConfigurationId: String, - internal val initialResponse: PONativeAlternativePaymentTokenizationResponse? = null + val gatewayConfigurationId: String ) : Flow() } 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 9e5d4aca0..558e58190 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,84 +1,55 @@ package com.processout.sdk.ui.napm import android.app.Application +import android.content.Context import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.core.app.ActivityOptionsCompat import androidx.core.net.toUri import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle 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.DeepLinkConfiguration -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 -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.POUnit +import com.processout.sdk.core.ProcessOutActivityResult +import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.core.toActivityResult import com.processout.sdk.ui.apm.POAlternativePaymentMethodCustomTabLauncher +import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Failure +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.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 com.processout.sdk.ui.shared.extension.openDeepLink import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Launcher that starts [NativeAlternativePaymentActivity] and provides the result. */ class PONativeAlternativePaymentLauncher private constructor( - private val hostActivity: ComponentActivity, + private val viewModel: NativeAlternativePaymentViewModel, private val launcher: ActivityResultLauncher, + private val activityOptions: ActivityOptionsCompat, private val delegate: PONativeAlternativePaymentDelegate, 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 val eventDispatcher: POEventDispatcher = POEventDispatcher.instance ) { - private val app: Application = hostActivity.application - private val scope: CoroutineScope = hostActivity.lifecycleScope - - private val activityOptions = ActivityOptionsCompat.makeCustomAnimation( - hostActivity, R.anim.po_slide_in_vertical, 0 - ) - - private val viewModel: NativeAlternativePaymentViewModel by hostActivity.viewModels { - NativeAlternativePaymentViewModel.Factory( - app = app, - configuration = PONativeAlternativePaymentConfiguration( - flow = Authorization( - invoiceId = String(), - gatewayConfigurationId = String() - ), - header = null - ) - ) - } - private lateinit var customTabLauncher: POAlternativePaymentMethodCustomTabLauncher - private object LocalCache { - var configuration: PONativeAlternativePaymentConfiguration? = null - } - companion object { /** * Creates the launcher from Fragment. @@ -88,19 +59,29 @@ class PONativeAlternativePaymentLauncher private constructor( from: Fragment, delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from.requireActivity(), - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - callback - ), - delegate = delegate, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.requireActivity().application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + callback + ), + activityOptions = createActivityOptions(context = from.requireContext()), + delegate = delegate, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + from.viewLifecycleOwnerLiveData.observe(from) { lifecycleOwner -> + collectViewModelState(lifecycleOwner) + dispatchAllEvents(coroutineScope = lifecycleOwner.lifecycleScope) + } + } } /** @@ -114,19 +95,29 @@ class PONativeAlternativePaymentLauncher private constructor( from: Fragment, delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from.requireActivity(), - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.requireActivity().application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + callback + ), + activityOptions = createActivityOptions(context = from.requireContext()), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + from.viewLifecycleOwnerLiveData.observe(from) { lifecycleOwner -> + collectViewModelState(lifecycleOwner) + dispatchAllEvents(coroutineScope = lifecycleOwner.lifecycleScope) + } + } } /** @@ -137,19 +128,29 @@ class PONativeAlternativePaymentLauncher private constructor( fun create( from: Fragment, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from.requireActivity(), - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.requireActivity().application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + callback + ), + activityOptions = createActivityOptions(context = from.requireContext()), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + from.viewLifecycleOwnerLiveData.observe(from) { lifecycleOwner -> + collectViewModelState(lifecycleOwner) + dispatchAllEvents(coroutineScope = lifecycleOwner.lifecycleScope) + } + } } /** @@ -160,20 +161,28 @@ class PONativeAlternativePaymentLauncher private constructor( from: ComponentActivity, delegate: PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from, - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - from.activityResultRegistry, - callback - ), - delegate = delegate, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(context = from), + delegate = delegate, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + collectViewModelState(lifecycleOwner = from) + dispatchAllEvents(coroutineScope = from.lifecycleScope) + } } /** @@ -187,20 +196,28 @@ class PONativeAlternativePaymentLauncher private constructor( from: ComponentActivity, delegate: com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentDelegate, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from, - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - from.activityResultRegistry, - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(context = from), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + collectViewModelState(lifecycleOwner = from) + dispatchAllEvents(coroutineScope = from.lifecycleScope) + } } /** @@ -211,38 +228,60 @@ class PONativeAlternativePaymentLauncher private constructor( fun create( from: ComponentActivity, callback: (ProcessOutActivityResult) -> Unit - ) = PONativeAlternativePaymentLauncher( - hostActivity = from, - launcher = from.registerForActivityResult( - NativeAlternativePaymentActivityContract(), - from.activityResultRegistry, - callback - ), - delegate = object : PONativeAlternativePaymentDelegate {}, - callback = callback - ).apply { - customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( - from = from, - callback = ::handleWebRedirect - ) + ): PONativeAlternativePaymentLauncher { + val viewModel: NativeAlternativePaymentViewModel by from.viewModels { + createViewModelFactory(app = from.application) + } + return PONativeAlternativePaymentLauncher( + viewModel = viewModel, + launcher = from.registerForActivityResult( + NativeAlternativePaymentActivityContract(), + from.activityResultRegistry, + callback + ), + activityOptions = createActivityOptions(context = from), + delegate = object : PONativeAlternativePaymentDelegate {}, + callback = callback + ).apply { + customTabLauncher = POAlternativePaymentMethodCustomTabLauncher.create( + from = from, + callback = ::handleWebRedirect + ) + collectViewModelState(lifecycleOwner = from) + dispatchAllEvents(coroutineScope = from.lifecycleScope) + } } + + private fun createViewModelFactory(app: Application) = + NativeAlternativePaymentViewModel.Factory( + app = app, + configuration = PONativeAlternativePaymentConfiguration( + flow = Authorization( + invoiceId = String(), + gatewayConfigurationId = String() + ), + header = null + ) + ) + + private fun createActivityOptions(context: Context) = + ActivityOptionsCompat.makeCustomAnimation( + context, R.anim.po_slide_in_vertical, 0 + ) } - init { - collectViewModelCompletion() - dispatchEvents() - dispatchDefaultValues() + private fun collectViewModelState(lifecycleOwner: LifecycleOwner) { + collectCompletion(lifecycleOwner) + collectSideEffects(lifecycleOwner) } - private fun collectViewModelCompletion() { - hostActivity.lifecycleScope.launch { - hostActivity.repeatOnLifecycle(Lifecycle.State.STARTED) { + private fun collectCompletion(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.completion.collect { completion -> when (completion) { - NativeAlternativePaymentCompletion.Success -> - completeHeadlessMode(result = ProcessOutResult.Success(value = POUnit)) - is NativeAlternativePaymentCompletion.Failure -> - completeHeadlessMode(result = completion.failure) + Success -> complete(result = ProcessOutResult.Success(POUnit)) + is Failure -> complete(result = completion.failure) else -> {} } } @@ -250,17 +289,34 @@ class PONativeAlternativePaymentLauncher private constructor( } } - private fun dispatchEvents() { + private fun collectSideEffects(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + withContext(Dispatchers.Main.immediate) { + viewModel.sideEffects.collect { + handle(sideEffect = it) + } + } + } + } + } + + private fun dispatchAllEvents(coroutineScope: CoroutineScope) { + dispatchEvents(coroutineScope) + dispatchDefaultValues(coroutineScope) + } + + private fun dispatchEvents(coroutineScope: CoroutineScope) { eventDispatcher.subscribe( - coroutineScope = scope + coroutineScope ) { delegate.onEvent(it) } } - private fun dispatchDefaultValues() { + private fun dispatchDefaultValues(coroutineScope: CoroutineScope) { eventDispatcher.subscribeForRequest( - coroutineScope = scope + coroutineScope ) { request -> - scope.launch { + coroutineScope.launch { val defaultValues = delegate.defaultValues( gatewayConfigurationId = request.gatewayConfigurationId, parameters = request.parameters @@ -271,240 +327,34 @@ class PONativeAlternativePaymentLauncher private constructor( } /** - * Launches the activity. + * Launches the payment. */ fun launch(configuration: PONativeAlternativePaymentConfiguration) { if (configuration.redirect?.enableHeadlessMode == true) { - launchHeadlessMode(configuration) + POLogger.info("Starting native alternative payment in headless mode.") + viewModel.start(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.") - 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, redirectConfirmation) - is Tokenization -> tokenize(flow, configuration, redirectConfirmation) - } - } - } - - private suspend fun authorize( - flow: Authorization, - configuration: PONativeAlternativePaymentConfiguration, - redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null - ) { - val request = PONativeAlternativePaymentAuthorizationRequest( - invoiceId = flow.invoiceId, - gatewayConfigurationId = flow.gatewayConfigurationId, - source = flow.customerTokenId, - redirectConfirmation = redirectConfirmation - ) - invoicesService.authorize(request) - .onSuccess { response -> - val updatedConfiguration = configuration.copy( - flow = flow.copy(initialResponse = response) - ) - LocalCache.configuration = 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, - redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null - ) { - val request = PONativeAlternativePaymentTokenizationRequest( - customerId = flow.customerId, - customerTokenId = flow.customerTokenId, - gatewayConfigurationId = flow.gatewayConfigurationId, - redirectConfirmation = redirectConfirmation - ) - customerTokensService.tokenize(request) - .onSuccess { response -> - val updatedConfiguration = configuration.copy( - flow = flow.copy(initialResponse = response) - ) - LocalCache.configuration = 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 -> viewModel.start(configuration) - SUCCESS -> { - 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 - } - when (redirect.type) { - RedirectType.WEB -> webRedirect( - redirectUrl = redirect.url, - configuration = configuration - ) - RedirectType.DEEP_LINK -> deepLinkRedirect( - redirectUrl = redirect.url, - deepLinkConfiguration = redirect.deepLinkConfiguration, - configuration = configuration + launcher.launch( + input = configuration, + options = activityOptions ) - 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 web redirect flow." + private fun handle(sideEffect: NativeAlternativePaymentSideEffect) { + if (sideEffect is WebRedirect) { + customTabLauncher.launch( + uri = sideEffect.redirectUrl.toUri(), + returnUrl = sideEffect.returnUrl ) - POLogger.warn( - message = "Failed headless web redirect: %s", failure, - attributes = configuration.logAttributes - ) - completeHeadlessMode(result = failure) - return } - customTabLauncher.launch( - uri = redirectUrl.toUri(), - returnUrl = returnUrl - ) } 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 web redirect result." - ) - POLogger.error(message = "Failed headless web redirect: %s", failure) - completeHeadlessMode(result = failure) - return - } - 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 web redirect: %s", failure, - attributes = configuration?.logAttributes - ) - completeHeadlessMode(result = failure) - } + viewModel.onEvent(WebRedirectResult(result)) } - private fun deepLinkRedirect( - redirectUrl: String, - deepLinkConfiguration: DeepLinkConfiguration?, - configuration: PONativeAlternativePaymentConfiguration - ) { - val didOpenUrl = app.openDeepLink( - url = redirectUrl, - packageNames = deepLinkConfiguration?.packageNames - ) - 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 = LocalCache.configuration?.flow) { - is Authorization -> flow.initialResponse?.state ?: UNKNOWN - is Tokenization -> flow.initialResponse?.state ?: UNKNOWN - null -> UNKNOWN - } - ) - ) - } - } - LocalCache.configuration = null + private fun complete(result: ProcessOutResult) { viewModel.reset() callback(result.toActivityResult()) }