Skip to content

Commit

Permalink
Merge pull request #1615 from Adyen/feature/upi_intent_integration
Browse files Browse the repository at this point in the history
UPI intent integration
  • Loading branch information
OscarSpruit authored Jun 19, 2024
2 parents d97d4cc + d6b39c7 commit eb696b4
Show file tree
Hide file tree
Showing 108 changed files with 2,288 additions and 153 deletions.
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
[//]: # (## Deprecated)
[//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object)

## Added
- UPI now supports `upi_intent` payment apps.

## Changed
- Drop-in navigation improvements:
- Top navigation has been added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ internal class DefaultACHDirectDebitDelegate(

override fun shouldShowSubmitButton(): Boolean = isConfirmationRequired() && componentParams.isSubmitButtonVisible

override fun shouldEnableSubmitButton(): Boolean = true

companion object {
private const val ENCRYPTION_KEY_FOR_BANK_ACCOUNT_NUMBER = "bankAccountNumber"
private const val ENCRYPTION_KEY_FOR_BANK_LOCATION_ID = "bankLocationId"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,15 @@ internal class DefaultACHDirectDebitDelegateTest(
}
}

@Nested
inner class SubmitButtonEnableTest {

@Test
fun `when shouldEnableSubmitButton is called, then true is returned`() {
assertTrue(delegate.shouldEnableSubmitButton())
}
}

@Nested
inner class SubmitHandlerTest {

Expand Down
3 changes: 2 additions & 1 deletion await/api/await.api
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
public final class com/adyen/checkout/await/AwaitComponent : androidx/lifecycle/ViewModel, com/adyen/checkout/components/core/internal/ActionComponent, com/adyen/checkout/ui/core/internal/ui/ViewableComponent {
public final class com/adyen/checkout/await/AwaitComponent : androidx/lifecycle/ViewModel, com/adyen/checkout/components/core/RedirectableActionComponent, com/adyen/checkout/components/core/internal/ActionComponent, com/adyen/checkout/ui/core/internal/ui/ViewableComponent {
public static final field Companion Lcom/adyen/checkout/await/AwaitComponent$Companion;
public static final field PROVIDER Lcom/adyen/checkout/components/core/internal/provider/ActionComponentProvider;
public fun canHandleAction (Lcom/adyen/checkout/components/core/action/Action;)Z
public fun getDelegate ()Lcom/adyen/checkout/await/internal/ui/AwaitDelegate;
public synthetic 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 setOnRedirectListener (Lkotlin/jvm/functions/Function0;)V
}

public final class com/adyen/checkout/await/AwaitComponent$Companion {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.adyen.checkout.await.internal.provider.AwaitComponentProvider
import com.adyen.checkout.await.internal.ui.AwaitDelegate
import com.adyen.checkout.components.core.RedirectableActionComponent
import com.adyen.checkout.components.core.action.Action
import com.adyen.checkout.components.core.internal.ActionComponent
import com.adyen.checkout.components.core.internal.ActionComponentEvent
Expand All @@ -32,7 +33,8 @@ class AwaitComponent internal constructor(
internal val actionComponentEventHandler: ActionComponentEventHandler,
) : ViewModel(),
ActionComponent,
ViewableComponent {
ViewableComponent,
RedirectableActionComponent {

override val viewFlow: Flow<ComponentViewType?> get() = delegate.viewFlow

Expand All @@ -56,6 +58,10 @@ class AwaitComponent internal constructor(
delegate.handleAction(action, activity)
}

override fun setOnRedirectListener(listener: () -> Unit) {
delegate.setOnRedirectListener(listener)
}

override fun onCleared() {
super.onCleared()
adyenLog(AdyenLogLevel.DEBUG) { "onCleared" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.adyen.checkout.components.core.internal.util.get
import com.adyen.checkout.components.core.internal.util.viewModelFactory
import com.adyen.checkout.core.internal.data.api.HttpClientFactory
import com.adyen.checkout.core.internal.util.LocaleProvider
import com.adyen.checkout.ui.core.internal.DefaultRedirectHandler

class AwaitComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand Down Expand Up @@ -89,11 +90,13 @@ constructor(
val httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
val statusService = StatusService(httpClient)
val statusRepository = DefaultStatusRepository(statusService, componentParams.clientKey)
val redirectHandler = DefaultRedirectHandler()
val paymentDataRepository = PaymentDataRepository(savedStateHandle)
return DefaultAwaitDelegate(
observerRepository = ActionObserverRepository(),
savedStateHandle = savedStateHandle,
componentParams = componentParams,
redirectHandler = redirectHandler,
statusRepository = statusRepository,
paymentDataRepository = paymentDataRepository,
analyticsManager = analyticsManager,
Expand Down Expand Up @@ -133,6 +136,7 @@ constructor(
PaymentMethodTypes.BLIK,
PaymentMethodTypes.MB_WAY,
PaymentMethodTypes.UPI_COLLECT,
PaymentMethodTypes.UPI_INTENT,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.annotation.RestrictTo
import com.adyen.checkout.await.internal.ui.model.AwaitOutputData
import com.adyen.checkout.components.core.internal.ui.ActionDelegate
import com.adyen.checkout.components.core.internal.ui.DetailsEmittingDelegate
import com.adyen.checkout.components.core.internal.ui.RedirectableDelegate
import com.adyen.checkout.components.core.internal.ui.StatusPollingDelegate
import com.adyen.checkout.components.core.internal.ui.ViewableDelegate
import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate
Expand All @@ -22,4 +23,5 @@ interface AwaitDelegate :
DetailsEmittingDelegate,
ViewableDelegate<AwaitOutputData>,
StatusPollingDelegate,
ViewProvidingDelegate
ViewProvidingDelegate,
RedirectableDelegate
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.adyen.checkout.core.AdyenLogLevel
import com.adyen.checkout.core.exception.CheckoutException
import com.adyen.checkout.core.exception.ComponentException
import com.adyen.checkout.core.internal.util.adyenLog
import com.adyen.checkout.ui.core.internal.RedirectHandler
import com.adyen.checkout.ui.core.internal.ui.ComponentViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
Expand All @@ -48,11 +49,12 @@ import org.json.JSONException
import org.json.JSONObject
import java.util.concurrent.TimeUnit

@Suppress("TooManyFunctions")
@Suppress("LongParameterList", "TooManyFunctions")
internal class DefaultAwaitDelegate(
private val observerRepository: ActionObserverRepository,
override val savedStateHandle: SavedStateHandle,
override val componentParams: GenericComponentParams,
private val redirectHandler: RedirectHandler,
private val statusRepository: StatusRepository,
private val paymentDataRepository: PaymentDataRepository,
private val analyticsManager: AnalyticsManager?,
Expand Down Expand Up @@ -88,10 +90,7 @@ internal class DefaultAwaitDelegate(

private fun restoreState() {
adyenLog(AdyenLogLevel.DEBUG) { "Restoring state" }
val action: AwaitAction? = action
if (action != null) {
initState(action)
}
action?.let { initState(it) }
}

override fun observe(
Expand Down Expand Up @@ -123,27 +122,52 @@ internal class DefaultAwaitDelegate(
}

this.action = action
paymentDataRepository.paymentData = action.paymentData

val event = GenericEvents.action(
component = action.paymentMethodType.orEmpty(),
subType = action.type.orEmpty(),
)
analyticsManager?.trackEvent(event)

launchAction(action, activity)
initState(action)
}

private fun launchAction(action: AwaitAction, activity: Activity) {
if (shouldLaunchRedirect(action)) {
makeRedirect(action, activity)
}
}

private fun shouldLaunchRedirect(action: AwaitAction) = !action.url.isNullOrEmpty()

private fun makeRedirect(action: AwaitAction, activity: Activity) {
val url = action.url
try {
adyenLog(AdyenLogLevel.DEBUG) { "makeRedirect - $url" }
redirectHandler.launchUriRedirect(activity, url)
val paymentData = paymentDataRepository.paymentData
?: throw CheckoutException("Payment data should not be null")
startStatusPolling(paymentData, action)
} catch (exception: CheckoutException) {
emitError(exception)
}
}

private fun initState(action: AwaitAction) {
val paymentData = action.paymentData
paymentDataRepository.paymentData = paymentData
if (paymentData == null) {
adyenLog(AdyenLogLevel.ERROR) { "Payment data is null" }
emitError(ComponentException("Payment data is null"))
return
}
createOutputData(null, action)

startStatusPolling(paymentData, action)
// Redirect flow starts polling after it launched a redirect
if (!shouldLaunchRedirect(action)) {
startStatusPolling(paymentData, action)
}
}

private fun startStatusPolling(paymentData: String, action: Action) {
Expand Down Expand Up @@ -184,7 +208,8 @@ internal class DefaultAwaitDelegate(
// Not authorized status should still call /details so that merchant can get more info
val payload = statusResponse.payload
if (StatusResponseUtils.isFinalResult(statusResponse) && !payload.isNullOrEmpty()) {
emitDetails(payload)
val details = createDetails(payload)
emitDetails(details)
} else {
emitError(ComponentException("Payment was not completed. - " + statusResponse.resultCode))
}
Expand Down Expand Up @@ -212,12 +237,15 @@ internal class DefaultAwaitDelegate(
clearState()
}

private fun emitDetails(payload: String) {
val details = createDetails(payload)
private fun emitDetails(details: JSONObject) {
detailsChannel.trySend(createActionComponentData(details))
clearState()
}

override fun setOnRedirectListener(listener: () -> Unit) {
redirectHandler.setOnRedirectListener(listener)
}

private fun clearState() {
action = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ 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.test.TestComponentViewType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
Expand All @@ -35,7 +34,6 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class)
internal class AwaitComponentTest(
@Mock private val awaitDelegate: AwaitDelegate,
Expand Down Expand Up @@ -111,4 +109,13 @@ internal class AwaitComponentTest(

verify(awaitDelegate).handleAction(action, activity)
}

@Test
fun `when setOnRedirectListener is called then setOnRedirectListener in delegate is called`() {
val listener = { }

component.setOnRedirectListener(listener)

verify(awaitDelegate).setOnRedirectListener(listener)
}
}
Loading

0 comments on commit eb696b4

Please sign in to comment.