TP-43709 | Added transition animation between destinations (#8129)

This commit is contained in:
Ujjwal Kumar
2023-10-04 16:12:10 +05:30
committed by GitHub
parent 153b156509
commit 774fdbfbb1
15 changed files with 143 additions and 94 deletions

View File

@@ -93,7 +93,7 @@ dependencies {
implementation libs.pierfrancescosoffritti.androidyoutubeplayer
implementation libs.raamcosta.composeDestinations.core
implementation libs.raamcosta.composeDestinations.animation.core
implementation libs.zetetic.android.database.sqlcipher

View File

@@ -2,8 +2,10 @@ package com.navi.pay.common.model.view
import androidx.annotation.DrawableRes
data class NaviPayErrorEvent(
val errorConfig: NaviPayErrorConfig, val timestamp: Long = System.currentTimeMillis()
data class NaviPayErrorEventState(
val errorConfig: NaviPayErrorConfig,
val showErrorState: Boolean,
val confirmStateChange: Boolean
)
data class ErrorCtaClickEvent(
@@ -32,18 +34,12 @@ data class NaviPayErrorButtonConfig(
)
sealed interface NaviPayButtonTheme {
object Primary : NaviPayButtonTheme
object Secondary : NaviPayButtonTheme
data object Primary : NaviPayButtonTheme
data object Secondary : NaviPayButtonTheme
}
sealed class NaviPayButtonAction {
object Dismiss : NaviPayButtonAction()
data object Dismiss : NaviPayButtonAction()
data class Redirect(val url : String) : NaviPayButtonAction()
data class Retry(val action : String) : NaviPayButtonAction()
}
sealed interface ErrorVisibilityEvent{
object ShowErrorSheet : ErrorVisibilityEvent
object HideErrorSheet : ErrorVisibilityEvent
object Nothing : ErrorVisibilityEvent
}

View File

@@ -1,27 +1,60 @@
package com.navi.pay.common.utils
import com.navi.common.CommonLibManager
import com.navi.pay.R
import com.navi.pay.common.model.view.ErrorCtaClickEvent
import com.navi.pay.common.model.view.ErrorVisibilityEvent
import com.navi.pay.common.model.view.NaviPayButtonAction
import com.navi.pay.common.model.view.NaviPayButtonTheme
import com.navi.pay.common.model.view.NaviPayErrorButtonConfig
import com.navi.pay.common.model.view.NaviPayErrorConfig
import com.navi.pay.common.model.view.NaviPayErrorEvent
import com.navi.pay.common.model.view.NaviPayErrorEventState
import com.navi.pay.common.viewmodel.NaviPayBaseVM
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.update
object ErrorEventHandler {
suspend fun postErrorEvent(errorConfig: NaviPayErrorConfig) {
NaviPayEventBus.triggerEvent(NaviPayErrorEvent(errorConfig))
postErrorVisibilityEvent(ErrorVisibilityEvent.ShowErrorSheet)
}
suspend fun postErrorVisibilityEvent(errorVisibilityEvent: ErrorVisibilityEvent) {
NaviPayEventBus.triggerEvent(errorVisibilityEvent)
private val _errorEventState = MutableStateFlow(
NaviPayErrorEventState(
errorConfig = NaviPayErrorConfig(
iconResId = R.drawable.ic_error_red_gradient,
title = CommonLibManager.application.resources.getString(R.string.something_went_wrong),
description = CommonLibManager.application.resources.getString(R.string.generic_error_description),
buttonConfigs = listOf(
NaviPayErrorButtonConfig(
text = CommonLibManager.application.resources.getString(R.string.okay),
type = NaviPayButtonTheme.Primary,
action = NaviPayButtonAction.Dismiss
)
),
code = NaviPayBaseVM.UNKNOWN_ERROR_CODE,
tag = NaviPayBaseVM.ERROR_DEFAULT_TAG
),
showErrorState = false,
confirmStateChange = true
)
)
val errorEventState = _errorEventState.asStateFlow()
fun updateErrorEventState(
errorConfig: NaviPayErrorConfig? = null,
showErrorState: Boolean,
confirmStateChange: Boolean? = null
) {
_errorEventState.update {
it.copy(
errorConfig = errorConfig ?: it.errorConfig,
showErrorState = showErrorState,
confirmStateChange = confirmStateChange ?: it.confirmStateChange
)
}
}
suspend fun postErrorClickEvent(errorConfig: NaviPayErrorConfig, naviPayErrorButtonConfig: NaviPayErrorButtonConfig) {
NaviPayEventBus.triggerEvent(ErrorCtaClickEvent(errorConfig, naviPayErrorButtonConfig))
}
val errorVisibilityEvent get() = NaviPayEventBus.events.filterIsInstance<ErrorVisibilityEvent>()
val errorEvent get() = NaviPayEventBus.events.filterIsInstance<NaviPayErrorEvent>()
val errorCtaClickEvent get() = NaviPayEventBus.events.filterIsInstance<ErrorCtaClickEvent>()
}

View File

@@ -14,9 +14,9 @@ import com.navi.pay.common.model.view.NaviPayButtonAction
import com.navi.pay.common.model.view.NaviPayButtonTheme
import com.navi.pay.common.model.view.NaviPayErrorButtonConfig
import com.navi.pay.common.model.view.NaviPayErrorConfig
import com.navi.pay.common.model.view.NaviPayErrorEvent
import com.navi.pay.common.model.view.NaviPayErrorEventState
import com.navi.pay.common.model.view.NaviPayVmData
import com.navi.pay.common.utils.ErrorEventHandler
import com.navi.pay.common.utils.ErrorEventHandler.updateErrorEventState
import com.navi.pay.common.utils.NaviPayEventBus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -32,14 +32,13 @@ abstract class NaviPayBaseVM(private val naviPayVmData: NaviPayVmData) : BaseVM(
private const val CTA_TYPE_RETRY = "retry"
private const val CTA_TYPE_REDIRECT = "redirect"
private const val UNKNOWN_ERROR_CODE = "UNKNOWN"
const val UNKNOWN_ERROR_CODE = "UNKNOWN"
}
private val analyticsErrorEventTracker = CommonNaviAnalytics.naviAnalytics.Errors()
private val analyticsTracker = CommonNaviAnalytics.naviAnalytics.ErrorBottomSheet()
/**
* Prepare error configuration using application specific logic and trigger event [NaviPayErrorEvent]
* Prepare error configuration using application specific logic and trigger event [NaviPayErrorEventState]
* using [NaviPayEventBus].
*
* @param response error response returned by navi pay backend APIs.
@@ -54,19 +53,27 @@ abstract class NaviPayBaseVM(private val naviPayVmData: NaviPayVmData) : BaseVM(
sendFailureEvent(naviPayVmData.screenName, response.errors?.firstOrNull(), response.error)
val errorConfig = getError(response, cancelable).copy(tag = tag, extras = extras)
withContext(Dispatchers.Main) {
ErrorEventHandler.postErrorEvent(errorConfig)
updateErrorEventState(
errorConfig = errorConfig,
showErrorState = true,
confirmStateChange = errorConfig.cancelable
)
}
}
/**
* Notify error based on the error config passed in the parameter and trigger event [NaviPayErrorEvent]
* Notify error based on the error config passed in the parameter and trigger event [NaviPayErrorEventState]
* using [NaviPayEventBus].
*
* @param errorConfig optional error config. By default it is set with generic error config.
*/
open fun notifyError(errorConfig: NaviPayErrorConfig = getGenericErrorConfig()) {
viewModelScope.launch(Dispatchers.Main) {
ErrorEventHandler.postErrorEvent(errorConfig)
updateErrorEventState(
errorConfig = errorConfig,
showErrorState = true,
confirmStateChange = errorConfig.cancelable
)
}
}

View File

@@ -28,7 +28,6 @@ import com.navi.common.model.ModuleNameV2
import com.navi.common.ui.activity.BaseActivity
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.model.view.ErrorVisibilityEvent
import com.navi.pay.common.theme.NaviPayMaterialTheme
import com.navi.pay.common.utils.ErrorEventHandler
import com.navi.pay.common.utils.NaviPayCommonUtils
@@ -189,7 +188,7 @@ class NaviPayActivity : BaseActivity() {
if (isErrorSheetCancellable) {
lifecycleScope.launch {
repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
ErrorEventHandler.postErrorVisibilityEvent(ErrorVisibilityEvent.HideErrorSheet)
ErrorEventHandler.updateErrorEventState(showErrorState = false)
}
}
}

View File

@@ -7,20 +7,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.navi.pay.common.model.view.NaviPayErrorButtonConfig
import com.navi.pay.common.model.view.NaviPayErrorConfig
import com.navi.pay.common.model.view.NaviPayErrorEvent
import com.navi.pay.common.model.view.NaviPayErrorEventState
import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButton
@Composable
fun GenericErrorBottomSheetContent(
errorEvent: NaviPayErrorEvent?,
errorEventState: NaviPayErrorEventState?,
onErrorCtaClick: (NaviPayErrorConfig, NaviPayErrorButtonConfig) -> Unit
) {
if (errorEvent == null) {
if (errorEventState == null) {
Spacer(modifier = Modifier.size(1.dp))
return
}
val errorConfig = errorEvent.errorConfig
val errorConfig = errorEventState.errorConfig
val primaryButtonConfig = errorConfig.firstPrimaryButtonConfig
val secondaryButtonConfig = errorConfig.firstSecondaryButtonConfig
BottomSheetContentWithIconHeaderPrimarySecondaryButton(iconId = errorConfig.iconResId,

View File

@@ -1,30 +1,37 @@
package com.navi.pay.entry.ui
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController
import com.navi.base.utils.orFalse
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
import com.navi.pay.NavGraphs
import com.navi.pay.common.model.view.ErrorVisibilityEvent
import com.navi.pay.common.model.view.NaviPayErrorButtonConfig
import com.navi.pay.common.model.view.NaviPayErrorConfig
import com.navi.pay.common.ui.NaviPayModalBottomSheetLayout
import com.navi.pay.common.utils.ErrorEventHandler
import com.navi.pay.entry.NaviPayActivity
import com.navi.pay.utils.GenericErrorCtaHandler
import com.navi.pay.utils.hideSheet
import com.navi.pay.utils.showSheet
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
import com.ramcosta.composedestinations.navigation.dependency
import com.ramcosta.composedestinations.spec.Route
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@OptIn(
ExperimentalMaterialApi::class, ExperimentalMaterialNavigationApi::class,
ExperimentalAnimationApi::class
)
@Composable
fun NaviPayMainScreen(
naviPayActivity: NaviPayActivity,
@@ -32,56 +39,85 @@ fun NaviPayMainScreen(
onErrorVisibilityChange: (Boolean, Boolean) -> Unit,
customerStatusRoute: Route?
) {
val errorEvent by ErrorEventHandler.errorEvent.collectAsStateWithLifecycle(initialValue = null)
val errorVisibilityEvent by ErrorEventHandler.errorVisibilityEvent.collectAsStateWithLifecycle(
initialValue = ErrorVisibilityEvent.Nothing
)
val confirmStateChange = errorEvent?.errorConfig?.cancelable.orFalse()
val state = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden,
confirmValueChange = { confirmStateChange })
val errorEventState by ErrorEventHandler.errorEventState.collectAsStateWithLifecycle()
val bottomSheetState =
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden,
confirmValueChange = { errorEventState.confirmStateChange })
val scope = rememberCoroutineScope()
val showErrorSheet = {
onErrorVisibilityChange(true, confirmStateChange)
scope.launch {
state.showSheet()
ErrorEventHandler.postErrorVisibilityEvent(ErrorVisibilityEvent.Nothing)
LaunchedEffect(key1 = errorEventState.showErrorState) {
when (errorEventState.showErrorState) {
true -> scope.launch {
bottomSheetState.show()
onErrorVisibilityChange(true, errorEventState.confirmStateChange)
}
false -> scope.launch {
bottomSheetState.hide()
onErrorVisibilityChange(false, errorEventState.confirmStateChange)
}
}
Unit
}
val dismissErrorSheet = {
onErrorVisibilityChange(false, confirmStateChange)
scope.launch {
state.hideSheet()
ErrorEventHandler.postErrorVisibilityEvent(ErrorVisibilityEvent.HideErrorSheet)
LaunchedEffect(Unit) {
snapshotFlow { bottomSheetState.currentValue }.collect {
if (it == ModalBottomSheetValue.Hidden) {
ErrorEventHandler.updateErrorEventState(
showErrorState = false
)
}
}
Unit
}
val onErrorCtaClick =
{ errorConfig: NaviPayErrorConfig, buttonErrorConfig: NaviPayErrorButtonConfig ->
dismissErrorSheet()
ErrorEventHandler.updateErrorEventState(
showErrorState = false
)
scope.launch { ErrorEventHandler.postErrorClickEvent(errorConfig, buttonErrorConfig) }
genericErrorCtaHandler.handleCtaCustomAction(errorConfig, buttonErrorConfig)
}
when (errorVisibilityEvent) {
is ErrorVisibilityEvent.ShowErrorSheet -> showErrorSheet()
is ErrorVisibilityEvent.HideErrorSheet -> dismissErrorSheet()
else -> Unit
}
NaviPayModalBottomSheetLayout(
sheetState = state,
sheetState = bottomSheetState,
sheetContent = {
GenericErrorBottomSheetContent(errorEvent, onErrorCtaClick)
GenericErrorBottomSheetContent(
errorEventState = errorEventState,
onErrorCtaClick = onErrorCtaClick
)
}
) {
naviPayActivity.navController = rememberNavController()
DestinationsNavHost(
startRoute = customerStatusRoute ?: NavGraphs.root.startRoute,
navGraph = NavGraphs.root,
engine = rememberAnimatedNavHostEngine(rootDefaultAnimations = RootNavGraphDefaultAnimations(
enterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
animationSpec = tween(400)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
animationSpec = tween(400)
)
},
popEnterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
animationSpec = tween(400)
)
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
animationSpec = tween(400)
)
}
)),
navController = naviPayActivity.navController,
dependenciesContainerBuilder = {
dependency(naviPayActivity)

View File

@@ -46,7 +46,6 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -668,8 +667,6 @@ class MandateDetailOfPendingCategoryViewModel @Inject constructor(
val declineMandateAPIResponse =
mandateRepository.reviewMandate(reviewMandateRequest = declineMandateRequest)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
updateMandateScreenBackNavigationState(mandateScreenBackNavigation = MandateScreenBackNavigation.RefreshAndMaintainSameTab)
if (!declineMandateAPIResponse.isSuccessWithData()) {
@@ -761,8 +758,6 @@ class MandateDetailOfPendingCategoryViewModel @Inject constructor(
blockSpamUserRequest = blockSpamUserRequest
)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
updateBottomSheetUIState(showBottomSheet = false)
if (!blockSpamUserAPIResponse.isSuccess()) {

View File

@@ -32,7 +32,6 @@ import com.navi.pay.utils.NOTES_REGEX
import com.navi.pay.utils.NOTE_MAX_LENGTH
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@@ -161,8 +160,6 @@ class RequestMoneyViewModel @Inject constructor(
val requestMoneyAPIResponse =
requestMoneyRepository.requestMoney(requestMoneyRequest = requestMoneyRequest)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
if (!requestMoneyAPIResponse.isSuccessWithData()) {
naviPayAnalytics.onRequestMoneyFailure(
error = requestMoneyAPIResponse.errors?.getOrNull(

View File

@@ -32,7 +32,6 @@ import com.navi.pay.utils.NaviPayPoller
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
@@ -203,8 +202,6 @@ class TransactionHistoryDetailViewModel @Inject constructor(
val paymentStatusAPIResponse =
paymentStatusRepository.fetchTransactionStatus(request = transactionStatusRequest)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
updateBottomSheetUIState(showBottomSheet = false)
if (!paymentStatusAPIResponse.isSuccessWithData()) {

View File

@@ -217,8 +217,6 @@ class SelectBankViewModel @Inject constructor(
)
)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
if (!fetchBankAccountAPIResponse.isSuccessWithData()) {
updateBottomSheetUIState(showBottomSheet = false)
notifyError(fetchBankAccountAPIResponse, tag = "onBankAccountsFailure")

View File

@@ -38,7 +38,6 @@ import com.navi.pay.utils.NAVI_PAY_LOADER
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
@@ -165,8 +164,6 @@ class LinkedAccountDetailViewModel @Inject constructor(
)
)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
if (accountPrimaryAPIResponse.isError()) {
naviPayAnalytics.onSetAsPrimaryFailure()
updateBottomSheetUIState(showBottomSheet = false)
@@ -221,8 +218,6 @@ class LinkedAccountDetailViewModel @Inject constructor(
)
)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
if (!deleteAccountAPIResponse.isSuccessWithData()) {
updateBottomSheetUIState(showBottomSheet = false)
notifyError(deleteAccountAPIResponse, tag = "onRemoveAccountFailure")

View File

@@ -39,7 +39,6 @@ import com.navi.pay.utils.PIN_TRIES_EXCEEDED
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -237,8 +236,6 @@ class LinkedAccountsViewModel @Inject constructor(
)
)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
if (!checkBalanceAPIResponse.isSuccessWithData()) {
updateUIState(uiState = LinkedAccountsScreenUIState.LinkedAccountsScreen)

View File

@@ -84,6 +84,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@HiltViewModel
class NaviPayOnboardingViewModel @Inject constructor(
@@ -628,7 +629,8 @@ class NaviPayOnboardingViewModel @Inject constructor(
showAddAccountBottomSheet()
}
private fun showAddAccountBottomSheet() {
private suspend fun showAddAccountBottomSheet() {
delay(300.milliseconds) // In case of device_bounded state, bottom sheet value gets dropped
naviPayAnalytics.onAddAccountSheetShown()
updateBottomSheetUIState(
showBottomSheet = true,

View File

@@ -29,7 +29,6 @@ import com.navi.pay.utils.ConfigKey
import com.navi.pay.utils.NAVI_PAY_API_STATUS_SUCCESS
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@@ -188,8 +187,6 @@ class NaviPayFaqViewModel @Inject constructor(
)
)
delay(300) // Because of single API call, the bottom sheet latest update gets dropped
updateBottomSheetUIState(showBottomSheet = false)
val isDeRegisterSuccess =