diff --git a/android/app/src/main/java/com/naviapp/home/common/actions/ApiActionHandler.kt b/android/app/src/main/java/com/naviapp/home/common/actions/ApiActionHandler.kt new file mode 100644 index 0000000000..ff6204a421 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/common/actions/ApiActionHandler.kt @@ -0,0 +1,34 @@ +package com.naviapp.home.common.actions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.navi.base.utils.orTrue +import com.navi.common.uitron.model.action.V3HomeAction +import com.naviapp.analytics.utils.NaviAnalytics +import com.naviapp.home.compose.activity.HomePageActivity +import com.naviapp.home.viewmodel.HomeVM + +@Composable +fun HandleApiAction( + viewModel: HomeVM, + activity: HomePageActivity, + naviAnalyticsEventTracker: NaviAnalytics.Home, + isPaymentLoaderShowing: Boolean +) { + LaunchedEffect(Unit) { + viewModel.getActionCallback().collect { action -> + when (action) { + is V3HomeAction -> { + viewModel.fetchCards( + showLoader = action.showLoader.orTrue(), + naviAnalyticsEventTracker = naviAnalyticsEventTracker, + activity = activity, + isPaymentLoaderShowing = isPaymentLoaderShowing + ) + } + + else -> {} + } + } + } +} diff --git a/android/app/src/main/java/com/naviapp/home/common/handler/ActionsHandler.kt b/android/app/src/main/java/com/naviapp/home/common/handler/ActionsHandler.kt new file mode 100644 index 0000000000..17db196cb1 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/common/handler/ActionsHandler.kt @@ -0,0 +1,25 @@ +package com.naviapp.home.common.handler + +import androidx.compose.runtime.Composable +import com.navi.ap.common.handler.HandlePublishEventAction +import com.navi.common.managers.PermissionsManager +import com.naviapp.analytics.utils.NaviAnalytics +import com.naviapp.home.common.actions.HandleApiAction +import com.naviapp.home.compose.activity.HomePageActivity +import com.naviapp.home.viewmodel.HomeVM + +@Composable +fun InitActionsHandler( + viewModel: HomeVM, + activity: HomePageActivity, + naviAnalyticsEventTracker: NaviAnalytics.Home, + isPaymentLoaderShowing: Boolean +) { + HandlePublishEventAction(viewModel = viewModel) + HandleApiAction( + viewModel = viewModel, + activity = activity, + naviAnalyticsEventTracker = naviAnalyticsEventTracker, + isPaymentLoaderShowing = isPaymentLoaderShowing + ) +} diff --git a/android/app/src/main/java/com/naviapp/home/common/handler/HomePageDataUpdateHandler.kt b/android/app/src/main/java/com/naviapp/home/common/handler/HomePageDataUpdateHandler.kt new file mode 100644 index 0000000000..3c56dd3e95 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/common/handler/HomePageDataUpdateHandler.kt @@ -0,0 +1,98 @@ +package com.naviapp.home.common.handler + +import androidx.compose.runtime.mutableStateMapOf +import com.navi.common.model.common.WidgetResponse +import com.navi.naviwidgets.models.NaviUiTronWidget +import com.navi.naviwidgets.models.NaviWidget +import com.naviapp.home.model.WidgetUiState +import com.naviapp.home.model.WidgetUiStateData +import javax.inject.Inject + +typealias WidgetId = String + +class HomePageDataUpdateHandler @Inject constructor() { + + //List to observe existingHomePageData + private var existingHomePageWidgets: MutableList = mutableListOf() + private val widgetUiStateMap = mutableStateMapOf() + private var homeContentList = mutableListOf() + + fun updateHomePageData( + widgetResponse: WidgetResponse, + updateHomePageSuccess: (WidgetResponse) -> Unit + ) { + val content = widgetResponse.contentWidget + when (widgetUiStateMap.isEmpty()) { + true -> { + if (content != null) { + val homeContent = mutableListOf() + content.forEach { naviWidget -> + val widgetId = naviWidget.widgetId.orEmpty() + homeContent.add(widgetId) + widgetUiStateMap[widgetId] = WidgetUiStateData( + widgetUiState = WidgetUiState.VISIBLE, + widgetData = (naviWidget as NaviUiTronWidget).uiTronWidget + ) + } + homeContentList = homeContent + } + } + + false -> { + if (content != null) { + homeContentList = constructHomePageList( + oldData = existingHomePageWidgets.toMutableList(), + newData = content.toMutableList() + ) + } + } + } + + //Update home data + updateHomePageSuccess.invoke(widgetResponse) + //Replace the existing widgets to new once + existingHomePageWidgets = + content?.toMutableList() ?: mutableListOf() + } + + private fun constructHomePageList( + oldData: MutableList, + newData: MutableList + ): MutableList { + val updatedHomeList: MutableList = mutableListOf() + val seenElements = mutableSetOf() + //For addition of new widgets compared to previous data + newData.forEach { naviWidget -> + val widgetId = naviWidget.widgetId.orEmpty() + seenElements.add(widgetId) + val uiState = if (oldData.any { it.widgetId.orEmpty() == widgetId }) { + WidgetUiState.VISIBLE + } else { + WidgetUiState.NEWLY_ADDED + } + widgetUiStateMap[widgetId] = WidgetUiStateData( + uiState, + (naviWidget as NaviUiTronWidget).uiTronWidget + ) + updatedHomeList.add(widgetId) + } + //For deletion of existing widgets compared to new data + oldData.forEachIndexed { index, naviWidget -> + val widgetId = naviWidget.widgetId.orEmpty() + if (widgetId !in seenElements) { + seenElements.add(widgetId) + widgetUiStateMap[widgetId]?.apply { + widgetUiState = WidgetUiState.NOT_VISIBLE + } + updatedHomeList.getOrNull(index)?.let { + updatedHomeList.add(index, widgetId) + } ?: updatedHomeList.add(widgetId) + } + } + return updatedHomeList + } + + fun getWidgetUiStateMap() = widgetUiStateMap + + fun getHomeContentList() = homeContentList +} diff --git a/android/app/src/main/java/com/naviapp/home/common/handler/SelectiveRefreshHandler.kt b/android/app/src/main/java/com/naviapp/home/common/handler/SelectiveRefreshHandler.kt new file mode 100644 index 0000000000..428a5c68e4 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/common/handler/SelectiveRefreshHandler.kt @@ -0,0 +1,57 @@ +package com.naviapp.home.common.handler + +import com.navi.uitron.model.action.PublishEventAction +import com.navi.uitron.model.action.PublishableEventData +import com.navi.uitron.viewmodel.UiTronViewModel +import javax.inject.Inject + +class SelectiveRefreshHandler @Inject constructor() { + + fun handleSuccessState(homeVM: UiTronViewModel) { + homeVM.handleAction( + PublishEventAction( + events = listOf( + PublishableEventData( + eventName = SELECTIVE_REFRESH_SUCCESS, + stateKey = SUCCESS_STATE + ) + ) + ) + ) + } + + fun handleErrorState(homeVM: UiTronViewModel) { + homeVM.handleAction( + PublishEventAction( + events = listOf( + PublishableEventData( + eventName = SELECTIVE_REFRESH_ERROR, + stateKey = ERROR_STATE + ) + ) + ) + ) + } + + fun handleLoadingState(homeVM: UiTronViewModel) { + homeVM.handleAction( + PublishEventAction( + events = listOf( + PublishableEventData( + eventName = SELECTIVE_REFRESH_LOADING, + stateKey = LOADING_STATE + ) + ) + ) + ) + } + + companion object { + const val SELECTIVE_REFRESH_SUCCESS = "selective_refresh_success" + const val SELECTIVE_REFRESH_ERROR = "selective_refresh_error" + const val SELECTIVE_REFRESH_LOADING = "selective_refresh_loading" + const val SUCCESS_STATE = "success_state" + const val ERROR_STATE = "error_state" + const val LOADING_STATE = "loading_state" + } +} diff --git a/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivityMainScreen.kt b/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivityMainScreen.kt index de43f575d2..c07b66ea5d 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivityMainScreen.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivityMainScreen.kt @@ -30,6 +30,7 @@ import com.naviapp.analytics.utils.NaviAnalytics import com.naviapp.common.viewmodel.BottomNavBarVM import com.naviapp.common.viewmodel.InAppUpdateVM import com.naviapp.dashboard.viewmodels.DashboardSharedVM +import com.naviapp.home.common.handler.InitActionsHandler import com.naviapp.home.compose.components.BottomBarWithFabButton import com.naviapp.home.compose.components.HomePageNavHost import com.naviapp.home.compose.homescreen.onTabClick @@ -62,6 +63,12 @@ fun HomePageActivityMainScreen( ) { val navController = rememberNavController() val selectedTabId by sharedVM.selectedTabId.collectAsStateWithLifecycle() + InitActionsHandler( + viewModel = homeVM, + activity = homePageActivity, + naviAnalyticsEventTracker = naviHomeAnalytics, + isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing() + ) LaunchedEffect(key1 = selectedTabId) { onTabSelected.invoke(selectedTabId) diff --git a/android/app/src/main/java/com/naviapp/home/compose/homescreen/BackdropScaffoldComponents.kt b/android/app/src/main/java/com/naviapp/home/compose/homescreen/BackdropScaffoldComponents.kt index 3ae96d240c..01e50a0d49 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/homescreen/BackdropScaffoldComponents.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/homescreen/BackdropScaffoldComponents.kt @@ -1,20 +1,23 @@ package com.naviapp.home.compose.homescreen + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -22,6 +25,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,6 +38,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import com.navi.base.sharedpref.PreferenceManager import com.navi.common.model.common.CollapsingTopNavConfig @@ -40,134 +46,119 @@ import com.navi.common.model.common.Header import com.navi.common.uitron.model.action.LayoutStateIds import com.navi.common.uitron.model.action.UpdateViewStateActionV2 import com.navi.common.utils.getSessionId -import com.navi.naviwidgets.models.NaviUiTronWidget -import com.navi.naviwidgets.models.NaviWidget import com.navi.pay.utils.conditional import com.navi.uitron.UiTronSdkManager import com.navi.uitron.render.UiTronRenderer import com.navi.uitron.utils.toPx import com.naviapp.R import com.naviapp.analytics.utils.NaviAnalytics +import com.naviapp.home.common.handler.WidgetId import com.naviapp.home.compose.extension.bottomShadow -import com.naviapp.home.utils.shimmerEffect +import com.naviapp.home.model.WidgetUiState +import com.naviapp.home.utils.getHomeWidgetAnimationSpec import com.naviapp.home.viewmodel.HomeVM import com.naviapp.utils.Constants -import com.naviapp.utils.Constants.SECTION_CONTENT_SIZE import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -/* -Order of composition: UpperContent and ContentLoader Lottie -> BackLayer and TopAppBar -> UpperMiddleContent -> LowerMiddleContent -> LowerContent -> ProfileScreen -Added some delay after rendering each layer to get some time to take clicks +/** + * Order of composition: + * + * 1. UpperContent and ContentLoader Lottie + * 2. BackLayer and TopAppBar + * 3. UpperMiddleContent + * 4. LowerMiddleContent + * 5. LowerContent + * 6. ProfileScreen */ +@Composable @OptIn(ExperimentalFoundationApi::class) -fun frontLayerContent(modifier: Modifier, frontLayerData: List?, homeVM: HomeVM) = - @Composable { - CompositionLocalProvider(LocalOverscrollConfiguration provides null) { - Column(modifier) { - frontLayerData?.let { - UpperContent(homeVM, it.take(homeVM.upperContentSize)) +fun FrontLayerContent( + modifier: Modifier, + isShimmerVisible: Boolean, + homeVM: HomeVM +) { + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + Column(modifier) { + if (isShimmerVisible.not()) { + val widgetOrderList = homeVM.getHomeContentList() + Column { + UpperContent(homeVM) { + RenderUiTronContent(widgetOrderList.take(homeVM.upperContentSize), homeVM) - val middleAndLowerContent = it.drop(homeVM.upperContentSize) - UpperMiddleContent(homeVM, middleAndLowerContent.take(SECTION_CONTENT_SIZE)) + } + val middleAndLowerContent = widgetOrderList.drop(homeVM.upperContentSize) + UpperMiddleContent(homeVM) { + RenderUiTronContent( + middleAndLowerContent.take(Constants.SECTION_CONTENT_SIZE), homeVM + ) + } + val lowerContent = middleAndLowerContent.drop(Constants.SECTION_CONTENT_SIZE) + LowerMiddleContent(homeVM) { + RenderUiTronContent( + lowerContent.take(Constants.SECTION_CONTENT_SIZE), homeVM + ) - val lowerContent = middleAndLowerContent.drop(SECTION_CONTENT_SIZE) - LowerMiddleContent(homeVM, lowerContent.take(SECTION_CONTENT_SIZE)) - LowerContent(homeVM, lowerContent.drop(SECTION_CONTENT_SIZE)) - - ContentLoaderLottie(homeVM) - } ?: run { - DefaultContentShimmer() + } + LowerContent(homeVM) { + RenderUiTronContent( + lowerContent.drop(Constants.SECTION_CONTENT_SIZE), homeVM + ) + } + ContentLoaderLottie(homeVM = homeVM) } + } else run { + HomePageContentShimmer() } } } +} @Composable -fun DefaultContentShimmer() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp) - ) { - Spacer(modifier = Modifier.height(24.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier - .height(34.dp) - .width(94.dp) - .shimmerEffect() - ) - Box( - modifier = Modifier - .height(34.dp) - .width(148.dp) - .shimmerEffect() - ) +private fun RenderUiTronContent( + elementList: List, + homeVM: HomeVM +) { + elementList.forEach { element -> + key(element) { + val widgetUiMap = homeVM.getWidgetUiStateMap() + val visible = remember(widgetUiMap[element]?.widgetUiState) { + mutableStateOf(widgetUiMap[element]?.widgetUiState == WidgetUiState.VISIBLE) + } + LaunchedEffect(widgetUiMap[element]?.widgetUiState) { + if (widgetUiMap[element]?.widgetUiState == WidgetUiState.NEWLY_ADDED) { + visible.value = true + widgetUiMap[element].apply { + this?.widgetUiState = WidgetUiState.VISIBLE + } + } + } + AnimatedContainerForWidgets(isVisible = visible) { + val response = homeVM.getWidgetUiStateMap()[element]?.widgetData + UiTronRenderer( + dataMap = response?.data, uiTronViewModel = homeVM + ).Render(composeViews = response?.parentComposeView.orEmpty()) + } } - Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = Modifier - .height(108.dp) - .fillMaxWidth() - .shimmerEffect() - ) - Spacer(modifier = Modifier.height(32.dp)) - Box( - modifier = Modifier - .height(24.dp) - .width(176.dp) - .shimmerEffect() - ) - Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = Modifier - .height(132.dp) - .fillMaxWidth() - .shimmerEffect() - ) + } +} - Spacer(modifier = Modifier.height(32.dp)) - Box( - modifier = Modifier - .height(24.dp) - .width(120.dp) - .shimmerEffect() - ) - - Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = Modifier - .height(104.dp) - .fillMaxWidth() - .shimmerEffect() - ) - - Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = Modifier - .height(78.dp) - .fillMaxWidth() - .shimmerEffect() - ) - Spacer(modifier = Modifier.height(32.dp)) - Box( - modifier = Modifier - .height(24.dp) - .width(176.dp) - .shimmerEffect() - ) - Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = Modifier - .height(132.dp) - .fillMaxWidth() - .shimmerEffect() - ) +@Composable +private fun AnimatedContainerForWidgets( + isVisible: MutableState, homeWidget: @Composable () -> Unit +) { + AnimatedVisibility(visible = isVisible.value, enter = expandVertically( + expandFrom = Alignment.Top, + clip = true, + animationSpec = getHomeWidgetAnimationSpec() + ) { 0 } + fadeIn(animationSpec = getHomeWidgetAnimationSpec()), + exit = shrinkVertically( + shrinkTowards = Alignment.Bottom, + clip = true, + animationSpec = getHomeWidgetAnimationSpec() + ) { 0 } + fadeOut(animationSpec = getHomeWidgetAnimationSpec())) { + homeWidget() } } @@ -190,7 +181,7 @@ fun ContentLoaderLottie( ) LottieAnimation( composition = composition, - iterations = 5000 + iterations = LottieConstants.IterateForever ) } } else { @@ -201,27 +192,27 @@ fun ContentLoaderLottie( @Composable fun UpperContent( homeVM: HomeVM, - content: List, + upperContent: @Composable () -> Unit, ) { LaunchedEffect(Unit) { homeVM.logAppLaunchTimeEvent(Constants.HOME_SCREEN_IN_CAPS) // Show back layer and top app bar homeVM.setIsReadyToRenderBackLayerAndTopAppBar(true) } - RenderUiTronContent(content = content, homeVM = homeVM) + upperContent() } @Composable fun UpperMiddleContent( homeVM: HomeVM, - content: List, + upperMiddleContent: @Composable () -> Unit, ) { val showUpperMiddleLayer by homeVM.isReadyToRenderUpperMiddleContent.collectAsStateWithLifecycle() if (showUpperMiddleLayer) { LaunchedEffect(Unit) { homeVM.setIsReadyToRenderLowerMiddleContent(true) } - RenderUiTronContent(content = content, homeVM = homeVM) + upperMiddleContent() } } @@ -229,21 +220,21 @@ fun UpperMiddleContent( @Composable fun LowerMiddleContent( homeVM: HomeVM, - content: List, + lowerMiddleContent: @Composable () -> Unit, ) { val showLowerMiddleLayer by homeVM.isReadyToRenderLowerMiddleContent.collectAsStateWithLifecycle() if (showLowerMiddleLayer) { LaunchedEffect(Unit) { homeVM.setIsReadyToRenderLowerContent(true) } - RenderUiTronContent(content = content, homeVM = homeVM) + lowerMiddleContent() } } @Composable fun LowerContent( homeVM: HomeVM, - content: List, + lowerContent: @Composable () -> Unit, ) { val showLowerFrontLayer by homeVM.isReadyToRenderLowerContent.collectAsStateWithLifecycle() if (showLowerFrontLayer) { @@ -252,41 +243,28 @@ fun LowerContent( homeVM.setIsShowContentLoader(false) homeVM.setIsReadyToRenderProfileScreen(true) } - RenderUiTronContent(content = content, homeVM = homeVM) + lowerContent() homeVM.logDnDataDisplayedEvent() } } @Composable -fun RenderUiTronContent( - content: List, - homeVM: HomeVM -) { - content.forEach { item -> - if (item is NaviUiTronWidget) { - UiTronRenderer(item.uiTronWidget?.data, homeVM) - .Render(composeViews = item.uiTronWidget?.parentComposeView.orEmpty()) - } - } -} - -fun backLayerComposable(modifier: Modifier, backLayerData: Header?, homeVM: HomeVM) = - @Composable { - Column(modifier.fillMaxWidth()) { - val isShowBackLayer by homeVM.isReadyToRenderBackLayerAndTopAppBar.collectAsStateWithLifecycle() - if (isShowBackLayer) { - LaunchedEffect(Unit) { - homeVM.setIsReadyToRenderUpperMiddleContent(true) - } - backLayerData?.collapsingTopNav?.uiTronResponse?.let { - UiTronRenderer( - it.data, - homeVM - ).Render(composeViews = it.parentComposeView.orEmpty()) - } +fun BackLayerComposable(modifier: Modifier, backLayerData: Header?, homeVM: HomeVM) { + Column(modifier.fillMaxWidth()) { + val isShowBackLayer by homeVM.isReadyToRenderBackLayerAndTopAppBar.collectAsStateWithLifecycle() + if (isShowBackLayer) { + LaunchedEffect(Unit) { + homeVM.setIsReadyToRenderUpperMiddleContent(true) + } + backLayerData?.collapsingTopNav?.uiTronResponse?.let { + UiTronRenderer( + it.data, + homeVM + ).Render(composeViews = it.parentComposeView.orEmpty()) } } } +} fun topBarLayout( appBarHeight: Dp, diff --git a/android/app/src/main/java/com/naviapp/home/compose/homescreen/HomePageContentShimmer.kt b/android/app/src/main/java/com/naviapp/home/compose/homescreen/HomePageContentShimmer.kt new file mode 100644 index 0000000000..c603238daa --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/compose/homescreen/HomePageContentShimmer.kt @@ -0,0 +1,102 @@ +package com.naviapp.home.compose.homescreen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.naviapp.home.utils.shimmerEffect + +@Composable +fun HomePageContentShimmer() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp) + ) { + Spacer(modifier = Modifier.height(24.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .height(34.dp) + .width(94.dp) + .shimmerEffect() + ) + Box( + modifier = Modifier + .height(34.dp) + .width(148.dp) + .shimmerEffect() + ) + } + Spacer(modifier = Modifier.height(24.dp)) + Box( + modifier = Modifier + .height(108.dp) + .fillMaxWidth() + .shimmerEffect() + ) + Spacer(modifier = Modifier.height(32.dp)) + Box( + modifier = Modifier + .height(24.dp) + .width(176.dp) + .shimmerEffect() + ) + Spacer(modifier = Modifier.height(24.dp)) + Box( + modifier = Modifier + .height(132.dp) + .fillMaxWidth() + .shimmerEffect() + ) + + Spacer(modifier = Modifier.height(32.dp)) + Box( + modifier = Modifier + .height(24.dp) + .width(120.dp) + .shimmerEffect() + ) + + Spacer(modifier = Modifier.height(24.dp)) + Box( + modifier = Modifier + .height(104.dp) + .fillMaxWidth() + .shimmerEffect() + ) + + Spacer(modifier = Modifier.height(24.dp)) + Box( + modifier = Modifier + .height(78.dp) + .fillMaxWidth() + .shimmerEffect() + ) + Spacer(modifier = Modifier.height(32.dp)) + Box( + modifier = Modifier + .height(24.dp) + .width(176.dp) + .shimmerEffect() + ) + Spacer(modifier = Modifier.height(24.dp)) + Box( + modifier = Modifier + .height(132.dp) + .fillMaxWidth() + .shimmerEffect() + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/naviapp/home/compose/homescreen/HomeScreen.kt b/android/app/src/main/java/com/naviapp/home/compose/homescreen/HomeScreen.kt index c6532745f2..4f1fd595ec 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/homescreen/HomeScreen.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/homescreen/HomeScreen.kt @@ -167,28 +167,31 @@ fun HomeScreenScaffold( NaviHomePageScaffold( appBar = - topBarLayout( - appBarHeight = appBarHeight, - statusBarHeight = statusBarHeight, - topBarData = widgetResponse.header, - homeVM = homeVM, - listState = listState - ), - backLayer = - backLayerComposable( + topBarLayout( + appBarHeight = appBarHeight, + statusBarHeight = statusBarHeight, + topBarData = widgetResponse.header, + homeVM = homeVM, + listState = listState + ), + backLayer = { + BackLayerComposable( modifier = Modifier.requiredHeight(backLayerHeight), backLayerData = widgetResponse.header, homeVM = homeVM - ), - frontLayer = - frontLayerContent( + ) + }, + frontLayer = { + FrontLayerContent( modifier = - Modifier.fillMaxHeight() - .background(Color.White, frontLayerShapeRadius) - .verticalScroll(listState()), - frontLayerData = widgetResponse.contentWidget, + Modifier + .fillMaxHeight() + .background(Color.White, frontLayerShapeRadius) + .verticalScroll(listState()), + isShimmerVisible = widgetResponse.contentWidget.isNullOrEmpty(), homeVM = homeVM - ), + ) + }, concealedHeight = appBarHeight, revealedHeight = backLayerHeight - curvature, gesturesEnabled = gesturesEnabled.value, diff --git a/android/app/src/main/java/com/naviapp/home/model/WidgetState.kt b/android/app/src/main/java/com/naviapp/home/model/WidgetState.kt new file mode 100644 index 0000000000..762a15a802 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/model/WidgetState.kt @@ -0,0 +1,14 @@ +package com.naviapp.home.model + +import com.navi.uitron.model.UiTronResponse + +enum class WidgetUiState { + VISIBLE, + NOT_VISIBLE, + NEWLY_ADDED +} + +data class WidgetUiStateData( + var widgetUiState: WidgetUiState, + var widgetData: UiTronResponse? = null +) diff --git a/android/app/src/main/java/com/naviapp/home/utils/HomePageUtils.kt b/android/app/src/main/java/com/naviapp/home/utils/HomePageUtils.kt index 73b2a0211a..1a67823529 100644 --- a/android/app/src/main/java/com/naviapp/home/utils/HomePageUtils.kt +++ b/android/app/src/main/java/com/naviapp/home/utils/HomePageUtils.kt @@ -1,5 +1,7 @@ package com.naviapp.home.utils +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -118,4 +120,8 @@ fun DrawIcon( painter = painter, contentDescription = content ) -} \ No newline at end of file +} + +fun getHomeWidgetAnimationSpec(): FiniteAnimationSpec { + return tween(500, easing = CubicBezierEasing(0.83f, 0.17f, 0.23f, 0.89f)) +} diff --git a/android/app/src/main/java/com/naviapp/home/viewmodel/HomeVM.kt b/android/app/src/main/java/com/naviapp/home/viewmodel/HomeVM.kt index 4cacfad4d6..4426e3084c 100644 --- a/android/app/src/main/java/com/naviapp/home/viewmodel/HomeVM.kt +++ b/android/app/src/main/java/com/naviapp/home/viewmodel/HomeVM.kt @@ -76,6 +76,8 @@ import com.naviapp.common.transformer.AppLoadTimerMapper import com.naviapp.common.viewmodel.BottomNavBarVM import com.naviapp.common.viewmodel.InAppUpdateVM import com.naviapp.home.activity.InAppNotificationActivity +import com.naviapp.home.common.handler.HomePageDataUpdateHandler +import com.naviapp.home.common.handler.SelectiveRefreshHandler import com.naviapp.home.compose.activity.HomePageActivity import com.naviapp.home.compose.listener.HomeScreenCallbackListener import com.naviapp.home.compose.model.BottomStickyNudgeState @@ -129,7 +131,9 @@ constructor( private val gson: Gson, private val naviCacheRepository: NaviCacheRepositoryImpl, private val connectivityObserver: ConnectivityObserver, - private val naviPayCustomerStatusHandler: NaviPayCustomerStatusHandler + private val naviPayCustomerStatusHandler: NaviPayCustomerStatusHandler, + private val selectiveRefreshHandler: SelectiveRefreshHandler, + private val homePageDataUpdateHandler: HomePageDataUpdateHandler ) : BaseVM() { private val _isReadyToRenderBackLayerAndTopAppBar = MutableStateFlow(false) @@ -418,7 +422,9 @@ constructor( ) { val needToShowLoader = TemporaryStorageHelper.isDataModified(screen = TemporaryStorageHelper.HOME) - + if (showLoader) { + selectiveRefreshHandler.handleLoadingState(this) + } if (needToShowLoader) { coroutineScope.launch(Dispatchers.IO) { val naviCacheAltSourceEntity = @@ -438,8 +444,12 @@ constructor( ) ) deserializeWidgetResponseString(response = getHomeTabFromDatabase())?.let { responseData -> - _homeScreenState.update { HomeScreenState.Success(responseData) } + homePageDataUpdateHandler.updateHomePageData( + responseData, + ::updateHomePageSuccess + ) _homeScreenExtraData.emit(HomePageExtraData(responseData.extraData)) + selectiveRefreshHandler.handleSuccessState(this@HomeVM) } updateApiTsForHomePage() @@ -463,7 +473,7 @@ constructor( val homeTabFromDatabase = getHomeTabFromDatabase() naviCacheRepository - .getDataAndFetchFromAltSource( + .getDataAndFetchFromAltSourceWithDetails( key = NaviSharedDbKeys.HOME_TAB.name, version = BuildConfig.VERSION_CODE.toLong(), getDataFromAltSource = { @@ -482,28 +492,36 @@ constructor( emitMultipleValues = true, ) .collect { response -> - deserializeWidgetResponseString(response)?.let { widgetResponse -> + deserializeWidgetResponseString(response.data)?.let { widgetResponse -> try { if ( showLoader.not() && getHomeScreenData().isNotNull() && - response.value == homeTabFromDatabase?.value + response.data?.value == homeTabFromDatabase?.value ) { updateCachedResponse(true) } else { - _homeScreenState.update { - HomeScreenState.Success( - widgetResponse + if (response.isCurrentAndAltDataSame.orFalse().not()) { + homePageDataUpdateHandler.updateHomePageData( + widgetResponse, + ::updateHomePageSuccess ) + _homeScreenExtraData.emit( + HomePageExtraData( + widgetResponse.extraData + ) + ) + updateApiTsForHomePage() + response.data?.updatedAt?.let { timestamp -> + homeTabLastUpdateTimestamp = timestamp + } + preCacheLottieUrls(widgetResponse) } - _homeScreenExtraData.emit(HomePageExtraData(widgetResponse.extraData)) - updateApiTsForHomePage() - response.updatedAt.let { timestamp -> - homeTabLastUpdateTimestamp = timestamp + if (response.isComingFromAltSource.orFalse()) { + selectiveRefreshHandler.handleSuccessState(this@HomeVM) } - } - preCacheLottieUrls(widgetResponse) + } } catch (e: Exception) { e.log() } @@ -514,6 +532,18 @@ constructor( } } + private fun updateHomePageSuccess(widgetResponse: WidgetResponse) { + _homeScreenState.update { + HomeScreenState.Success( + widgetResponse + ) + } + } + + fun getWidgetUiStateMap() = homePageDataUpdateHandler.getWidgetUiStateMap() + + fun getHomeContentList() = homePageDataUpdateHandler.getHomeContentList() + private fun deserializeWidgetResponseString(response: NaviCacheEntity?): WidgetResponse? = getGsonBuilderForWidgetizedResponse().fromJson(response?.value, WidgetResponse::class.java) @@ -585,27 +615,30 @@ constructor( naviUpiDeviceFingerprint = deviceInfoDetails.deviceFingerPrint, ssid = deviceInfoDetails.provider.ssid ) - if (!response.isValidResponse()) { if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !connectivityObserver.isInternetConnected() ) { _connectivityObserverHolder.emit(ConnectivityObserver.Status.Unavailable) + } else if (response.error?.statusCode == API_CODE_SOCKET_TIMEOUT) { + selectiveRefreshHandler.handleErrorState(this) } else if ( response.error?.statusCode != API_CODE_UNKNOWN_HOST && response.error?.statusCode != API_CODE_CONNECT_EXCEPTION && - response.error?.statusCode != NO_INTERNET && - response.error?.statusCode != API_CODE_SOCKET_TIMEOUT + response.error?.statusCode != NO_INTERNET ) { val errorUnifiedResponse = getErrorUnifiedResponse(errors = response.errors, error = response.error) sendFailureEvent(NaviAnalytics.NEW_HOME_ACTIVITY, errorUnifiedResponse) - _homeScreenState.emit(HomeScreenState.Error(errorUnifiedResponse.errorResponse)) + if ((homeScreenState.value is HomeScreenState.Success).not()) { + _homeScreenState.emit(HomeScreenState.Error(errorUnifiedResponse.errorResponse)) + } else { + selectiveRefreshHandler.handleErrorState(this) + } } return NaviCacheAltSourceEntity(isSuccess = false) } - return NaviCacheAltSourceEntity( value = naviAppSerializerGsonBuilder().toJson(response.data), version = BuildConfig.VERSION_CODE, @@ -893,9 +926,7 @@ constructor( isPaymentLoaderShowing: Boolean, ) { if (BaseUtils.isUserLoggedIn()) { - if (showLoader && !isPaymentLoaderShowing && isProfileDrawerOpen.not()) { - activity.showLoader() - } else { + if (showLoader.not() || isPaymentLoaderShowing || isProfileDrawerOpen) { naviAnalyticsEventTracker.onHomePageInit( System.currentTimeMillis() - analyticsStartTs ) @@ -1067,6 +1098,7 @@ constructor( _topNavOffsetValue.update { value } } } + private fun getHomeScreenData(): WidgetResponse? { return if (homeScreenState.value is HomeScreenState.Success) { (homeScreenState.value as HomeScreenState.Success).data diff --git a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/handler/PublishEventActionHandler.kt b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/handler/PublishEventActionHandler.kt index cce9dcce46..803a653db8 100644 --- a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/handler/PublishEventActionHandler.kt +++ b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/handler/PublishEventActionHandler.kt @@ -11,20 +11,20 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.rememberCoroutineScope import com.google.gson.GsonBuilder -import com.navi.ap.common.viewmodel.ApplicationPlatformVM import com.navi.ap.utils.registerApUiTronDeSerializers import com.navi.base.utils.isNotNullAndNotEmpty import com.navi.uitron.model.action.PublishEventAction import com.navi.uitron.model.data.SubscriberEventData import com.navi.uitron.model.event.UiTronDataProviderFactory import com.navi.uitron.model.ui.BaseProperty +import com.navi.uitron.viewmodel.UiTronViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @Composable fun HandlePublishEventAction( - viewModel: ApplicationPlatformVM, + viewModel: UiTronViewModel, uiTronDataProviderFactory: UiTronDataProviderFactory = UiTronDataProviderFactory() ) { @@ -62,18 +62,14 @@ fun HandlePublishEventAction( } } -private fun handlePropertyUpdate( - stateKey: String?, - layoutId: String?, - viewModel: ApplicationPlatformVM -) { +private fun handlePropertyUpdate(stateKey: String?, layoutId: String?, viewModel: UiTronViewModel) { stateKey?.let { viewModel.handle[layoutId + BaseProperty.PROPERTY_SUFFIX] = it } } private fun handleDataUpdate( uiTronData: Any?, subscriberData: SubscriberEventData, - viewModel: ApplicationPlatformVM, + viewModel: UiTronViewModel, uiTronDataProviderFactory: UiTronDataProviderFactory ) { uiTronData?.let { diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 2b1acf8337..119bd0cdc1 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -95,7 +95,7 @@ moengage-rich-notification = "4.3.2" navi-alfred = "1.5.0" navi-guarddog = "1.10.0" navi-pulse = "1.3.0" -navi-uitron = "1.7.0" +navi-uitron = "1.8.0" navigation = "2.5.3" okhttp-bom = "4.12.0" otaliastudios-cameraview = "2.7.2" diff --git a/android/navi-base/src/main/java/com/navi/base/cache/model/NaviCacheEntityDetails.kt b/android/navi-base/src/main/java/com/navi/base/cache/model/NaviCacheEntityDetails.kt new file mode 100644 index 0000000000..2eb03b4be7 --- /dev/null +++ b/android/navi-base/src/main/java/com/navi/base/cache/model/NaviCacheEntityDetails.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.base.cache.model + +data class NaviCacheEntityDetails( + val data: NaviCacheEntity? = null, + val isComingFromAltSource: Boolean? = null, + val isCurrentAndAltDataSame: Boolean? = null +) diff --git a/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt b/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt index 26d7007942..c47a1e730e 100644 --- a/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt +++ b/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt @@ -10,6 +10,7 @@ package com.navi.base.cache.repository import com.navi.base.cache.dao.NaviCacheDao import com.navi.base.cache.model.NaviCacheAltSourceEntity import com.navi.base.cache.model.NaviCacheEntity +import com.navi.base.cache.model.NaviCacheEntityDetails import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -45,6 +46,22 @@ interface NaviCacheRepository { }, emitMultipleValues: Boolean = false ): Flow + + fun getDataAndFetchFromAltSourceWithDetails( + key: String, + version: Long? = null, + getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity, + isCurrentAndAltDataSame: + (currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean = + fun(currentData, altData): Boolean { + val currentDataValue = currentData?.value ?: return false + val altDataValue = altData.value ?: return false + + return currentData.version == altData.version && + currentDataValue.hashCode() == altDataValue.hashCode() + }, + emitMultipleValues: Boolean = false + ): Flow } class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: NaviCacheDao) : @@ -165,6 +182,62 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi } } + override fun getDataAndFetchFromAltSourceWithDetails( + key: String, + version: Long?, + getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity, + isCurrentAndAltDataSame: + (currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean, + emitMultipleValues: Boolean + ) = flow { + var isValueEmitted = false + + val currentValueInDB = get(key = key) + + if (currentValueInDB != null) { // Its a valid value + emit(NaviCacheEntityDetails(data = currentValueInDB, isComingFromAltSource = false)) + isValueEmitted = true + } + + val naviCacheValueEntityFromAltSource = getDataFromAltSource.invoke() + + if ( + !naviCacheValueEntityFromAltSource.isSuccess || + naviCacheValueEntityFromAltSource.value == null + ) { // alternate source data invalid + return@flow + } + + val naviCacheEntity = + NaviCacheEntity( + key = key, + value = naviCacheValueEntityFromAltSource.value, + version = naviCacheValueEntityFromAltSource.version ?: 1, + ttl = naviCacheValueEntityFromAltSource.ttl, + clearOnLogout = naviCacheValueEntityFromAltSource.clearOnLogout, + metaData = naviCacheValueEntityFromAltSource.metaData + ) + + val isDataSame = + isCurrentAndAltDataSame(currentValueInDB, naviCacheValueEntityFromAltSource) + + if (isDataSame.not()) { + // Data from Alt source is different + save(naviCacheEntity = naviCacheEntity) + } + + // If multiple values are required or no value is emitted yet then emit latest value + if (emitMultipleValues || !isValueEmitted) { + emit( + NaviCacheEntityDetails( + data = naviCacheEntity, + isComingFromAltSource = true, + isCurrentAndAltDataSame = isDataSame + ) + ) + } + } + private suspend fun checkIfDBValueIsValidElseRemoveEntry( naviCacheEntity: NaviCacheEntity?, version: Long?, diff --git a/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt b/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt index 64f05ed7a3..b317bc56d3 100644 --- a/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt +++ b/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt @@ -22,6 +22,7 @@ import com.navi.common.uitron.model.action.RedeemCoinAction import com.navi.common.uitron.model.action.RewardsAndReferralApiAction import com.navi.common.uitron.model.action.SdkExitAction import com.navi.common.uitron.model.action.SubmitFeedbackAction +import com.navi.common.uitron.model.action.V3HomeAction import com.navi.common.uitron.model.action.ValidateVpaAction import com.navi.uitron.deserializer.BaseUiTronTriggerApiActionDeserializer import com.navi.uitron.model.action.TriggerApiAction @@ -63,6 +64,7 @@ class UiTronTriggerApiActionDeserializer : BaseUiTronTriggerApiActionDeserialize context?.deserialize(jsonObject, ValidateVpaAction::class.java) ApiType.RedeemCoins.name -> context?.deserialize(jsonObject, RedeemCoinAction::class.java) + ApiType.V3Home.name -> context?.deserialize(jsonObject, V3HomeAction::class.java) else -> super.deserialize(json, typeOfT, context) } } diff --git a/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt b/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt index e537233f62..36173620bf 100644 --- a/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt +++ b/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt @@ -60,7 +60,8 @@ enum class ApiType { SdkExitAction, RewardsAndReferralAction, ValidateVpa, - RedeemCoins + RedeemCoins, + V3Home } enum class SourceType { diff --git a/android/navi-common/src/main/java/com/navi/common/uitron/model/action/V3HomeAction.kt b/android/navi-common/src/main/java/com/navi/common/uitron/model/action/V3HomeAction.kt new file mode 100644 index 0000000000..81474bad79 --- /dev/null +++ b/android/navi-common/src/main/java/com/navi/common/uitron/model/action/V3HomeAction.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.common.uitron.model.action + +import com.navi.uitron.model.action.TriggerApiAction + +data class V3HomeAction(val showLoader: Boolean? = null) : TriggerApiAction()