From c1d0f677ffd40409fe2a58f086c718ca24cba82f Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Wed, 29 May 2024 11:47:06 +0200 Subject: [PATCH] Correctly restore state of redirect COAND-857 --- .../provider/RedirectComponentProvider.kt | 1 + .../internal/ui/DefaultRedirectDelegate.kt | 61 +++++++++++++++---- .../ui/DefaultRedirectDelegateTest.kt | 43 ++++++++++++- 3 files changed, 92 insertions(+), 13 deletions(-) diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt b/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt index 8b3960c0ac..59e000d2f5 100644 --- a/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt +++ b/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt @@ -91,6 +91,7 @@ constructor( return DefaultRedirectDelegate( observerRepository = ActionObserverRepository(), + savedStateHandle = savedStateHandle, componentParams = componentParams, redirectHandler = redirectHandler, paymentDataRepository = paymentDataRepository, diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt b/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt index ffff2f8ca8..375cd2a270 100644 --- a/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt +++ b/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt @@ -10,7 +10,9 @@ package com.adyen.checkout.redirect.internal.ui import android.app.Activity import android.content.Intent +import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.ActionTypes @@ -18,6 +20,8 @@ import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.PaymentDataRepository +import com.adyen.checkout.components.core.internal.SavedStateHandleContainer +import com.adyen.checkout.components.core.internal.SavedStateHandleProperty import com.adyen.checkout.components.core.internal.analytics.AnalyticsManager import com.adyen.checkout.components.core.internal.analytics.GenericEvents import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams @@ -42,14 +46,17 @@ import kotlinx.coroutines.launch import org.json.JSONObject @Suppress("TooManyFunctions") -internal class DefaultRedirectDelegate( +internal class DefaultRedirectDelegate +@Suppress("LongParameterList") +constructor( private val observerRepository: ActionObserverRepository, + override val savedStateHandle: SavedStateHandle, override val componentParams: GenericComponentParams, private val redirectHandler: RedirectHandler, private val paymentDataRepository: PaymentDataRepository, private val nativeRedirectService: NativeRedirectService, private val analyticsManager: AnalyticsManager?, -) : RedirectDelegate { +) : RedirectDelegate, SavedStateHandleContainer { private val detailsChannel: Channel = bufferedChannel() override val detailsFlow: Flow = detailsChannel.receiveAsFlow() @@ -62,8 +69,16 @@ internal class DefaultRedirectDelegate( private var _coroutineScope: CoroutineScope? = null private val coroutineScope: CoroutineScope get() = requireNotNull(_coroutineScope) + private var action: RedirectAction? by SavedStateHandleProperty(ACTION_KEY) + override fun initialize(coroutineScope: CoroutineScope) { _coroutineScope = coroutineScope + restoreState() + } + + private fun restoreState() { + adyenLog(AdyenLogLevel.DEBUG) { "Restoring state" } + action?.let { initState(it) } } override fun observe( @@ -87,16 +102,23 @@ internal class DefaultRedirectDelegate( override fun handleAction(action: Action, activity: Activity) { if (action !is RedirectAction) { - exceptionChannel.trySend(ComponentException("Unsupported action")) + emitError(ComponentException("Unsupported action")) return } + this.action = action + val event = GenericEvents.action( component = action.paymentMethodType.orEmpty(), subType = action.type.orEmpty(), ) analyticsManager?.trackEvent(event) + initState(action) + launchAction(activity, action.url) + } + + private fun initState(action: RedirectAction) { when (action.type) { ActionTypes.NATIVE_REDIRECT -> { paymentDataRepository.nativeRedirectData = action.nativeRedirectData @@ -106,8 +128,6 @@ internal class DefaultRedirectDelegate( paymentDataRepository.paymentData = action.paymentData } } - - launchAction(activity, action.url) } private fun launchAction(activity: Activity, url: String?) { @@ -118,7 +138,7 @@ internal class DefaultRedirectDelegate( // PaymentComponentState for actions. redirectHandler.launchUriRedirect(activity, url) } catch (ex: CheckoutException) { - exceptionChannel.trySend(ex) + emitError(ex) } } @@ -132,11 +152,11 @@ internal class DefaultRedirectDelegate( } else -> { - detailsChannel.trySend(createActionComponentData(details)) + emitDetails(details) } } } catch (ex: CheckoutException) { - exceptionChannel.trySend(ex) + emitError(ex) } } @@ -156,23 +176,37 @@ internal class DefaultRedirectDelegate( try { val response = nativeRedirectService.makeNativeRedirect(request, componentParams.clientKey) val detailsJson = NativeRedirectResponse.SERIALIZER.serialize(response) - detailsChannel.trySend(createActionComponentData(detailsJson)) + emitDetails(detailsJson) } catch (e: HttpException) { - onError(e) + emitError(e) } catch (e: ModelSerializationException) { - onError(e) + emitError(e) } } } override fun onError(e: CheckoutException) { - exceptionChannel.trySend(e) + emitError(e) } override fun setOnRedirectListener(listener: () -> Unit) { redirectHandler.setOnRedirectListener(listener) } + private fun emitError(e: CheckoutException) { + exceptionChannel.trySend(e) + clearState() + } + + private fun emitDetails(details: JSONObject) { + detailsChannel.trySend(createActionComponentData(details)) + clearState() + } + + private fun clearState() { + action = null + } + override fun onCleared() { removeObserver() redirectHandler.removeOnRedirectListener() @@ -181,5 +215,8 @@ internal class DefaultRedirectDelegate( companion object { private const val RETURN_URL_QUERY_STRING_PARAMETER = "returnUrlQueryString" + + @VisibleForTesting + internal const val ACTION_KEY = "ACTION_KEY" } } diff --git a/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt b/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt index 8ed15a008e..eeddf1fe54 100644 --- a/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt +++ b/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt @@ -201,8 +201,48 @@ internal class DefaultRedirectDelegateTest( } } + @Test + fun `when initializing and action is set, then state is restored`() = runTest { + val savedStateHandle = SavedStateHandle().apply { + set( + DefaultRedirectDelegate.ACTION_KEY, + RedirectAction(paymentMethodType = "test", paymentData = "paymentData"), + ) + } + delegate = createDelegate(savedStateHandle = savedStateHandle) + + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + + assertEquals("paymentData", paymentDataRepository.paymentData) + } + + @Test + fun `when details are emitted, then state is cleared`() = runTest { + val savedStateHandle = SavedStateHandle() + delegate = createDelegate(savedStateHandle = savedStateHandle) + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + delegate.handleAction(RedirectAction(paymentMethodType = "test", paymentData = "paymentData"), Activity()) + + delegate.handleIntent(Intent()) + + assertNull(savedStateHandle[DefaultRedirectDelegate.ACTION_KEY]) + } + + @Test + fun `when an error is emitted, then state is cleared`() = runTest { + val savedStateHandle = SavedStateHandle() + delegate = createDelegate(savedStateHandle = savedStateHandle) + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + redirectHandler.exception = ComponentException("Test") + + delegate.handleAction(RedirectAction(paymentMethodType = "test", paymentData = "paymentData"), Activity()) + + assertNull(savedStateHandle[DefaultRedirectDelegate.ACTION_KEY]) + } + private fun createDelegate( - observerRepository: ActionObserverRepository = ActionObserverRepository() + observerRepository: ActionObserverRepository = ActionObserverRepository(), + savedStateHandle: SavedStateHandle = SavedStateHandle(), ): DefaultRedirectDelegate { val configuration = CheckoutConfiguration( Environment.TEST, @@ -212,6 +252,7 @@ internal class DefaultRedirectDelegateTest( } return DefaultRedirectDelegate( observerRepository = observerRepository, + savedStateHandle = savedStateHandle, componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( configuration, Locale.US,