Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly restore state of redirect #1639

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ constructor(

return DefaultRedirectDelegate(
observerRepository = ActionObserverRepository(),
savedStateHandle = savedStateHandle,
componentParams = componentParams,
redirectHandler = redirectHandler,
paymentDataRepository = paymentDataRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ 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
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
Expand All @@ -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<ActionComponentData> = bufferedChannel()
override val detailsFlow: Flow<ActionComponentData> = detailsChannel.receiveAsFlow()
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -106,8 +128,6 @@ internal class DefaultRedirectDelegate(
paymentDataRepository.paymentData = action.paymentData
}
}

launchAction(activity, action.url)
}

private fun launchAction(activity: Activity, url: String?) {
Expand All @@ -118,7 +138,7 @@ internal class DefaultRedirectDelegate(
// PaymentComponentState for actions.
redirectHandler.launchUriRedirect(activity, url)
} catch (ex: CheckoutException) {
exceptionChannel.trySend(ex)
emitError(ex)
}
}

Expand All @@ -132,11 +152,11 @@ internal class DefaultRedirectDelegate(
}

else -> {
detailsChannel.trySend(createActionComponentData(details))
emitDetails(details)
}
}
} catch (ex: CheckoutException) {
exceptionChannel.trySend(ex)
emitError(ex)
}
}

Expand All @@ -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()
Expand All @@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -212,6 +252,7 @@ internal class DefaultRedirectDelegateTest(
}
return DefaultRedirectDelegate(
observerRepository = observerRepository,
savedStateHandle = savedStateHandle,
componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams(
configuration,
Locale.US,
Expand Down