Skip to content

Commit

Permalink
Add PayToDelegate and PayToComponent
Browse files Browse the repository at this point in the history
COAND-1058
  • Loading branch information
araratthehero committed Feb 4, 2025
1 parent 191bc0c commit c281351
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@ internal fun getComponentFor(
)
}

// TODO Add PayTo here when component provider is created

checkCompileOnly { SepaComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> {
SepaComponentProvider(dropInOverrideParams, analyticsManager).get(
fragment = fragment,
Expand Down
21 changes: 21 additions & 0 deletions payto/api/payto.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ public final class com/adyen/checkout/payto/BuildConfig {
public fun <init> ()V
}

public final class com/adyen/checkout/payto/PayToComponent : androidx/lifecycle/ViewModel, com/adyen/checkout/action/core/internal/ActionHandlingComponent, com/adyen/checkout/components/core/internal/ButtonComponent, com/adyen/checkout/components/core/internal/PaymentComponent, com/adyen/checkout/ui/core/internal/ui/ViewableComponent {
public static final field Companion Lcom/adyen/checkout/payto/PayToComponent$Companion;
public static final field PAYMENT_METHOD_TYPES Ljava/util/List;
public static final field PROVIDER Lcom/adyen/checkout/payto/internal/provider/PayToComponentProvider;
public fun canHandleAction (Lcom/adyen/checkout/components/core/action/Action;)Z
public fun getDelegate ()Lcom/adyen/checkout/components/core/internal/ui/ComponentDelegate;
public fun getViewFlow ()Lkotlinx/coroutines/flow/Flow;
public fun handleAction (Lcom/adyen/checkout/components/core/action/Action;Landroid/app/Activity;)V
public fun handleIntent (Landroid/content/Intent;)V
public fun isConfirmationRequired ()Z
public fun setInteractionBlocked (Z)V
public fun setOnRedirectListener (Lkotlin/jvm/functions/Function0;)V
public fun submit ()V
}

public final class com/adyen/checkout/payto/PayToComponent$Companion {
}

public final class com/adyen/checkout/payto/PayToComponentState : com/adyen/checkout/components/core/PaymentComponentState {
public fun <init> (Lcom/adyen/checkout/components/core/PaymentComponentData;ZZ)V
public final fun component1 ()Lcom/adyen/checkout/components/core/PaymentComponentData;
Expand All @@ -22,3 +40,6 @@ public final class com/adyen/checkout/payto/PayToComponentState : com/adyen/chec
public fun toString ()Ljava/lang/String;
}

public final class com/adyen/checkout/payto/internal/provider/PayToComponentProvider {
}

1 change: 1 addition & 0 deletions payto/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ dependencies {
testImplementation testFixtures(project(':ui-core'))
testImplementation libs.bundles.junit
testImplementation libs.bundles.kotlin.coroutines.test
testImplementation libs.bundles.mockito
}
103 changes: 103 additions & 0 deletions payto/src/main/java/com/adyen/checkout/payto/PayToComponent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) 2025 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ararat on 3/2/2025.
*/

package com.adyen.checkout.payto

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.adyen.checkout.action.core.internal.ActionHandlingComponent
import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.PaymentMethodTypes
import com.adyen.checkout.components.core.internal.ButtonComponent
import com.adyen.checkout.components.core.internal.ComponentEventHandler
import com.adyen.checkout.components.core.internal.PaymentComponent
import com.adyen.checkout.components.core.internal.PaymentComponentEvent
import com.adyen.checkout.components.core.internal.toActionCallback
import com.adyen.checkout.components.core.internal.ui.ComponentDelegate
import com.adyen.checkout.core.AdyenLogLevel
import com.adyen.checkout.core.internal.util.adyenLog
import com.adyen.checkout.payto.internal.provider.PayToComponentProvider
import com.adyen.checkout.payto.internal.ui.PayToDelegate
import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate
import com.adyen.checkout.ui.core.internal.ui.ComponentViewType
import com.adyen.checkout.ui.core.internal.ui.ViewableComponent
import com.adyen.checkout.ui.core.internal.util.mergeViewFlows
import kotlinx.coroutines.flow.Flow

/**
* A [PaymentComponent] that supports the [PaymentMethodTypes.PAY_TO] payment method.
*/
class PayToComponent internal constructor(
private val payToDelegate: PayToDelegate,
private val genericActionDelegate: GenericActionDelegate,
private val actionHandlingComponent: DefaultActionHandlingComponent,
internal val componentEventHandler: ComponentEventHandler<PayToComponentState>,
) : ViewModel(),
PaymentComponent,
ViewableComponent,
ButtonComponent,
ActionHandlingComponent by actionHandlingComponent {

override val delegate: ComponentDelegate get() = actionHandlingComponent.activeDelegate

override val viewFlow: Flow<ComponentViewType?> = mergeViewFlows(
viewModelScope,
payToDelegate.viewFlow,
genericActionDelegate.viewFlow,
)

init {
payToDelegate.initialize(viewModelScope)
genericActionDelegate.initialize(viewModelScope)
componentEventHandler.initialize(viewModelScope)
}

internal fun observe(
lifecycleOwner: LifecycleOwner,
callback: (PaymentComponentEvent<PayToComponentState>) -> Unit
) {
payToDelegate.observe(lifecycleOwner, viewModelScope, callback)
genericActionDelegate.observe(lifecycleOwner, viewModelScope, callback.toActionCallback())
}

internal fun removeObserver() {
payToDelegate.removeObserver()
genericActionDelegate.removeObserver()
}

override fun isConfirmationRequired(): Boolean = payToDelegate.isConfirmationRequired()

override fun submit() {
(delegate as? ButtonDelegate)?.onSubmit()
?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." }
}

override fun setInteractionBlocked(isInteractionBlocked: Boolean) {
(delegate as? PayToDelegate)?.setInteractionBlocked(isInteractionBlocked)
?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." }
}

override fun onCleared() {
super.onCleared()
adyenLog(AdyenLogLevel.DEBUG) { "onCleared" }
payToDelegate.onCleared()
genericActionDelegate.onCleared()
componentEventHandler.onCleared()
}

companion object {

@JvmField
val PROVIDER = PayToComponentProvider()

@JvmField
val PAYMENT_METHOD_TYPES = listOf(PaymentMethodTypes.PAY_TO)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ararat on 3/2/2025.
*/

package com.adyen.checkout.payto.internal.provider

import androidx.annotation.RestrictTo

// TODO To be implemented
class PayToComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
constructor()
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ararat on 3/2/2025.
*/

package com.adyen.checkout.payto.internal.ui

import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate
import com.adyen.checkout.payto.PayToComponentState
import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate
import com.adyen.checkout.ui.core.internal.ui.UIStateDelegate
import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate

internal interface PayToDelegate :
PaymentComponentDelegate<PayToComponentState>,
ViewProvidingDelegate,
ButtonDelegate,
UIStateDelegate {

fun setInteractionBlocked(isInteractionBlocked: Boolean)
}
159 changes: 159 additions & 0 deletions payto/src/test/java/com/adyen/checkout/payto/PayToComponentTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright (c) 2025 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ararat on 3/2/2025.
*/

package com.adyen.checkout.payto

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.internal.ComponentEventHandler
import com.adyen.checkout.components.core.internal.PaymentComponentEvent
import com.adyen.checkout.payto.internal.ui.PayToComponentViewType
import com.adyen.checkout.payto.internal.ui.PayToDelegate
import com.adyen.checkout.test.LoggingExtension
import com.adyen.checkout.test.TestDispatcherExtension
import com.adyen.checkout.test.extensions.invokeOnCleared
import com.adyen.checkout.ui.core.internal.ui.TestComponentViewType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class)
internal class PayToComponentTest(
@Mock private val payToDelegate: PayToDelegate,
@Mock private val genericActionDelegate: GenericActionDelegate,
@Mock private val actionHandlingComponent: DefaultActionHandlingComponent,
@Mock private val componentEventHandler: ComponentEventHandler<PayToComponentState>,
) {

private lateinit var component: PayToComponent

@BeforeEach
fun before() {
whenever(payToDelegate.viewFlow) doReturn MutableStateFlow(PayToComponentViewType)
whenever(genericActionDelegate.viewFlow) doReturn MutableStateFlow(null)

component = PayToComponent(
payToDelegate,
genericActionDelegate,
actionHandlingComponent,
componentEventHandler,
)
}

@Test
fun `when component is created, then delegates are initialized`() {
verify(payToDelegate).initialize(component.viewModelScope)
verify(genericActionDelegate).initialize(component.viewModelScope)
verify(componentEventHandler).initialize(component.viewModelScope)
}

@Test
fun `when component is cleared, then delegates are cleared`() {
component.invokeOnCleared()

verify(payToDelegate).onCleared()
verify(genericActionDelegate).onCleared()
verify(componentEventHandler).onCleared()
}

@Test
fun `when observe is called, then observe in delegates is called`() {
val lifecycleOwner = mock<LifecycleOwner>()
val callback: (PaymentComponentEvent<PayToComponentState>) -> Unit = {}

component.observe(lifecycleOwner, callback)

verify(payToDelegate).observe(lifecycleOwner, component.viewModelScope, callback)
verify(genericActionDelegate).observe(eq(lifecycleOwner), eq(component.viewModelScope), any())
}

@Test
fun `when removeObserver is called, then removeObserver in delegates is called`() {
component.removeObserver()

verify(payToDelegate).removeObserver()
verify(genericActionDelegate).removeObserver()
}

@Test
fun `when component is initialized, then view flow should match delegate view flow`() = runTest {
component.viewFlow.test {
assertEquals(PayToComponentViewType, awaitItem())
expectNoEvents()
}
}

@Test
fun `when delegate view flow emits a value, then component view flow should match that value`() = runTest {
val payToDelegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1)
whenever(payToDelegate.viewFlow) doReturn payToDelegateViewFlow
component = PayToComponent(payToDelegate, genericActionDelegate, actionHandlingComponent, componentEventHandler)

component.viewFlow.test {
assertEquals(TestComponentViewType.VIEW_TYPE_1, awaitItem())

payToDelegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
assertEquals(TestComponentViewType.VIEW_TYPE_2, awaitItem())

expectNoEvents()
}
}

@Test
fun `when action delegate view flow emits a value, then component view flow should match that value`() = runTest {
val actionDelegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1)
whenever(genericActionDelegate.viewFlow) doReturn actionDelegateViewFlow
component = PayToComponent(payToDelegate, genericActionDelegate, actionHandlingComponent, componentEventHandler)

component.viewFlow.test {
// this value should match the value of the main delegate and not the action delegate
// and in practice the initial value of the action delegate view flow is always null so it should be ignored
assertEquals(PayToComponentViewType, awaitItem())

actionDelegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
assertEquals(TestComponentViewType.VIEW_TYPE_2, awaitItem())

expectNoEvents()
}
}

@Test
fun `when isConfirmationRequired, then delegate is called`() {
component.isConfirmationRequired()
verify(payToDelegate).isConfirmationRequired()
}

@Test
fun `when submit is called and active delegate is the payment delegate, then delegate onSubmit is called`() {
whenever(component.delegate).thenReturn(payToDelegate)
component.submit()
verify(payToDelegate).onSubmit()
}

@Test
fun `when submit is called and active delegate is the action delegate, then delegate onSubmit is not called`() {
whenever(component.delegate).thenReturn(genericActionDelegate)
component.submit()
verify(payToDelegate, never()).onSubmit()
}
}

0 comments on commit c281351

Please sign in to comment.