TP-83681 | Naman Khurmi | Home mvi startup optimization (#12575)

Signed-off-by: namankhurmi <naman.khurmi@navi.com>
Co-authored-by: Girish Suragani <girish.suragani@navi.com>
This commit is contained in:
Naman Khurmi
2024-09-19 01:29:29 +05:30
committed by GitHub
parent b7686e4173
commit 8f5a0edadb
47 changed files with 2202 additions and 2222 deletions

View File

@@ -77,7 +77,7 @@ constructor(private val bottomNavBarRepository: BottomNavBarRepository) : BaseVM
_updateTabSelection.resetReplayCache()
}
fun getBottomNudgeLatestState() = bottomStickyNudgeData.value.bottomStickyNudgeState
fun getBottomNudgeLatestState() = _bottomStickyNudgeData.value.bottomStickyNudgeState
private fun getGiNavCta(): CtaData? {
return _giNavCtaFlow.value?.cta

View File

@@ -9,26 +9,21 @@ 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
import com.naviapp.home.viewmodel.HomeViewModel
@Composable
fun HandleApiAction(
viewModel: HomeVM,
viewModel: HomeViewModel,
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,
viewModel.loadHomeElements(
activity = activity,
isPaymentLoaderShowing = isPaymentLoaderShowing
)

View File

@@ -12,11 +12,11 @@ import com.navi.ap.common.handler.HandlePublishEventAction
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
import com.naviapp.home.viewmodel.HomeViewModel
@Composable
fun InitActionsHandler(
viewModel: HomeVM,
viewModel: HomeViewModel,
activity: HomePageActivity,
naviAnalyticsEventTracker: NaviAnalytics.Home,
isPaymentLoaderShowing: Boolean
@@ -25,7 +25,6 @@ fun InitActionsHandler(
HandleApiAction(
viewModel = viewModel,
activity = activity,
naviAnalyticsEventTracker = naviAnalyticsEventTracker,
isPaymentLoaderShowing = isPaymentLoaderShowing
)
HandleInstallDynamicModulesAction(

View File

@@ -0,0 +1,214 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.common.handler
import android.Manifest
import com.google.gson.Gson
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.BaseUtils
import com.navi.common.model.ModuleNameV2
import com.navi.common.network.ApiConstants
import com.navi.common.utils.TemporaryStorageHelper
import com.navi.uitron.model.action.UpdateDataAction
import com.navi.uitron.model.action.UpdateViewStateAction
import com.navi.uitron.model.data.TextData
import com.navi.uitron.model.data.UiTronActionData
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.transformer.AppLoadTimerMapper
import com.naviapp.home.compose.model.CtaActionEvent
import com.naviapp.home.compose.model.InitiatePaymentFromComposeData
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.network.di.DataDeserializers
import com.naviapp.utils.Constants
import com.naviapp.utils.Constants.HomePageConstants.NAVI_APP_NAV_HOME_PAGE_VIEWED
import com.naviapp.utils.Constants.HomePageConstants.NAVI_APP_NAV_INSURANCE_PAGE_VIEWED
import com.naviapp.utils.Constants.HomePageConstants.NAVI_APP_NAV_INVESTMENT_PAGE_VIEWED
import com.naviapp.utils.Constants.HomePageConstants.NAVI_APP_NAV_LOAN_PAGE_VIEWED
import com.naviapp.utils.Constants.Notification.HIDE_NOTIFICATION_COUNT
import com.naviapp.utils.Constants.Notification.NINE_PLUS
import com.naviapp.utils.Constants.Notification.NOTIFICATION_COUNT_TEXT
import com.naviapp.utils.Constants.Notification.SHOW_NOTIFICATION_COUNT
import com.naviapp.utils.NOTIFICATION_PERMISSION_SHOWN
import javax.inject.Inject
class EffectHandler
@Inject
constructor(
@DataDeserializers private val dataDeserializers: Gson,
private val dashBoardAnalytics: NaviAnalytics.Dashboard,
private val selectiveRefreshHandler: SelectiveRefreshHandler,
) {
private val naviAnalyticsEventTracker = lazy { NaviAnalytics.naviAnalytics.Home() }
suspend fun handleEffects(
effects: HpEffects,
homeVM: HomeViewModel,
onPaymentInitiated: (InitiatePaymentFromComposeData) -> Unit,
onFetchHomeApiCall: () -> Unit,
handleCtaAction: (CtaActionEvent) -> Unit
) {
when (effects) {
is HpEffects.OnActionData -> homeVM.handleActions(effects.actionData)
is HpEffects.OnUitronAction -> homeVM.handleAction(effects.action)
is HpEffects.OnActionsFromJson -> handleActionsFromJson(effects, homeVM)
is HpEffects.TrackBottomBarEvents -> trackBottomBarEvents()
is HpEffects.ShowNotificationPermissions -> handleNotificationPermissions(effects)
is HpEffects.OnNotificationUpdatedCount -> handleNotificationCount(effects, homeVM)
is HpEffects.OnRenderActions -> handleRenderActions(homeVM)
is HpEffects.OnApiFailure -> handleApiFailure(effects, homeVM)
is HpEffects.LogAppLaunchTime -> logAppLaunchTime(effects)
is HpEffects.OnHideBalanceAction -> handleHideBalanceAction(homeVM)
is HpEffects.OnPrioritySectionRendered -> homeVM.handleRemainingItemsForFirstLoad()
is HpEffects.InitiatePayment -> onPaymentInitiated(effects.data)
is HpEffects.FetchHomeApi -> onFetchHomeApiCall()
is HpEffects.HandleCtaActionEvents -> handleCtaAction(effects.event)
}
}
private fun handleActionsFromJson(effects: HpEffects.OnActionsFromJson, homeVM: HomeViewModel) {
TemporaryStorageHelper.setIsDataModified(
screen = TemporaryStorageHelper.HOME,
isDataModified = false
)
homeVM.handleActionsFromJson(
actionsString = effects.handler?.actionsString.orEmpty(),
variableMap = effects.handler?.variableMap.orEmpty(),
gson = dataDeserializers
)
}
private fun trackBottomBarEvents() {
if (BaseUtils.isUserLoggedIn()) {
val events =
listOf(
NAVI_APP_NAV_HOME_PAGE_VIEWED to "1",
NAVI_APP_NAV_INVESTMENT_PAGE_VIEWED to "2",
NAVI_APP_NAV_LOAN_PAGE_VIEWED to "3",
NAVI_APP_NAV_INSURANCE_PAGE_VIEWED to "4"
)
events.forEach { (event, position) ->
dashBoardAnalytics.trackEventWithProperties(
event,
mapOf(NaviAnalytics.POSITION to position)
)
}
}
}
private fun handleNotificationPermissions(effects: HpEffects.ShowNotificationPermissions) {
effects.manager.let { manager ->
if (
!manager.hasPermission(Manifest.permission.POST_NOTIFICATIONS) &&
BaseUtils.isUserLoggedIn() &&
!PreferenceManager.getBooleanPreference(NOTIFICATION_PERMISSION_SHOWN, false)
) {
PreferenceManager.setBooleanPreference(NOTIFICATION_PERMISSION_SHOWN, true)
manager.requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS))
}
}
}
private fun handleNotificationCount(
effects: HpEffects.OnNotificationUpdatedCount,
homeVM: HomeViewModel
) {
val count = effects.count
if (count > 0) {
val countText = if (count > 9) NINE_PLUS else count.toString()
UpdateDataAction(
listOf(
UpdateDataAction.ViewData(
layoutId = NOTIFICATION_COUNT_TEXT,
data = TextData(text = countText)
)
)
)
}
homeVM.state.value.screenDefinition
?.screenStructure
?.collapsingToolbar
?.toolBarNav
?.uiTronActionWithKey
?.find { it.key == if (count > 0) SHOW_NOTIFICATION_COUNT else HIDE_NOTIFICATION_COUNT }
?.uiTronAction
?.let { homeVM.handleAction(it) }
}
private fun handleRenderActions(homeVM: HomeViewModel) {
homeVM.state.value.screenDefinition?.screenStructure?.renderActions?.let { renderActions ->
renderActions.postRenderAction?.let { homeVM.handleActions(it) }
renderActions.apiSuccessRenderAction?.let { homeVM.handleActions(it) }
}
}
private fun handleApiFailure(effects: HpEffects.OnApiFailure, homeVM: HomeViewModel) {
when (effects.errorMessage?.statusCode) {
ApiConstants.API_CODE_SOCKET_TIMEOUT -> selectiveRefreshHandler.handleErrorState(homeVM)
ApiConstants.API_CODE_UNKNOWN_HOST,
ApiConstants.API_CODE_CONNECT_EXCEPTION,
ApiConstants.NO_INTERNET -> {}
else -> {
val errorUnifiedResponse =
homeVM.getErrorUnifiedResponse(effects.errors, effects.errorMessage)
homeVM.sendFailureEvent(
NaviAnalytics.NEW_HOME_ACTIVITY,
errorUnifiedResponse,
ModuleNameV2.App.name
)
homeVM.sendEvent(HpEvents.UpdateError(errorUnifiedResponse.errorResponse))
}
}
}
private fun logAppLaunchTime(effects: HpEffects.LogAppLaunchTime) {
if (!AppLoadTimerMapper.getAppLaunchEventStatus()) {
val activityTime =
System.currentTimeMillis() - AppLoadTimerMapper.getActivityStartTime()
val coldBootTime = activityTime + AppLoadTimerMapper.getApplicationOnCreateTime()
naviAnalyticsEventTracker.value.appLaunchTimeEvent(
destination = effects.destination,
coldBootTime = coldBootTime,
activityBootTime = activityTime,
applicationBootTime = AppLoadTimerMapper.getApplicationOnCreateTime()
)
AppLoadTimerMapper.setAppLaunchEventStatus(true)
AppLoadTimerMapper.resetApplicationCreatedOnLaunchStatus()
}
}
private fun handleHideBalanceAction(homeVM: HomeViewModel) {
val hideBalanceActionData =
UiTronActionData(
actions =
listOf(
UpdateViewStateAction(
viewStates =
mapOf(
Constants.CHECK_BALANCE_ROW to Constants.VISIBLE,
Constants.HIDE_BALANCE_ROW to Constants.INVISIBLE,
Constants.CHECK_BALANCE_CLICK to Constants.VISIBLE,
Constants.HIDE_BALANCE_CLICK to Constants.INVISIBLE
)
),
UpdateDataAction(
viewDataList =
listOf(
UpdateDataAction.ViewData(
layoutId = Constants.BALANCE_TEXT,
data = TextData(text = Constants.BALANCE_TEXT_MASKED_VALUE)
)
)
)
)
)
homeVM.handleActions(hideBalanceActionData)
}
}

View File

@@ -1,106 +0,0 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.common.handler
import androidx.compose.runtime.mutableStateMapOf
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.uitron.model.UiTronResponse
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<AlchemistWidgetModelDefinition<UiTronResponse>> =
mutableListOf()
private val widgetUiStateMap = mutableStateMapOf<WidgetId, WidgetUiStateData>()
private var homeContentList = mutableListOf<WidgetId>()
fun updateHomePageData(
screenDefinition: AlchemistScreenDefinition,
updateHomePageSuccess: (AlchemistScreenDefinition) -> Unit
) {
val content = screenDefinition.screenStructure?.content?.widgets
when (widgetUiStateMap.isEmpty()) {
true -> {
if (content != null) {
val homeContent = mutableListOf<WidgetId>()
content.forEach { naviWidget ->
val widgetId = naviWidget.widgetId.orEmpty()
if (widgetId.isNotEmpty()) {
homeContent.add(widgetId)
widgetUiStateMap[widgetId] =
WidgetUiStateData(
widgetUiState = WidgetUiState.VISIBLE,
widgetData = naviWidget.widgetData
)
}
}
homeContentList = homeContent
}
}
false -> {
if (content != null) {
homeContentList =
constructHomePageList(
oldData = existingHomePageWidgets.toMutableList(),
newData = content.toMutableList()
)
}
}
}
// Update home data
updateHomePageSuccess.invoke(screenDefinition)
// Replace the existing widgets to new once
existingHomePageWidgets = content?.toMutableList() ?: mutableListOf()
}
private fun constructHomePageList(
oldData: MutableList<AlchemistWidgetModelDefinition<UiTronResponse>>,
newData: MutableList<AlchemistWidgetModelDefinition<UiTronResponse>>
): MutableList<WidgetId> {
val updatedHomeList: MutableList<WidgetId> = mutableListOf()
val seenElements = mutableSetOf<WidgetId>()
// For addition of new widgets compared to previous data
newData.forEach { naviWidget ->
val widgetId = naviWidget.widgetId
if (widgetId?.isNotBlank() == true) {
seenElements.add(widgetId)
val uiState =
if (oldData.any { it.widgetId.orEmpty() == widgetId }) {
WidgetUiState.VISIBLE
} else {
WidgetUiState.NEWLY_ADDED
}
widgetUiStateMap[widgetId] = WidgetUiStateData(uiState, naviWidget.widgetData)
updatedHomeList.add(widgetId)
}
}
// For deletion of existing widgets compared to new data
oldData.forEachIndexed { index, naviWidget ->
val widgetId = naviWidget.widgetId
if (widgetId !in seenElements && widgetId?.isNotBlank() == true) {
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
}

View File

@@ -13,11 +13,11 @@ import com.navi.common.uitron.model.action.InstallDynamicModulesAction
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.home.utils.checkForModulesInstall
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.HomeViewModel
@Composable
fun HandleInstallDynamicModulesAction(
viewModel: HomeVM,
viewModel: HomeViewModel,
activity: HomePageActivity,
naviAnalyticsEventTracker: NaviAnalytics.Home
) {

View File

@@ -10,7 +10,6 @@ package com.naviapp.home.compose.activity
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import androidx.activity.SystemBarStyle
@@ -40,6 +39,7 @@ import com.navi.base.model.NaviWidgetClickWithActionData
import com.navi.base.sharedpref.CommonPrefConstants.CURRENT_USER
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.AppLaunchUtils
import com.navi.base.utils.BaseUtils
import com.navi.base.utils.ConnectivityObserver
import com.navi.base.utils.ConnectivityObserverImpl
import com.navi.base.utils.DateUtils
@@ -79,9 +79,7 @@ import com.navi.common.utils.Constants.ScreenLockConstants.DISABLED
import com.navi.common.utils.Constants.ScreenLockConstants.ENABLED
import com.navi.common.utils.Constants.ScreenLockConstants.IS_SCREEN_LOCK_ENABLED
import com.navi.common.utils.TemporaryStorageHelper
import com.navi.common.utils.getDensityName
import com.navi.common.utils.getDeviceSignature
import com.navi.common.utils.getInstalledDynamicModulesCommaSeparated
import com.navi.common.utils.getLocalStorageLocation
import com.navi.common.utils.getNetworkType
import com.navi.common.utils.getScreenRefreshRate
@@ -103,12 +101,12 @@ import com.navi.naviwidgets.models.LottieFieldData
import com.navi.naviwidgets.models.NaviTextComponent
import com.navi.naviwidgets.models.response.BottomSheetInfoV2
import com.navi.naviwidgets.utils.AP_LAUNCH
import com.navi.naviwidgets.utils.CURRENT_VERSION_IN_STORE
import com.navi.naviwidgets.utils.IN_APP_UPDATE
import com.navi.naviwidgets.utils.LottieEnums
import com.navi.naviwidgets.utils.NaviWidgetIconUtils
import com.navi.naviwidgets.utils.toCtaData
import com.navi.naviwidgets.widgets.ParameterValueJsonDeserializer.Companion.KEY_CTA_DATA
import com.navi.pay.common.setup.NaviPayManager
import com.navi.pay.utils.DATE_TIME_FORMAT_DATE_MONTH_NAME_AT_TIME
import com.navi.pay.utils.KEY_CHECK_BALANCE_ACTION
import com.navi.pay.utils.KEY_VALUE_MAPPING
@@ -151,6 +149,7 @@ import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.dashboard.DashboardBaseActivity
import com.naviapp.dashboard.listeners.DialogBoxPrimaryCtaListener
import com.naviapp.dashboard.views.fragment.PaymentFailedBottomSheet
import com.naviapp.home.common.handler.EffectHandler
import com.naviapp.home.common.hopperProcessor.processHandlerImpl.Hopper
import com.naviapp.home.compose.home.ui.screen.HomePageActivityMainScreen
import com.naviapp.home.compose.model.BottomStickyNudgeState
@@ -160,8 +159,10 @@ import com.naviapp.home.dashboard.ui.DashboardFragment
import com.naviapp.home.dashboard.ui.ProductFragment.DashboardTypes
import com.naviapp.home.listener.StickyBottomNudgeListener
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.usecase.HomePageRedirectionUseCase
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.ProfileVM
import com.naviapp.home.viewmodel.SharedVM
@@ -195,11 +196,13 @@ import com.naviapp.utils.addDivider
import com.naviapp.utils.deleteCacheAndOpenLoginPage
import com.naviapp.utils.giDeeplink
import com.naviapp.utils.toast
import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import java.util.Locale
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
@@ -228,8 +231,12 @@ class HomePageActivity :
@Inject lateinit var hopper: Hopper
@Inject lateinit var naviPayManager: Lazy<NaviPayManager>
@Inject lateinit var homeEffectHandler: Lazy<EffectHandler>
private val configVM by lazy { ViewModelProvider(this)[ConfigVM::class.java] }
private val homeVM by lazy { ViewModelProvider(this)[HomeVM::class.java] }
private val homeVM by lazy { ViewModelProvider(this)[HomeViewModel::class.java] }
private val profileVM by lazy { ViewModelProvider(this)[ProfileVM::class.java] }
private val notificationVM by lazy { ViewModelProvider(this)[NotificationVM::class.java] }
private val registrationVM by lazy { ViewModelProvider(this)[RegistrationVM::class.java] }
@@ -296,11 +303,11 @@ class HomePageActivity :
AppLoadTimerMapper.initActivityStartTime()
installSplashScreen()
super.onCreate(savedInstanceState)
redirectionUseCase.redirectToDestination(this, homeVM)
redirectionUseCase.redirectToDestination(homeVM)
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
)
initHomeItems()
homeVM.loadHomeElements(this@HomePageActivity)
checkForAppLaunchEvent()
setContent {
HomePageMaterialTheme {
@@ -341,7 +348,7 @@ class HomePageActivity :
}
private fun fetchOtherModulesData() {
if (homeVM.isUserLoggedIn()) {
if (BaseUtils.isUserLoggedIn()) {
fetchUserProfileData()
}
}
@@ -381,15 +388,15 @@ class HomePageActivity :
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
val selectedTabId = sharedVM.getSelectedTabId()
if (homeVM.isProfileDrawerOpen) {
homeVM.updateProfileDrawerState(false)
if (homeVM.state.value.profileDrawerState) {
homeVM.sendEvent(HpEvents.UpdateProfileDrawerState(false))
} else if (selectedTabId == BottomBarTabType.HOME.name) {
TemporaryStorageHelper.homePageBackPressed = true
super.onBackPressed()
} else if (selectedTabId == BottomBarTabType.LOAN.name && sharedVM.showBottomSheet.value) {
sharedVM.setBottomSheetState(false)
} else if (selectedTabId == BottomBarTabType.INVESTMENT.name) {
hopper.cancelProcess(this)
hopper.cancelProcess(this@HomePageActivity)
navigateToHomeTab()
} else {
navigateToHomeTab()
@@ -401,7 +408,7 @@ class HomePageActivity :
}
private fun startPeriodicDataUploadWorker() {
if (homeVM.isUserLoggedIn()) {
if (BaseUtils.isUserLoggedIn()) {
val isReadSmsPermissionGranted = isReadSmsPermissionGranted(applicationContext)
if (isReadSmsPermissionGranted) {
userDataAnalyticsTracker.onDataPermissionAvailable(
@@ -432,10 +439,10 @@ class HomePageActivity :
fetchGiNavCta()
TemporaryStorageHelper.fetchGiNavCta = false
}
if (homeVM.isUserLoggedIn()) {
if (BaseUtils.isUserLoggedIn()) {
notificationVM.fetchNotificationsItems(true)
fetchProfileItems()
homeVM.syncNaviPayDelayedOnboardingExperiment()
homeVM.coroutineScope.launch { naviPayManager.get().syncOnBoardingExperiment() }
}
TempStorageHelper.clear()
if (connectivityObserver.isInternetConnected()) {
@@ -449,7 +456,9 @@ class HomePageActivity :
}
}
if (redirectionUseCase.isUpiNuxRedirection(intent.extras).not()) {
homeVM.showNotificationPermission(permissionsManager)
homeVM.sendEffect(homeVM.coroutineScope) {
HpEffects.ShowNotificationPermissions(permissionsManager)
}
}
}
@@ -537,38 +546,36 @@ class HomePageActivity :
}
private fun observeNetworkConnectivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeVM.connectivityObserverHolder.collect {
when (it) {
ConnectivityObserver.Status.Available -> {
if (homeVM.isUserLoggedIn()) {
callHomeItemsApi()
fetchProfileItems()
}
// Set the connecting nudge and
// remove any network related nudge after 2 seconds
bottomNavBarVM.setBottomNudge(
true,
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeVM.internetConnectivity.collect {
when (it) {
ConnectivityObserver.Status.Available -> {
homeVM.loadHomeElements(this@HomePageActivity)
if (BaseUtils.isUserLoggedIn()) {
fetchProfileItems()
}
// Set the connecting nudge and
// remove any network related nudge after 2 seconds
bottomNavBarVM.setBottomNudge(
true,
BottomStickyNudgeState.NetworkConnectivityNudgeState(
networkConnectivityData = getNudgeDataForInternetConnected()
)
)
delay(2.seconds)
bottomNavBarVM.setBottomNudge(false)
}
ConnectivityObserver.Status.Unavailable,
ConnectivityObserver.Status.Losing,
ConnectivityObserver.Status.Lost -> {
bottomNavBarVM.setBottomNudge(
visible = true,
bottomStickyNudgeState =
BottomStickyNudgeState.NetworkConnectivityNudgeState(
networkConnectivityData = getNudgeDataForInternetConnected()
getNudgeDataForInternetDisconnected()
)
)
delay(2.seconds)
bottomNavBarVM.setBottomNudge(false)
}
ConnectivityObserver.Status.Unavailable,
ConnectivityObserver.Status.Losing,
ConnectivityObserver.Status.Lost -> {
bottomNavBarVM.setBottomNudge(
visible = true,
bottomStickyNudgeState =
BottomStickyNudgeState.NetworkConnectivityNudgeState(
getNudgeDataForInternetDisconnected()
)
)
}
)
}
}
}
@@ -681,7 +688,7 @@ class HomePageActivity :
}
inAppUpdateVM.showInstallAppSnackBar.observe(this) { shouldShowAppUpdateStrip ->
if (shouldShowAppUpdateStrip) {
homeVM.updateHomeScreenSnackBarState(true)
homeVM.sendEvent(HpEvents.UpdateSnackBarState(true))
}
}
}
@@ -689,43 +696,47 @@ class HomePageActivity :
private fun observeNpsRatingSubmitResponse() {
npsVM.ratingSubmitResponse.observeNonNull(this) {
if (it.response == Constants.SUCCESS) {
homeVM.homeFeatures.value?.survey?.content?.dialogBoxInfo?.let { dialogBoxResponse
->
if (
intent.extras
?.getParcelable<PreviousScreenNameRequest>(Constants.PREVIOUS_SCREEN)
?.moduleType
.orEmpty() == GI
) {
NaviInsuranceDeeplinkNavigator.navigate(
activity = this,
ctaData =
CtaData(
url =
giDeeplink(
NaviInsuranceDeeplinkNavigator.DASHBOARD.plus(
Constants.DIVIDER
)
.plus(NaviInsuranceDeeplinkNavigator.HOME)
)
),
finish = true,
bundle =
Bundle().apply {
putParcelable(NPS_SUBMIT_DIALOG, dialogBoxResponse)
}
)
} else {
val commonDialogBox =
CommonDialogBox.newInstance(
screenName = NaviAnalytics.FEEDBACK_OFFER_DIALOG,
title = dialogBoxResponse.title,
description = dialogBoxResponse.subtitle,
ctaData = dialogBoxResponse.cta,
iconCode = dialogBoxResponse.iconCode,
lottieAnimationFile = "success.json"
CoroutineScope(Dispatchers.IO).launch {
homeVM.state.value.homeFeatures?.survey?.content?.dialogBoxInfo?.let {
dialogBoxResponse ->
if (
intent.extras
?.getParcelable<PreviousScreenNameRequest>(
Constants.PREVIOUS_SCREEN
)
?.moduleType
.orEmpty() == GI
) {
NaviInsuranceDeeplinkNavigator.navigate(
activity = this@HomePageActivity,
ctaData =
CtaData(
url =
giDeeplink(
NaviInsuranceDeeplinkNavigator.DASHBOARD.plus(
Constants.DIVIDER
)
.plus(NaviInsuranceDeeplinkNavigator.HOME)
)
),
finish = true,
bundle =
Bundle().apply {
putParcelable(NPS_SUBMIT_DIALOG, dialogBoxResponse)
}
)
safelyShowDialogFragment(commonDialogBox, CommonDialogBox.TAG)
} else {
val commonDialogBox =
CommonDialogBox.newInstance(
screenName = NaviAnalytics.FEEDBACK_OFFER_DIALOG,
title = dialogBoxResponse.title,
description = dialogBoxResponse.subtitle,
ctaData = dialogBoxResponse.cta,
iconCode = dialogBoxResponse.iconCode,
lottieAnimationFile = "success.json"
)
safelyShowDialogFragment(commonDialogBox, CommonDialogBox.TAG)
}
}
}
}
@@ -733,32 +744,46 @@ class HomePageActivity :
}
private fun observeHomeFeaturesData() {
homeVM.homeFeatures.observeNonNull(this) {
if (
it.ppeFeatures?.positiveReinforcement?.cta != null &&
it.ppeFeatures.positiveReinforcement.enable == true &&
AppLaunchUtils.isLandingFirstTimeAfterAppOpen(
AppLaunchUtils.HOME_FEATURE_POSITIVE_REINFORCEMENT
)
) {
AppLaunchUtils.setAppOpenStatus(AppLaunchUtils.HOME_FEATURE_POSITIVE_REINFORCEMENT)
homeVM.handleCtaData(it.ppeFeatures.positiveReinforcement.cta, this) {}
} else if (
it.ppeFeatures?.negativeReinforcement?.cta != null &&
it.ppeFeatures.negativeReinforcement.enable == true &&
AppLaunchUtils.isLandingFirstTimeAfterAppOpen(
AppLaunchUtils.HOME_FEATURE_NEGATIVE_REINFORCEMENT
)
) {
AppLaunchUtils.setAppOpenStatus(AppLaunchUtils.HOME_FEATURE_NEGATIVE_REINFORCEMENT)
homeVM.handleCtaData(it.ppeFeatures.negativeReinforcement.cta, this) {}
}
lifecycleScope.launch {
homeVM.state.collect { state ->
state.homeFeatures.let {
if (
it?.ppeFeatures?.positiveReinforcement?.cta != null &&
it.ppeFeatures.positiveReinforcement.enable == true &&
AppLaunchUtils.isLandingFirstTimeAfterAppOpen(
AppLaunchUtils.HOME_FEATURE_POSITIVE_REINFORCEMENT
)
) {
AppLaunchUtils.setAppOpenStatus(
AppLaunchUtils.HOME_FEATURE_POSITIVE_REINFORCEMENT
)
homeVM.handleCtaData(
it.ppeFeatures.positiveReinforcement.cta,
this@HomePageActivity
) {}
} else if (
it?.ppeFeatures?.negativeReinforcement?.cta != null &&
it.ppeFeatures.negativeReinforcement.enable == true &&
AppLaunchUtils.isLandingFirstTimeAfterAppOpen(
AppLaunchUtils.HOME_FEATURE_NEGATIVE_REINFORCEMENT
)
) {
AppLaunchUtils.setAppOpenStatus(
AppLaunchUtils.HOME_FEATURE_NEGATIVE_REINFORCEMENT
)
homeVM.handleCtaData(
it.ppeFeatures.negativeReinforcement.cta,
this@HomePageActivity
) {}
}
it.survey?.let { npsResponse ->
safelyShowDialogFragment(
NetPromoterScoreFragment.getInstance(npsResponse),
NetPromoterScoreFragment.TAG
)
it?.survey?.let { npsResponse ->
safelyShowDialogFragment(
NetPromoterScoreFragment.getInstance(npsResponse),
NetPromoterScoreFragment.TAG
)
}
}
}
}
}
@@ -888,11 +913,11 @@ class HomePageActivity :
}
private fun trackLaunchEvents() {
homeVM.trackBottomBarEvent(dashboardAnalytics)
homeVM.sendEffect(homeVM.coroutineScope) { HpEffects.TrackBottomBarEvents }
}
private fun initHomeScreenActions() {
if (homeVM.isUserLoggedIn()) {
if (BaseUtils.isUserLoggedIn()) {
naviAnalyticsEventTracker.onHomePageCreated()
sendFCEvent()
sendLocationUpdates()
@@ -908,11 +933,12 @@ class HomePageActivity :
}
private fun fetchHomeData() {
if (homeVM.isUserLoggedIn()) {
if (BaseUtils.isUserLoggedIn()) {
fetchNuxScreenDataForEligibleUsers()
configVM.fetchHomeExtras()
fetchBottomNavigationBar()
fetchOfferDialog()
homeVM.fetchHomeFeature(Constants.HOME_FEATURE)
}
}
@@ -976,14 +1002,14 @@ class HomePageActivity :
}
private fun fetchGiNavCta() {
if (homeVM.isUserLoggedIn()) {
if (BaseUtils.isUserLoggedIn()) {
bottomNavBarVM.fetchGiNavCta()
}
}
private fun fetchOfferDialog() {
if (
homeVM.isUserLoggedIn() &&
BaseUtils.isUserLoggedIn() &&
intent?.getStringExtra(Constants.REDIRECT_STATUS) != Constants.EMI_DATE_CHANGE
)
registrationVM.fetchOfferDialog()
@@ -1135,9 +1161,7 @@ class HomePageActivity :
override fun getCurrentFragmentScreenName(): String {
return if (sharedVM.getSelectedTabId() == BottomBarTabType.HOME.name) {
NaviAnalytics.NEW_HOME
} else {
homeVM.getCurrentFragmentScreenName()
}
} else homeVM.state.value.currentLoadedFragmentScreenName
}
override fun removeInstallAppSnackBar() {
@@ -1146,7 +1170,7 @@ class HomePageActivity :
}
private fun uploadUserData() {
if (homeVM.isUserLoggedIn()) {
if (BaseUtils.isUserLoggedIn()) {
val isLocationPermissionGranted = isLocationPermissionGranted(applicationContext)
val isReadSmsPermissionGranted = isReadSmsPermissionGranted(applicationContext)
naviAnalyticsEventTracker.homePagePermissionGrantedInfoEvent(
@@ -1175,24 +1199,6 @@ class HomePageActivity :
}
}
private fun initHomeItems() {
if (homeVM.isUserLoggedIn()) {
callHomeItemsApi()
}
}
private fun callHomeItemsApi(showLoader: Boolean = false) {
homeVM.fetchHomeItems(
showLoader = showLoader,
density = getDensityName(context = this).orEmpty(),
connectivityType = getNetworkType(context = this),
availableAppVersionCode =
PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
installedModules = getInstalledDynamicModulesCommaSeparated(),
clearReferralPopupPreferences = false
)
}
override fun shouldUpdateApp(appUpgradeSettings: AppUpgradeResponse) {
if (appUpgradeSettings.hardUpgrade.orTrue() || appUpgradeSettings.softUpgrade.orTrue()) {
val intent =
@@ -1741,9 +1747,9 @@ class HomePageActivity :
private fun initResourceManager() {
homeVM.viewModelScope.launch(Dispatchers.IO) {
homeVM.initiateResourceManager.collect { initResourceManager ->
homeVM.state.collect {
if (
initResourceManager &&
it.renderingFirstTime.not() &&
FirebaseRemoteConfigHelper.getBoolean(RESOURCE_MANAGER_ENABLED)
) {
ResourceManager.init(context = applicationContext)

View File

@@ -21,7 +21,8 @@ import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.common.hopperProcessor.processHandlerImpl.Hopper
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.payment.viewmodel.PaymentVM
@@ -31,7 +32,8 @@ fun HomePageNavHost(
homePageActivity: HomePageActivity,
navController: NavHostController,
dashboardSharedVM: DashboardSharedVM,
homeVM: HomeVM,
homeViewModel: () -> HomeViewModel,
hpStates: () -> HpStates,
notificationVM: NotificationVM,
sharedVM: SharedVM,
paymentVM: PaymentVM,
@@ -53,14 +55,15 @@ fun HomePageNavHost(
activity = homePageActivity,
tabId = tab.tabId,
dashboardSharedVM = dashboardSharedVM,
homeVM = homeVM,
hpStates = hpStates,
notificationVM = notificationVM,
paymentVM = paymentVM,
sharedVM = sharedVM,
naviHomeAnalytics = naviHomeAnalytics,
investmentListState = investmentListState,
inAppUpdateVM = inAppUpdateVM,
hopper = hopper
hopper = hopper,
homeVM = homeViewModel
)
}
}

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.navi.common.ui.fragment.BaseFragment
import com.navi.common.utils.TemporaryStorageHelper
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.dashboard.viewmodels.DashboardSharedVM
@@ -29,7 +30,10 @@ import com.naviapp.home.compose.home.ui.footer.utils.FragmentContainer
import com.naviapp.home.compose.home.ui.screen.HomeScreen
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.dashboard.ui.compose.loansTab.LoansTabScreen
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.payment.viewmodel.PaymentVM
@@ -39,7 +43,8 @@ fun NavGraphNavigationItem(
activity: HomePageActivity,
tabId: String,
dashboardSharedVM: DashboardSharedVM,
homeVM: HomeVM,
hpStates: () -> HpStates,
homeVM: () -> HomeViewModel,
paymentVM: PaymentVM,
notificationVM: NotificationVM,
sharedVM: SharedVM,
@@ -54,24 +59,30 @@ fun NavGraphNavigationItem(
HomeScreen(
activity = activity,
dashboardSharedVM = dashboardSharedVM,
homeVM = homeVM,
paymentVM = paymentVM,
hpStates = hpStates(),
sharedVM = sharedVM,
notificationVM = notificationVM,
naviAnalyticsEventTracker = naviHomeAnalytics,
inAppUpdateVM = inAppUpdateVM
inAppUpdateVM = inAppUpdateVM,
homeVM = homeVM,
onHomeScreenEvent = { homeVM().sendEvent(it) }
) {
when (it) {
HomeScreenCallbackListener.ApiCallingWithCondition -> {
homeVM.homePageApiCallingWithCondition(
hidden = false,
activity = activity,
isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing(),
naviAnalyticsEventTracker = naviHomeAnalytics
TemporaryStorageHelper.updateViewVisibility(
TemporaryStorageHelper.HOME,
false
)
homeVM()
.loadHomeElements(
activity = activity,
isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing()
)
}
is HomeScreenCallbackListener.InitiatePaymentOnHomePage -> {
homeVM.updatePaymentDataComposeScreen(it.paymentData)
homeVM().sendEffect(homeVM().coroutineScope) {
HpEffects.InitiatePayment(it.paymentData)
}
}
}
}
@@ -103,9 +114,9 @@ fun NavGraphNavigationItem(
fragmentManager = activity.supportFragmentManager
) { containerId ->
val fragment = InsuranceContainerFragment.getInstance(insuranceScrollState)
homeVM.setCurrentFragmentScreenName(
val screenName =
(fragment as? BaseFragment)?.screenName ?: NaviAnalytics.NEW_HOME_ACTIVITY
)
homeVM().sendEvent(HpEvents.UpdateCurrentLoadedFragmentName(screenName))
if (!fragment.isStateSaved && !fragment.isAdded) {
if (fragment.isAdded) {
show(fragment)

View File

@@ -1,44 +0,0 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.compose.home.ui.content
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.common.alchemist.model.AlchemistCollapsingToolbar
import com.naviapp.home.compose.widgetfactory.HomeWidgetRenderer
import com.naviapp.home.viewmodel.HomeVM
@Composable
fun BackLayerContent(
modifier: Modifier,
backLayerData: AlchemistCollapsingToolbar?,
homeVM: HomeVM,
homeScrollState: () -> ScrollState
) {
Column(modifier.fillMaxWidth()) {
val isShowBackLayer by
homeVM.isReadyToRenderBackLayerAndTopAppBar.collectAsStateWithLifecycle()
if (isShowBackLayer) {
LaunchedEffect(Unit) { homeVM.setIsReadyToRenderUpperMiddleContent(true) }
backLayerData?.collapsingTopNav?.uiTronResponse?.let {
HomeWidgetRenderer(
widgetData = it.data,
viewModel = homeVM,
composeView = it.parentComposeView,
homeScrollState = homeScrollState
)
}
}
}
}

View File

@@ -15,9 +15,11 @@ import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.verticalScroll
@@ -32,84 +34,68 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
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.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.common.alchemist.model.WidgetRenderState
import com.navi.naviwidgets.R
import com.navi.uitron.model.UiTronResponse
import com.naviapp.home.compose.widgetfactory.HomeWidgetRenderer
import com.naviapp.home.model.WidgetUiState
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.utils.Constants
import com.naviapp.home.viewmodel.HomeViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
typealias WidgetId = String
/**
* Order of composition:
* 1. UpperContent and ContentLoader Lottie
* 2. BackLayer and TopAppBar
* 3. UpperMiddleContent
* 4. LowerMiddleContent
* 5. LowerContent
* 6. ProfileScreen
*/
@Composable
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
fun FrontLayerContent(
modifier: Modifier,
isShimmerVisible: Boolean,
homeVM: HomeVM,
homeVM: HomeViewModel,
frontLayerShape: Shape,
hpStates: () -> HpStates,
homeScrollState: () -> ScrollState
) {
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Column(
modifier
Modifier.fillMaxHeight()
.background(Color.White, frontLayerShape)
.verticalScroll(homeScrollState())
.semantics { testTagsAsResourceId = true }
.testTag("homeScreenWidgetsList")
) {
if (isShimmerVisible.not()) {
val widgetOrderList = homeVM.getHomeContentList()
val widgets = hpStates().screenDefinition?.screenStructure?.content?.widgets
if (!widgets.isNullOrEmpty()) {
Column {
UpperContent(homeVM) {
RenderUiTronContent(
widgetOrderList.take(homeVM.upperContentSize),
homeVM,
homeScrollState
)
}
val middleAndLowerContent = widgetOrderList.drop(homeVM.upperContentSize)
UpperMiddleContent(homeVM) {
RenderUiTronContent(
middleAndLowerContent.take(Constants.SECTION_CONTENT_SIZE),
homeVM,
homeScrollState
)
}
val lowerContent = middleAndLowerContent.drop(Constants.SECTION_CONTENT_SIZE)
LowerMiddleContent(homeVM) {
RenderUiTronContent(
lowerContent.take(Constants.SECTION_CONTENT_SIZE),
homeVM,
homeScrollState
)
}
LowerContent(homeVM) {
RenderUiTronContent(
lowerContent.drop(Constants.SECTION_CONTENT_SIZE),
homeVM,
homeScrollState
)
}
ContentLoaderLottie(homeVM = homeVM)
RenderUiTronContent(widgets, homeVM, hpStates, homeScrollState)
ContentLoaderLottie(hpStates)
}
} else run { HomePageContentShimmer() }
} else HomePageContentShimmer()
}
}
}
@Composable
private fun ContentLoaderLottie(hpStates: () -> HpStates) {
if (hpStates().renderingFirstTime) {
Row(
modifier = Modifier.fillMaxWidth().padding(top = 32.dp, bottom = 100.dp),
horizontalArrangement = Arrangement.Center
) {
val composition by
rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.cta_loader_purple),
)
LottieAnimation(composition = composition, iterations = LottieConstants.IterateForever)
}
}
}
@@ -142,107 +128,56 @@ private fun AnimatedContainerForWidgets(
}
}
@Composable
fun UpperContent(
homeVM: HomeVM,
upperContent: @Composable () -> Unit,
) {
LaunchedEffect(Unit) {
homeVM.logAppLaunchTimeEvent(Constants.HOME_SCREEN_IN_CAPS)
// Show back layer and top app bar
homeVM.setIsReadyToRenderBackLayerAndTopAppBar(true)
}
upperContent()
}
@Composable
fun UpperMiddleContent(
homeVM: HomeVM,
upperMiddleContent: @Composable () -> Unit,
) {
val showUpperMiddleLayer by
homeVM.isReadyToRenderUpperMiddleContent.collectAsStateWithLifecycle()
if (showUpperMiddleLayer) {
LaunchedEffect(Unit) { homeVM.setIsReadyToRenderLowerMiddleContent(true) }
upperMiddleContent()
}
}
@Composable
fun LowerMiddleContent(
homeVM: HomeVM,
lowerMiddleContent: @Composable () -> Unit,
) {
val showLowerMiddleLayer by
homeVM.isReadyToRenderLowerMiddleContent.collectAsStateWithLifecycle()
if (showLowerMiddleLayer) {
LaunchedEffect(Unit) { homeVM.setIsReadyToRenderLowerContent(true) }
lowerMiddleContent()
}
}
@Composable
fun LowerContent(
homeVM: HomeVM,
lowerContent: @Composable () -> Unit,
) {
val showLowerFrontLayer by homeVM.isReadyToRenderLowerContent.collectAsStateWithLifecycle()
if (showLowerFrontLayer) {
LaunchedEffect(Unit) {
// Immediately remove content lottie loader
homeVM.setIsShowContentLoader(false)
homeVM.setIsReadyToInitiateResourceManager(true)
}
lowerContent()
homeVM.logDnDataDisplayedEvent()
}
}
@Composable
fun ContentLoaderLottie(homeVM: HomeVM) {
val isShowLoader by homeVM.isShowContentLoader.collectAsStateWithLifecycle()
if (isShowLoader) {
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 100.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
val composition by
rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.cta_loader_purple),
)
LottieAnimation(composition = composition, iterations = LottieConstants.IterateForever)
}
}
}
@Composable
private fun RenderUiTronContent(
elementList: List<WidgetId>,
homeVM: HomeVM,
elementList: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
homeVM: HomeViewModel,
hpStates: () -> HpStates,
homeScrollState: () -> ScrollState
) {
elementList.forEach { element ->
key(element) {
val widgetUiMap = homeVM.getWidgetUiStateMap()
val visible =
remember(widgetUiMap[element]?.widgetUiState) {
mutableStateOf(widgetUiMap[element]?.widgetUiState == WidgetUiState.VISIBLE)
element.widgetId?.let { widgetId ->
key(widgetId) {
val visible = remember {
mutableStateOf(element.widgetRenderState == WidgetRenderState.VISIBLE)
}
LaunchedEffect(widgetUiMap[element]?.widgetUiState) {
if (widgetUiMap[element]?.widgetUiState == WidgetUiState.NEWLY_ADDED) {
visible.value = true
widgetUiMap[element].apply { this?.widgetUiState = WidgetUiState.VISIBLE }
LaunchedEffect(element.widgetRenderState) {
when (element.widgetRenderState) {
WidgetRenderState.NEWLY_ADDED -> {
visible.value = true
homeVM.sendEvent(
HpEvents.UpdateScreenContentWidgetRenderState(
widgetId,
WidgetRenderState.VISIBLE
)
)
}
WidgetRenderState.NOT_VISIBLE -> {
visible.value = false
}
WidgetRenderState.VISIBLE -> {
visible.value = true
}
}
}
AnimatedContainerForWidgets(visible) {
val response = element.widgetData
HomeWidgetRenderer(
widgetData = response?.data,
viewModel = homeVM,
composeView = response?.parentComposeView.orEmpty(),
homeScrollState = homeScrollState
)
}
}
AnimatedContainerForWidgets(isVisible = visible) {
val response = homeVM.getWidgetUiStateMap()[element]?.widgetData
HomeWidgetRenderer(
widgetData = response?.data,
viewModel = homeVM,
composeView = response?.parentComposeView.orEmpty(),
homeScrollState = homeScrollState
)
}
}
LaunchedEffect(hpStates().renderingFirstTime) {
if (hpStates().renderingFirstTime) {
homeVM.sendEffect(CoroutineScope(Dispatchers.Main)) {
HpEffects.OnPrioritySectionRendered
}
}
}

View File

@@ -17,44 +17,57 @@ import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.common.alchemist.model.AlchemistDialogStructure
import com.navi.common.utils.Constants.DIALOG_VISIBILITY
import com.navi.common.utils.Constants.HIDE
import com.navi.uitron.render.UiTronRenderer
import com.naviapp.home.model.HpDialogStateHolder
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@Composable
fun HomeScreenDialog(viewModel: HomeVM) {
fun HomeScreenDialog(homeVM: () -> HomeViewModel, hpStates: () -> HpStates) {
val dialogStateHolder by viewModel.dialogStateHolder.collectAsStateWithLifecycle()
val dialogStateHolder = hpStates().hpDialogStateHolder
LaunchedEffect(dialogStateHolder.state) {
if (dialogStateHolder.state == HpDialogStateHolder.HpDialogState.Hidden) {
dialogStateHolder.dialogUIContent?.onDismissAction?.let { viewModel.handleActions(it) }
dialogStateHolder.dialogUIContent?.onDismissAction?.let {
homeVM().sendEffect(CoroutineScope(Dispatchers.Default)) {
HpEffects.OnActionData(it)
}
}
}
}
LaunchedEffect(Unit) {
viewModel.handle.getStateFlow<String?>(DIALOG_VISIBILITY, null).collect {
homeVM().handle.getStateFlow<String?>(DIALOG_VISIBILITY, null).collect {
if (it == HIDE) {
viewModel.updateDialogState(HpDialogStateHolder.HpDialogState.Hidden)
viewModel.handle[DIALOG_VISIBILITY] = null
homeVM()
.sendEvent(
HpEvents.UpdateHpDialogStateHolder(HpDialogStateHolder.HpDialogState.Hidden)
)
homeVM().handle[DIALOG_VISIBILITY] = null
}
}
}
if (dialogStateHolder.state == HpDialogStateHolder.HpDialogState.Visible) {
dialogStateHolder.dialogUIContent?.let {
DialogContentRenderer(it, viewModel) {
viewModel.updateDialogState(HpDialogStateHolder.HpDialogState.Hidden)
DialogContentRenderer(it, homeVM) {
homeVM()
.sendEvent(
HpEvents.UpdateHpDialogStateHolder(HpDialogStateHolder.HpDialogState.Hidden)
)
}
}
}
@@ -63,7 +76,7 @@ fun HomeScreenDialog(viewModel: HomeVM) {
@Composable
private fun DialogContentRenderer(
data: AlchemistDialogStructure,
viewModel: HomeVM,
homeVM: () -> HomeViewModel,
onDismissRequest: () -> Unit
) {
when (data.type) {
@@ -86,7 +99,7 @@ private fun DialogContentRenderer(
data.content?.widgets?.forEach {
UiTronRenderer(
dataMap = it.widgetData?.data,
viewModel,
homeVM(),
)
.Render(composeViews = it.widgetData?.parentComposeView ?: listOf())
}

View File

@@ -20,19 +20,18 @@ import com.naviapp.common.model.BottomStickyNudgeData
import com.naviapp.common.viewmodel.BottomNavBarVM
import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.home.ui.footer.utils.HomeFooterEvents
import com.naviapp.home.compose.home.ui.footer.utils.HomeFooterStates
import com.naviapp.home.compose.home.ui.footer.utils.handleHomeFooterEvent
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.SharedVM
@Composable
fun HomeFooterRoot(
modifier: Modifier,
homeVM: HomeVM,
homePageActivity: HomePageActivity,
homeVM: () -> HomeViewModel,
hpStates: () -> HpStates,
inAppUpdateVM: InAppUpdateVM,
dashboardSharedVM: DashboardSharedVM,
bottomNavBarVM: BottomNavBarVM,
@@ -40,11 +39,8 @@ fun HomeFooterRoot(
navController: NavHostController,
selectedTabId: String
) {
val showSnackBar by homeVM.showHomeScreenSnackBar.collectAsStateWithLifecycle()
val isFabButtonVisible by homeVM.fabButtonVisibility.collectAsStateWithLifecycle()
val bottomStickyNudgeData by bottomNavBarVM.bottomStickyNudgeData.collectAsStateWithLifecycle()
val bottomNavBarState by sharedVM.bottomNavBarStateHolder.collectAsStateWithLifecycle()
val isHomeTabSelected = selectedTabId == BottomBarTabType.HOME.name
HomeFooter(
modifier = modifier,
@@ -52,8 +48,7 @@ fun HomeFooterRoot(
bottomNudgeData = bottomStickyNudgeData,
state =
HomeFooterStates(
isFabButtonVisible = isHomeTabSelected && isFabButtonVisible,
showSnackBar = showSnackBar,
showSnackBar = hpStates().homeScreenSnackBarState,
bottomNavBarState = bottomNavBarState
),
onEvent = { homeFooterEvent ->
@@ -64,7 +59,6 @@ fun HomeFooterRoot(
sharedVM = sharedVM,
dashboardSharedVM = dashboardSharedVM,
inAppUpdateVM = inAppUpdateVM,
homePageActivity = homePageActivity,
navController = navController
)
}
@@ -83,12 +77,6 @@ fun HomeFooter(
if (state.showSnackBar) {
AppInstallSnackBar { onEvent(HomeFooterEvents.SnackBarOnClick) }
}
if (state.isFabButtonVisible) {
UpiFloatingActionButton(
onLaunchEvent = { onEvent(HomeFooterEvents.FabButtonOnLaunchEvent) },
onClickEvent = { onEvent(HomeFooterEvents.FabButtonOnClick) }
)
}
bottomNudgeData?.let {
if (it.visible) {
BottomStickyNudgeUI(

View File

@@ -1,71 +0,0 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.compose.home.ui.footer
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.design.font.FontWeightEnum
import com.navi.design.theme.getFontWeight
import com.navi.design.theme.ttComposeFontFamily
import com.navi.naviwidgets.R
import com.navi.naviwidgets.extensions.NaviText
import com.navi.pay.utils.noRippleClickable
import com.naviapp.home.utils.DrawIcon
@Composable
fun ColumnScope.UpiFloatingActionButton(onLaunchEvent: () -> Unit, onClickEvent: () -> Unit) {
LaunchedEffect(Unit) { onLaunchEvent() }
FloatingActionButton(
onClick = onClickEvent,
modifier =
Modifier.align(Alignment.End)
.padding(end = 16.dp, bottom = 16.dp)
.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
.noRippleClickable {},
shape = RoundedCornerShape(4.dp),
containerColor = Color(0xFF1F002A)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
DrawIcon(
drawableIconId = R.drawable.scan_pay,
content = stringResource(id = com.naviapp.R.string.scan_pay)
)
NaviText(
text = stringResource(id = com.naviapp.R.string.scan_pay),
modifier = Modifier.padding(start = 8.dp),
fontSize = 14.sp,
color = Color.White,
style =
TextStyle(
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_SEMI_BOLD),
textAlign = TextAlign.Center
)
)
}
}
}

View File

@@ -12,34 +12,26 @@ import androidx.navigation.NavController
import androidx.navigation.NavHostController
import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.model.ActionData
import com.navi.base.model.CtaData
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.commoncomposables.utils.resetScrollToTop
import com.navi.common.model.common.AppUpdateData
import com.navi.common.utils.Constants.APP_UPDATE_DATA
import com.navi.common.utils.Constants.CARD_NAME
import com.navi.common.utils.Constants.HOME_BOTTOM_NAV_BAR_DATA
import com.navi.naviwidgets.utils.IN_APP_UPDATE
import com.naviapp.app.NaviApplication
import com.naviapp.common.navigator.NaviDeepLinkNavigator
import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.model.BottomStickyNudgeState
import com.naviapp.home.listener.StickyBottomNudgeListener
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.model.BottomNavBarStateHolder
import com.naviapp.home.model.HomeBottomNavBarData
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.utils.Constants
import com.naviapp.utils.Constants.BOTTOM_TABS
sealed interface HomeFooterEvents {
data object FabButtonOnClick : HomeFooterEvents
data object FabButtonOnLaunchEvent : HomeFooterEvents
data object SnackBarOnClick : HomeFooterEvents
data class BottomNudgeOnClick(val actionData: ActionData) : HomeFooterEvents
@@ -49,7 +41,6 @@ sealed interface HomeFooterEvents {
@Stable
data class HomeFooterStates(
val isFabButtonVisible: Boolean,
val showSnackBar: Boolean,
val bottomNavBarState: BottomNavBarStateHolder
)
@@ -57,27 +48,13 @@ data class HomeFooterStates(
fun handleHomeFooterEvent(
homeFooterEvents: HomeFooterEvents,
selectedTabId: String,
homeVM: HomeVM,
homePageActivity: HomePageActivity,
homeVM: () -> HomeViewModel,
inAppUpdateVM: InAppUpdateVM,
navController: NavHostController,
sharedVM: SharedVM,
dashboardSharedVM: DashboardSharedVM
) {
when (homeFooterEvents) {
is HomeFooterEvents.FabButtonOnClick -> {
handleFabOnClick(
selectedTabId = selectedTabId,
homeVM = homeVM,
homePageActivity = homePageActivity
)
}
is HomeFooterEvents.FabButtonOnLaunchEvent -> {
NaviTrackEvent.trackEventOnClickStream(
"NaviPay_fab_button_viewed",
mapOf(BOTTOM_TABS to selectedTabId, CARD_NAME to homeVM.getFabButtonCardId())
)
}
is HomeFooterEvents.BottomBarOnTabClick -> {
onTabClick(
selectedTabId = selectedTabId,
@@ -102,22 +79,11 @@ fun handleHomeFooterEvent(
inAppUpdateVM.inAppUpdateSnackBarClick()
inAppUpdateVM.registerAppUpdateSuccessAnalytics()
inAppUpdateVM.completeUpdate()
homeVM.updateHomeScreenSnackBarState(showSnackBar = false)
homeVM().sendEvent(HpEvents.UpdateSnackBarState(false))
}
}
}
fun handleFabOnClick(selectedTabId: String, homeVM: HomeVM, homePageActivity: HomePageActivity) {
NaviDeepLinkNavigator.navigate(
activity = homePageActivity,
ctaData = CtaData(url = "naviPay/NAVI_PAY_QR_SCANNER_SCREEN", needsResult = true)
)
NaviTrackEvent.trackEventOnClickStream(
"NaviPay_fab_button_clicked",
mapOf(BOTTOM_TABS to selectedTabId, CARD_NAME to homeVM.getFabButtonCardId())
)
}
private fun onTabClick(
selectedTabId: String,
navController: NavController,

View File

@@ -27,70 +27,34 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.common.alchemist.model.AlchemistCollapsingToolbar
import com.navi.common.extensions.conditional
import com.navi.naviwidgets.R as WidgetR
import com.naviapp.R
import com.naviapp.home.compose.extension.bottomShadow
import com.naviapp.home.compose.widgetfactory.HomeWidgetRenderer
import com.naviapp.home.viewmodel.HomeVM
@Composable
fun HomeTopBarRoot(
modifier: Modifier,
appBarHeight: Dp,
statusBarHeight: Dp,
homeVM: HomeVM,
topBarData: AlchemistCollapsingToolbar?,
homeScrollState: () -> ScrollState
) {
val isScrollingUp by remember { derivedStateOf { homeScrollState().value.dp < appBarHeight } }
val renderTopBar by homeVM.isReadyToRenderBackLayerAndTopAppBar.collectAsStateWithLifecycle()
HomeTopBar(
modifier = modifier,
statusBarHeight = statusBarHeight,
appBarHeight = appBarHeight,
isScrollingUp = { isScrollingUp },
renderTopBar = { renderTopBar },
topBarContent = {
topBarData?.toolBarNav?.uiTronResponse?.let {
HomeWidgetRenderer(
widgetData = it.data,
viewModel = homeVM,
composeView = it.parentComposeView,
homeScrollState = homeScrollState
)
}
}
)
}
@Composable
fun HomeTopBar(
modifier: Modifier,
appBarHeight: Dp,
statusBarHeight: Dp,
isScrollingUp: () -> Boolean,
renderTopBar: () -> Boolean,
topBarContent: @Composable () -> Unit
topBarContent: @Composable () -> Unit,
homeScrollState: () -> ScrollState
) {
val isScrollingUp by remember { derivedStateOf { homeScrollState().value.dp < appBarHeight } }
// Set the modifier for the top bar based on scrolling direction
val topBarModifier =
remember(isScrollingUp()) {
remember(isScrollingUp) {
modifier
.conditional(!isScrollingUp()) { requiredHeight(appBarHeight + 4.dp) }
.bottomShadow(showShadow = isScrollingUp().not(), elevation = 8f)
.conditional(!isScrollingUp) { requiredHeight(appBarHeight + 4.dp) }
.bottomShadow(showShadow = isScrollingUp.not(), elevation = 8f)
.drawBehind {
drawRect(color = if (isScrollingUp()) Color.Transparent else Color.White)
drawRect(color = if (isScrollingUp) Color.Transparent else Color.White)
}
}
// Render the top bar
Row(modifier = topBarModifier, verticalAlignment = Alignment.Top) {
Row(Modifier.padding(top = statusBarHeight)) {
if (renderTopBar()) topBarContent() else DefaultTopBar()
}
Row(Modifier.padding(top = statusBarHeight)) { topBarContent() }
}
}

View File

@@ -18,7 +18,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -48,16 +47,19 @@ import com.naviapp.home.compose.home.navigation.HomePageNavHost
import com.naviapp.home.compose.home.ui.dialog.HomeScreenDialog
import com.naviapp.home.compose.home.ui.footer.HomeFooterRoot
import com.naviapp.home.compose.home.ui.footer.utils.updateTabSelection
import com.naviapp.home.compose.home.utils.handleCtaActionEvents
import com.naviapp.home.compose.home.utils.onFetchHomeApiCall
import com.naviapp.home.compose.model.InitiatePaymentFromComposeData
import com.naviapp.home.compose.profile.ProfileScreen
import com.naviapp.home.compose.profile.ProfileScreenShimmer
import com.naviapp.home.model.HomeCtaTypes
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.ProfileVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.payment.viewmodel.PaymentVM
import kotlinx.coroutines.launch
@Composable
fun HomePageActivityMainScreen(
@@ -66,7 +68,7 @@ fun HomePageActivityMainScreen(
dashboardSharedVM: DashboardSharedVM,
inAppUpdateVM: InAppUpdateVM,
profileVM: ProfileVM,
homeVM: HomeVM,
homeVM: HomeViewModel,
paymentVM: PaymentVM,
notificationVM: NotificationVM,
sharedVM: SharedVM,
@@ -75,24 +77,52 @@ fun HomePageActivityMainScreen(
initiatePayment: (paymentData: InitiatePaymentFromComposeData) -> Unit,
hopper: Hopper
) {
val hpStates by homeVM.state.collectAsStateWithLifecycle()
val navController = rememberNavController()
val selectedTabId by sharedVM.selectedTabId.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
homeVM.effect.collect { hpEffect ->
homePageActivity.homeEffectHandler
.get()
.handleEffects(
effects = hpEffect,
onPaymentInitiated = initiatePayment,
onFetchHomeApiCall = {
onFetchHomeApiCall(homeVM, homePageActivity, paymentVM)
},
homeVM = homeVM,
handleCtaAction = {
handleCtaActionEvents(
it,
homeVM,
hpStates,
sharedVM,
homePageActivity,
paymentVM
)
}
)
}
}
InitActionsHandler(
viewModel = homeVM,
activity = homePageActivity,
naviAnalyticsEventTracker = naviHomeAnalytics,
isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing()
)
LaunchedEffect(key1 = selectedTabId) {
LaunchedEffect(selectedTabId) {
onTabSelected.invoke(selectedTabId)
homeVM.checkForOnHiddenChanged(
selectedTabId = selectedTabId,
bottomNavBarVM = bottomNavBarVM,
inAppUpdateVM = inAppUpdateVM
homeVM.sendEvent(
HpEvents.HandleAppUpdateNudgeVisibility(
selectedTab = selectedTabId,
bottomNavBarVM = bottomNavBarVM,
inAppUpdateVM = inAppUpdateVM
)
)
}
LaunchedEffect(key1 = Unit) {
LaunchedEffect(Unit) {
bottomNavBarVM.updateTabSelection.collect { updateTabSelection ->
bottomNavBarVM.clearUpdateTabSelectionReplay()
updateTabSelection(
@@ -104,10 +134,6 @@ fun HomePageActivityMainScreen(
)
}
}
LaunchedEffect(key1 = Unit) {
homeVM.initiatePaymentFromComposeScreen.collect { initiatePayment.invoke(it) }
}
InitHomeActivityScreen(
bottomNavBarVM = bottomNavBarVM,
inAppUpdateVM = inAppUpdateVM,
@@ -115,13 +141,15 @@ fun HomePageActivityMainScreen(
naviHomeAnalytics = naviHomeAnalytics,
selectedTabId = selectedTabId,
profileVM = profileVM,
homeVM = homeVM,
hpStates = { hpStates },
paymentVM = paymentVM,
homeVM = { homeVM },
dashboardSharedVM = dashboardSharedVM,
notificationVM = notificationVM,
sharedVM = sharedVM,
navController = navController,
hopper = hopper
hopper = hopper,
onHomeScreenEvent = { homeVM.sendEvent(it) }
)
}
@@ -133,30 +161,31 @@ private fun InitHomeActivityScreen(
inAppUpdateVM: InAppUpdateVM,
naviHomeAnalytics: NaviAnalytics.Home,
selectedTabId: String,
homeVM: () -> HomeViewModel,
profileVM: ProfileVM,
homeVM: HomeVM,
hpStates: () -> HpStates,
paymentVM: PaymentVM,
dashboardSharedVM: DashboardSharedVM,
notificationVM: NotificationVM,
sharedVM: SharedVM,
navController: NavHostController,
hopper: Hopper
hopper: Hopper,
onHomeScreenEvent: (event: HpEvents) -> Unit = {}
) {
val drawerState = rememberNaviDrawerState(NaviDrawerValue.Closed)
val investmentListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
ResetProfileScroll(homeVM, profileVM) { drawerState }
ResetProfileScroll(
profileVM = profileVM,
drawerState = { drawerState },
onHomeScreenEvent = onHomeScreenEvent
)
LaunchedEffect(key1 = Unit) {
homeVM.profileDrawerState.collect { openDrawer ->
coroutineScope.launch {
if (openDrawer) {
if (drawerState.isClosed) drawerState.open()
} else {
if (drawerState.isOpen) drawerState.close()
}
}
LaunchedEffect(hpStates().profileDrawerState) {
if (hpStates().profileDrawerState) {
if (drawerState.isClosed) drawerState.open()
} else {
if (drawerState.isOpen) drawerState.close()
}
}
@@ -164,16 +193,14 @@ private fun InitHomeActivityScreen(
handleBottomSheetAction(action = action, activity = homePageActivity)
}
HomeScreenDialog(homeVM)
HomeScreenDialog(homeVM = homeVM, hpStates = hpStates)
NaviModalNavigationDrawer(
drawerState = { drawerState },
modifier = Modifier.fillMaxSize(),
drawerContent = {
// Will show shimmer until the home screen is rendered
val isReadyToRenderProfileScreen by
homeVM.isReadyToRenderProfileScreen.collectAsStateWithLifecycle()
if (isReadyToRenderProfileScreen) {
if (hpStates().renderingFirstTime.not()) {
ProfileScreen(
profileVM = profileVM,
drawerState = { drawerState },
@@ -198,7 +225,8 @@ private fun InitHomeActivityScreen(
sharedVM = sharedVM,
naviHomeAnalytics = naviHomeAnalytics,
selectedTabId = selectedTabId,
hopper = hopper
hopper = hopper,
hpStates = hpStates
)
}
}
@@ -238,13 +266,13 @@ private fun handleBottomSheetAction(action: UiTronAction?, activity: HomePageAct
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun ResetProfileScroll(
homeVM: HomeVM,
profileVM: ProfileVM,
drawerState: () -> DrawerState
drawerState: () -> DrawerState,
onHomeScreenEvent: (event: HpEvents) -> Unit
) {
LaunchedEffect(drawerState().currentValue) {
val isDrawerOpen = drawerState().currentValue == NaviDrawerValue.Open
homeVM.isProfileDrawerOpen = isDrawerOpen
onHomeScreenEvent(HpEvents.UpdateProfileDrawerState(isDrawerOpen))
if (!isDrawerOpen) {
profileVM.resetProfileScrollToTop(reset = true)
}
@@ -258,7 +286,8 @@ private fun ScreenContent(
bottomNavBarVM: BottomNavBarVM,
inAppUpdateVM: InAppUpdateVM,
investmentListState: () -> LazyListState,
homeVM: HomeVM,
homeVM: () -> HomeViewModel,
hpStates: () -> HpStates,
dashboardSharedVM: DashboardSharedVM,
paymentVM: PaymentVM,
notificationVM: NotificationVM,
@@ -273,7 +302,8 @@ private fun ScreenContent(
homePageActivity = homePageActivity,
navController = navController,
dashboardSharedVM = dashboardSharedVM,
homeVM = homeVM,
hpStates = hpStates,
homeViewModel = homeVM,
notificationVM = notificationVM,
sharedVM = sharedVM,
paymentVM = paymentVM,
@@ -286,8 +316,8 @@ private fun ScreenContent(
HomeFooterRoot(
modifier = Modifier.align(Alignment.BottomCenter),
homeVM = homeVM,
hpStates = hpStates,
inAppUpdateVM = inAppUpdateVM,
homePageActivity = homePageActivity,
bottomNavBarVM = bottomNavBarVM,
dashboardSharedVM = dashboardSharedVM,
sharedVM = sharedVM,

View File

@@ -8,11 +8,11 @@
package com.naviapp.home.compose.home.ui.screen
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -22,7 +22,6 @@ import androidx.compose.material.BackdropValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.rememberBackdropScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -31,28 +30,28 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.ui.errorview.FullScreenErrorComposeView
import com.navi.common.utils.getStatusBarHeight
import com.navi.uitron.model.UiTronResponse
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.home.ui.content.BackLayerContent
import com.naviapp.home.compose.home.ui.content.FrontLayerContent
import com.naviapp.home.compose.home.ui.header.HomeTopBarRoot
import com.naviapp.home.compose.home.ui.header.DefaultTopBar
import com.naviapp.home.compose.home.ui.header.HomeTopBar
import com.naviapp.home.compose.home.utils.InitHomeScreenComponents
import com.naviapp.home.compose.home.utils.TopNaviMiddlePillAnimation
import com.naviapp.home.compose.home.utils.handleHomeFooterRendering
import com.naviapp.home.compose.home.utils.retryHomePageApi
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.compose.widgetfactory.HomeWidgetRenderer
import com.naviapp.home.listener.StickyBottomNudgeListener
import com.naviapp.home.ui.state.HomeScreenState
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.payment.viewmodel.PaymentVM
import com.naviapp.utils.Constants.HomePageConstants.HOME_APP_BAR_HEIGHT
import com.naviapp.utils.Constants.HomePageConstants.HOME_BACK_LAYER_BANNER_HEIGHT
import com.naviapp.utils.Constants.HomePageConstants.HOME_FRONT_LAYER_ELEVATION
@@ -62,15 +61,15 @@ import com.naviapp.utils.Constants.HomePageConstants.HOME_SHAPE_CURVATURE
fun HomeScreen(
activity: HomePageActivity,
dashboardSharedVM: DashboardSharedVM,
homeVM: HomeVM,
paymentVM: PaymentVM,
homeVM: () -> HomeViewModel,
hpStates: HpStates,
onHomeScreenEvent: (event: HpEvents) -> Unit,
sharedVM: SharedVM,
notificationVM: NotificationVM,
naviAnalyticsEventTracker: NaviAnalytics.Home,
inAppUpdateVM: InAppUpdateVM,
callBackToActivityScreen: (callback: HomeScreenCallbackListener) -> Unit
) {
val homeScreenData by homeVM.homeScreenState.collectAsStateWithLifecycle()
val stickyBottomNudgeListener: StickyBottomNudgeListener = remember { activity }
val homeScrollState = rememberScrollState()
@@ -78,7 +77,6 @@ fun HomeScreen(
activity = activity,
dashboardSharedVM = dashboardSharedVM,
homeVM = homeVM,
paymentVM = paymentVM,
sharedVM = sharedVM,
notificationVM = notificationVM,
naviAnalyticsEventTracker = naviAnalyticsEventTracker,
@@ -88,35 +86,51 @@ fun HomeScreen(
homeScrollState = { homeScrollState },
)
when (homeScreenData) {
HomeScreenState.Loading -> {
HomeScreenScaffoldRoot(
screenDefinition = AlchemistScreenDefinition(),
homeVM = homeVM,
homeScrollState = { homeScrollState },
) {
true
when (hpStates.isError.not()) {
true -> {
if (hpStates.isLoading) {
onHomeScreenEvent(HpEvents.UpdateShadowOnFrontLayer(true))
HomeScreenScaffoldRoot(
homeWidgetRenderer = {
HomeWidgetRenderer(
widgetData = it?.data,
viewModel = homeVM(),
composeView = it?.parentComposeView,
homeScrollState = { homeScrollState }
)
},
homeScrollState = { homeScrollState },
hpStates = hpStates,
homeVM = homeVM
)
} else {
onHomeScreenEvent(HpEvents.UpdateShadowOnFrontLayer(false))
hpStates.screenDefinition?.let {
HomeScreenScaffoldRoot(
homeWidgetRenderer = { widget ->
HomeWidgetRenderer(
widgetData = widget?.data,
viewModel = homeVM(),
composeView = widget?.parentComposeView,
homeScrollState = { homeScrollState }
)
},
homeScrollState = { homeScrollState },
hpStates = hpStates,
homeVM = homeVM
)
handleHomeFooterRendering(
screenDefinition = it,
sharedVM = sharedVM,
stickyBottomNudgeListener = stickyBottomNudgeListener
)
}
}
}
is HomeScreenState.Success -> {
val screenDefinition = (homeScreenData as HomeScreenState.Success).data
HomeScreenScaffoldRoot(
screenDefinition = screenDefinition,
homeVM = homeVM,
homeScrollState = { homeScrollState }
) {
false
}
handleHomeFooterRendering(
screenDefinition = screenDefinition,
sharedVM = sharedVM,
stickyBottomNudgeListener = stickyBottomNudgeListener
)
}
is HomeScreenState.Error -> {
false -> {
activity.hideLoader()
FullScreenErrorComposeView(
error = (homeScreenData as HomeScreenState.Error).error,
error = hpStates.error,
onRetryClick = {
retryHomePageApi(
homeVM = homeVM,
@@ -131,12 +145,11 @@ fun HomeScreen(
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun HomeScreenScaffoldRoot(
screenDefinition: AlchemistScreenDefinition,
homeVM: HomeVM,
hpStates: HpStates,
homeScrollState: () -> ScrollState,
showShadowOnFrontLayer: () -> Boolean,
homeVM: () -> HomeViewModel,
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit
) {
val density = LocalDensity.current
val statusBarHeight = remember { with(density) { getStatusBarHeight().toDp() } }
val appBarHeight = remember { HOME_APP_BAR_HEIGHT + statusBarHeight }
@@ -144,38 +157,46 @@ private fun HomeScreenScaffoldRoot(
val frontLayerShape = remember {
RoundedCornerShape(topStart = HOME_SHAPE_CURVATURE, topEnd = HOME_SHAPE_CURVATURE)
}
val homePageState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
val backdropScaffoldState =
rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
HomeBackdropScaffold(
state = { homePageState },
state = { backdropScaffoldState },
appBarHeight = appBarHeight,
backLayerHeight = backLayerHeight,
frontLayerShape = frontLayerShape,
showShadowOnFrontLayer = showShadowOnFrontLayer,
showShadowOnFrontLayer = hpStates.showShadowOnFrontLayer,
appBar = {
HomeTopBarRoot(
HomeTopBar(
modifier = Modifier.align(Alignment.TopCenter),
appBarHeight = appBarHeight,
statusBarHeight = statusBarHeight,
topBarData = screenDefinition.screenStructure?.collapsingToolbar,
homeVM = homeVM,
topBarContent = {
(hpStates.screenDefinition)
?.screenStructure
?.collapsingToolbar
?.toolBarNav
?.uiTronResponse
?.let { homeWidgetRenderer(it) } ?: DefaultTopBar()
},
homeScrollState = homeScrollState
)
},
backLayer = {
BackLayerContent(
modifier = Modifier.requiredHeight(backLayerHeight + HOME_SHAPE_CURVATURE),
backLayerData = screenDefinition.screenStructure?.collapsingToolbar,
homeVM = homeVM,
homeScrollState = homeScrollState
)
Column(Modifier.requiredHeight(backLayerHeight + HOME_SHAPE_CURVATURE).fillMaxWidth()) {
(hpStates.screenDefinition)
?.screenStructure
?.collapsingToolbar
?.collapsingTopNav
?.uiTronResponse
?.let { homeWidgetRenderer(it) }
}
},
frontLayer = {
FrontLayerContent(
modifier = Modifier.fillMaxHeight().background(Color.White, frontLayerShape),
isShimmerVisible =
screenDefinition.screenStructure?.content?.widgets.isNullOrEmpty(),
homeVM = homeVM,
homeVM = homeVM(),
frontLayerShape = frontLayerShape,
hpStates = { hpStates },
homeScrollState = homeScrollState
)
}
@@ -185,7 +206,7 @@ private fun HomeScreenScaffoldRoot(
appBarHeight = appBarHeight,
backLayerHeight = backLayerHeight - HOME_SHAPE_CURVATURE,
homeVM = homeVM,
naviHomeScaffoldState = { homePageState }
backdropState = { backdropScaffoldState }
)
}
@@ -196,7 +217,7 @@ fun HomeBackdropScaffold(
appBarHeight: Dp,
backLayerHeight: Dp,
frontLayerShape: Shape,
showShadowOnFrontLayer: () -> Boolean,
showShadowOnFrontLayer: Boolean,
appBar: @Composable BoxScope.() -> Unit,
backLayer: @Composable () -> Unit,
frontLayer: @Composable () -> Unit
@@ -212,8 +233,7 @@ fun HomeBackdropScaffold(
frontLayerShape = frontLayerShape,
frontLayerScrimColor = Color.Unspecified,
backLayerContent = backLayer,
frontLayerElevation =
if (showShadowOnFrontLayer()) HOME_FRONT_LAYER_ELEVATION else 0.dp,
frontLayerElevation = if (showShadowOnFrontLayer) HOME_FRONT_LAYER_ELEVATION else 0.dp,
frontLayerBackgroundColor = Color.White,
backLayerBackgroundColor = Color.White,
frontLayerContent = frontLayer,

View File

@@ -7,6 +7,7 @@
package com.naviapp.home.compose.home.utils
import com.navi.common.utils.TemporaryStorageHelper
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.compose.model.CtaActionEvent
@@ -14,13 +15,50 @@ import com.naviapp.home.model.HpBottomSheetContent
import com.naviapp.home.model.HpBottomSheetRenderType
import com.naviapp.home.model.HpBottomSheetState
import com.naviapp.home.model.HpDialogStateHolder
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.payment.viewmodel.PaymentVM
import com.naviapp.utils.copyText
fun handleCtaAction(
fun handleCtaActionEvents(
it: CtaActionEvent,
homeVM: HomeViewModel,
hpStates: HpStates,
sharedVM: SharedVM,
homePageActivity: HomePageActivity,
paymentVM: PaymentVM
) {
handleCtaAction(
event = it,
homeVM = { homeVM },
hpStates = { hpStates },
sharedVM = sharedVM,
activity = homePageActivity
) { callback ->
when (callback) {
HomeScreenCallbackListener.ApiCallingWithCondition -> {
TemporaryStorageHelper.updateViewVisibility(TemporaryStorageHelper.HOME, false)
homeVM.loadHomeElements(
activity = homePageActivity,
isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing()
)
}
is HomeScreenCallbackListener.InitiatePaymentOnHomePage -> {
homeVM.sendEffect(homeVM.coroutineScope) {
HpEffects.InitiatePayment(callback.paymentData)
}
}
}
}
}
private fun handleCtaAction(
event: CtaActionEvent,
homeVM: HomeVM,
homeVM: () -> HomeViewModel,
hpStates: () -> HpStates,
sharedVM: SharedVM,
activity: HomePageActivity,
callBackToActivityScreen: (callback: HomeScreenCallbackListener) -> Unit
@@ -28,19 +66,20 @@ fun handleCtaAction(
when (event) {
is CtaActionEvent.RedirectToCta -> {
event.ctaData?.let {
homeVM.handleCtaData(
naviClickAction = it,
activity = activity,
callBackToActivityScreen = callBackToActivityScreen,
)
homeVM()
.handleCtaData(
naviClickAction = it,
activity = activity,
callBackToActivityScreen = callBackToActivityScreen,
)
}
}
is CtaActionEvent.CopyToClipboard -> {
copyText(context = activity, text = event.value)
}
is CtaActionEvent.ShowBottomSheet -> {
homeVM
.getHomeScreenData()
hpStates()
.screenDefinition
?.screenStructure
?.bottomSheets
?.firstOrNull { event.bottomSheetId == it.screenId }
@@ -57,10 +96,16 @@ fun handleCtaAction(
}
is CtaActionEvent.ShowDialog -> {
val dialogData =
homeVM.getHomeScreenData()?.screenStructure?.dialogs?.find {
hpStates().screenDefinition?.screenStructure?.dialogs?.find {
it.dialogId == event.dialogId
}
homeVM.updateDialogState(HpDialogStateHolder.HpDialogState.Visible, dialogData)
homeVM()
.sendEvent(
HpEvents.UpdateHpDialogStateHolder(
HpDialogStateHolder.HpDialogState.Visible,
dialogData
)
)
}
}
}

View File

@@ -7,46 +7,54 @@
package com.naviapp.home.compose.home.utils
//noinspection UsingMaterialAndMaterial3Libraries
import androidx.compose.material.BackdropScaffoldState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import com.navi.uitron.utils.toPx
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.utils.Constants.HomePageConstants.SCROLL_FADE
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TopNaviMiddlePillAnimation(
appBarHeight: Dp,
backLayerHeight: Dp,
homeVM: HomeVM,
naviHomeScaffoldState: () -> BackdropScaffoldState
homeVM: () -> HomeViewModel,
backdropState: () -> BackdropScaffoldState
) {
val offsetValue by homeVM.topNavOffsetValue.collectAsStateWithLifecycle()
LaunchedEffect(naviHomeScaffoldState().currentValue) {
if (naviHomeScaffoldState().isRevealed) {
homeVM.triggerCollapsedEvent()
}
}
LaunchedEffect(naviHomeScaffoldState().progress) {
homeVM.updateOffsetValue(naviHomeScaffoldState().offset.value)
}
UpdateTopNavOffset(homeVM, backdropState)
val normalizedOffset =
normalizeOffset(
offsetValue,
offset = homeVM().backdropScaffoldProgress.collectAsStateWithLifecycle().value,
minOffset = appBarHeight.toPx(),
maxOffset = backLayerHeight.toPx()
)
homeVM.handle[SCROLL_FADE] = (1 - normalizedOffset).coerceIn(0f, 1f)
homeVM().handle[SCROLL_FADE] = (1 - normalizedOffset).coerceIn(0f, 1f)
}
fun normalizeOffset(offset: Float, minOffset: Float, maxOffset: Float): Float {
@Composable
@OptIn(ExperimentalMaterialApi::class)
private fun UpdateTopNavOffset(
homeVM: () -> HomeViewModel,
backdropState: () -> BackdropScaffoldState
) {
LaunchedEffect(backdropState().progress) {
homeVM().viewModelScope.launch {
homeVM().updateBackdropScaffoldProgress(backdropState().offset.value)
}
}
}
private fun normalizeOffset(offset: Float, minOffset: Float, maxOffset: Float): Float {
val offsetRange = maxOffset - minOffset
if (offsetRange <= 0) {
error("Invalid offset range. minOffset must be less than maxOffset.")

View File

@@ -13,49 +13,63 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.navi.alfred.AlfredManager
import com.navi.base.sharedpref.PreferenceManager
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.common.alchemist.model.WidgetRenderState
import com.navi.common.utils.CommonUtils.getDynamicModulePrefix
import com.navi.common.utils.getDensityName
import com.navi.common.utils.getInstalledDynamicModulesCommaSeparated
import com.navi.common.utils.getNetworkType
import com.navi.common.utils.installDynamicModules
import com.navi.common.utils.isDynamicModuleInstalled
import com.navi.naviwidgets.utils.CURRENT_VERSION_IN_STORE
import com.navi.uitron.model.UiTronResponse
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.home.ui.footer.utils.handleAppUpdateNudge
import com.naviapp.home.compose.home.ui.footer.utils.handleBottomBarData
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.listener.StickyBottomNudgeListener
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.payment.viewmodel.PaymentVM
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@Composable
fun InitLifecycleListener(
homeVM: HomeVM,
homeVM: () -> HomeViewModel,
activity: HomePageActivity,
callBackToActivityScreen: (callback: HomeScreenCallbackListener) -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner) {
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> {
AlfredManager.setCurrentScreenName(screenName = activity.screenName)
homeVM.updateUPIVpaObserver()
homeVM()
.observeUPIVpa(
onAction = {
homeVM().sendEffect(CoroutineScope(Dispatchers.Default)) {
HpEffects.OnUitronAction(it)
}
},
onActionData = {
homeVM().sendEffect(CoroutineScope(Dispatchers.Default)) {
HpEffects.OnActionData(it)
}
}
)
}
Lifecycle.Event.ON_RESUME -> {
if (homeVM.shouldMakeAPICall()) {
homeVM.triggerHideBalanceAction()
if (homeVM().shouldRefreshHomeApi) {
homeVM().sendEffect(homeVM().coroutineScope) { HpEffects.OnRenderActions }
callBackToActivityScreen.invoke(
HomeScreenCallbackListener.ApiCallingWithCondition
)
}
homeVM.updateShouldMakeAPICallState(shouldRefresh = true)
homeVM.updateUpiLiteBalance()
homeVM.updateUPILiteBalanceV2()
homeVM.updateUPIVpa()
homeVM().setHomeApiRefreshFlag(true)
homeVM().handleUpiAdaptations()
AlfredManager.setCurrentScreenName(screenName = activity.screenName)
}
else -> {}
@@ -67,17 +81,11 @@ fun InitLifecycleListener(
}
fun retryHomePageApi(
homeVM: HomeVM,
homeVM: () -> HomeViewModel,
activity: HomePageActivity,
) {
homeVM.fetchHomeItems(
density = getDensityName(context = activity).orEmpty(),
connectivityType = getNetworkType(context = activity),
availableAppVersionCode = PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
installedModules = getInstalledDynamicModulesCommaSeparated(),
clearReferralPopupPreferences = false
)
homeVM.resetHomeScreenDataStateToLoading()
homeVM().loadHomeElements(activity)
homeVM().sendEvent(HpEvents.TriggerLoadingState)
}
fun handleHomeFooterRendering(
@@ -89,6 +97,18 @@ fun handleHomeFooterRendering(
handleBottomBarData(screenDefinition, sharedVM)
}
fun onFetchHomeApiCall(
homeVM: HomeViewModel,
homePageActivity: HomePageActivity,
paymentVM: PaymentVM
) {
homeVM.naviAnalyticsEventTracker.onHomePageApiCalledFromUiTronAction()
homeVM.loadHomeElements(
activity = homePageActivity,
isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing()
)
}
fun checkForModulesInstall(
dynamicModulesList: List<String?>,
screenName: String,
@@ -114,3 +134,41 @@ fun checkForModulesInstall(
naviAnalyticsEventTracker.moduleAlreadyInstalled(module)
}
}
fun updateScreenContent(
state: HpStates,
newWidgets: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
currentWidgets: List<AlchemistWidgetModelDefinition<UiTronResponse>>
): List<AlchemistWidgetModelDefinition<UiTronResponse>> {
val updatedList = mutableListOf<AlchemistWidgetModelDefinition<UiTronResponse>>()
val seenWidgetIds = mutableSetOf<String>()
newWidgets.forEach { widget ->
val widgetId = widget.widgetId
if (!widgetId.isNullOrBlank()) {
seenWidgetIds.add(widgetId)
val uiState =
if (currentWidgets.any { it.widgetId == widgetId } || state.renderingFirstTime) {
WidgetRenderState.VISIBLE
} else {
WidgetRenderState.NEWLY_ADDED
}
updatedList.add(widget.copy(widgetRenderState = uiState))
}
}
currentWidgets.forEachIndexed { index, widget ->
val widgetId = widget.widgetId
if (!widgetId.isNullOrBlank() && widgetId !in seenWidgetIds) {
updatedList.getOrNull(index)?.let {
updatedList.add(
index,
widget.copy(widgetRenderState = WidgetRenderState.NOT_VISIBLE)
)
} ?: updatedList.add(widget.copy(widgetRenderState = WidgetRenderState.NOT_VISIBLE))
updatedList.add(widget.copy(widgetRenderState = WidgetRenderState.NOT_VISIBLE))
}
}
return updatedList
}

View File

@@ -12,7 +12,6 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.navi.common.commoncomposables.utils.ScrollToTopListener
import com.navi.common.utils.TemporaryStorageHelper
import com.navi.insurance.util.observeNonNull
import com.navi.naviwidgets.utils.toCtaData
import com.naviapp.R
@@ -24,18 +23,19 @@ import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.listener.StickyBottomNudgeListener
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.NotificationVM
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.payment.viewmodel.PaymentVM
import com.naviapp.utils.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@Composable
fun InitHomeScreenComponents(
activity: HomePageActivity,
dashboardSharedVM: DashboardSharedVM,
homeVM: HomeVM,
paymentVM: PaymentVM,
homeVM: () -> HomeViewModel,
sharedVM: SharedVM,
notificationVM: NotificationVM,
naviAnalyticsEventTracker: NaviAnalytics.Home,
@@ -53,20 +53,6 @@ fun InitHomeScreenComponents(
LaunchedEffect(Unit) { naviAnalyticsEventTracker.onHomePageLand() }
LaunchedEffect(key1 = Unit) {
homeVM.shouldFetchHomeApi.collect { shouldFetchHomeApi ->
if (shouldFetchHomeApi) {
naviAnalyticsEventTracker.onHomePageApiCalledFromUiTronAction()
homeVM.fetchCards(
showLoader = true,
naviAnalyticsEventTracker = naviAnalyticsEventTracker,
activity = activity,
isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing(),
)
}
}
}
LaunchedEffect(key1 = Unit) {
dashboardSharedVM.ctaData.collect { actionData ->
if (actionData.url == NaviDeepLinkNavigator.WEB_APP_UPDATE) {
@@ -79,17 +65,12 @@ fun InitHomeScreenComponents(
}
}
LaunchedEffect(key1 = Unit) {
homeVM.cachedResponse.collect {
TemporaryStorageHelper.updateApiTs(TemporaryStorageHelper.HOME)
activity.hideLoader()
}
}
LaunchedEffect(key1 = Unit) {
sharedVM.uiTronActionHandler.collect { uiTronActionHandler ->
uiTronActionHandler?.let {
homeVM.updateHandleActionsFromJson(it)
homeVM().sendEffect(CoroutineScope(Dispatchers.Default)) {
HpEffects.OnActionsFromJson(it)
}
sharedVM.updateUiTronAction(uiTronActionHandler = null)
}
}
@@ -104,13 +85,9 @@ fun InitHomeScreenComponents(
LaunchedEffect(key1 = Unit) {
notificationVM.unReadNotificationCount.collect { count ->
if (count > 0) naviAnalyticsEventTracker.onInAppNotificationsCountUpdate(count)
homeVM.updateNotificationCount(count)
}
}
LaunchedEffect(key1 = Unit) {
homeVM.ctaActionEvent.collect { event ->
handleCtaAction(event, homeVM, sharedVM, activity, callBackToActivityScreen)
homeVM().sendEffect(CoroutineScope(Dispatchers.Default)) {
HpEffects.OnNotificationUpdatedCount(count)
}
}
}

View File

@@ -0,0 +1,17 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.compose.model
import com.navi.uitron.model.UiTronResponse
import com.naviapp.home.model.WidgetUiState
data class FrontLayerWidget(
val id: String,
val state: WidgetUiState = WidgetUiState.VISIBLE,
val data: UiTronResponse? = null
)

View File

@@ -35,7 +35,7 @@ import com.navi.uitron.utils.setWidth
import com.navi.uitron.utils.setWidthRange
import com.navi.uitron.viewmodel.UiTronViewModel
import com.naviapp.home.compose.uiTron.model.viewProperties.RotatingViewProperty
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.utils.Constants.HomeCustomUitronWidgetConstants.ROTATING_VIEW_ANIMATION
class RotatingViewRenderer(
@@ -84,7 +84,7 @@ class RotatingViewRenderer(
RotatingView(
modifier = viewModifier,
indexValue = {
(uiTronViewModel as HomeVM)
(uiTronViewModel as HomeViewModel)
.rotatingViewHelper
.get()
.currentIndexToDisplay(

View File

@@ -13,13 +13,13 @@ import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.ui.UiTronView
import com.navi.uitron.render.UiTronRenderer
import com.naviapp.home.compose.uiTron.renderer.HomeCustomUiTronRenderer
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.HomeViewModel
@Composable
fun HomeWidgetRenderer(
widgetData: MutableMap<String, UiTronData?>?,
composeView: List<UiTronView>?,
viewModel: HomeVM,
viewModel: HomeViewModel,
homeScrollState: () -> ScrollState
) {
UiTronRenderer(

View File

@@ -0,0 +1,268 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.reducer
import androidx.compose.runtime.Immutable
import com.navi.common.alchemist.model.AlchemistDialogStructure
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.alchemist.model.AlchemistScreenStructure
import com.navi.common.alchemist.model.AlchemistWidgetGroup
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.common.alchemist.model.WidgetRenderState
import com.navi.common.basemvi.BaseReducer
import com.navi.common.basemvi.UiEffect
import com.navi.common.basemvi.UiEvent
import com.navi.common.basemvi.UiState
import com.navi.common.managers.PermissionsManager
import com.navi.common.model.common.AppUpdateData
import com.navi.common.network.models.ErrorMessage
import com.navi.common.network.models.GenericErrorResponse
import com.navi.common.utils.Constants.APP_UPDATE_DATA
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.model.data.UiTronAction
import com.navi.uitron.model.data.UiTronActionData
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.model.UiTronActionHandler
import com.naviapp.common.viewmodel.BottomNavBarVM
import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.home.compose.home.utils.updateScreenContent
import com.naviapp.home.compose.model.BottomStickyNudgeState
import com.naviapp.home.compose.model.CtaActionEvent
import com.naviapp.home.compose.model.InitiatePaymentFromComposeData
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.model.HpDialogStateHolder
import com.naviapp.home.model.HpDialogStateHolder.HpDialogState
import com.naviapp.models.response.HomeFeatureResponse
class HomeReducer : BaseReducer<HpStates, HpEvents, HpEffects> {
override fun reduce(previousState: HpStates, event: HpEvents): HpStates {
return when (event) {
is HpEvents.UpdateHpDialogStateHolder -> {
previousState.copy(
hpDialogStateHolder = HpDialogStateHolder(event.state, event.content)
)
}
is HpEvents.UpdateLastSelectedTab -> {
previousState.copy(lastSelectedTab = event.tab)
}
is HpEvents.UpdateProfileDrawerState -> {
previousState.copy(profileDrawerState = event.state)
}
is HpEvents.OnProfileIconClicked -> {
previousState.copy(profileDrawerState = true)
}
is HpEvents.UpdateSnackBarState -> {
previousState.copy(homeScreenSnackBarState = event.show)
}
is HpEvents.UpdateCurrentLoadedFragmentName -> {
previousState.copy(currentLoadedFragmentScreenName = event.screenName)
}
is HpEvents.TriggerLoadingState -> {
previousState.copy(isLoading = true, isError = false, error = null)
}
is HpEvents.UpdateError -> {
previousState.copy(error = event.error)
}
is HpEvents.FirstLoadCompleted -> {
previousState.copy(renderingFirstTime = false)
}
is HpEvents.UpdateShadowOnFrontLayer -> {
previousState.copy(showShadowOnFrontLayer = event.showShadowOnFrontLayer)
}
is HpEvents.UpdateHpFeatures -> {
previousState.copy(homeFeatures = event.homeFeatures)
}
is HpEvents.TriggerErrorState -> {
previousState.copy(isError = true)
}
is HpEvents.HandleAppUpdateNudgeVisibility -> {
if (previousState.lastSelectedTab != event.selectedTab) {
val isHomeTab = event.selectedTab == BottomBarTabType.HOME.name
val appUpdateData =
previousState.screenDefinition
?.screenStructure
?.footer
?.widgets
?.firstOrNull { it.widgetData?.data?.get(APP_UPDATE_DATA) != null }
?.widgetData
?.data
?.get(APP_UPDATE_DATA) as? AppUpdateData
appUpdateData?.let {
event.bottomNavBarVM.setBottomNudge(
isHomeTab && event.inAppUpdateVM.showAppUpdateStrip.value != false,
BottomStickyNudgeState.AppUpdateNudgeState(it)
)
}
previousState.copy(lastSelectedTab = event.selectedTab)
} else previousState
}
is HpEvents.AddScreenContent -> {
val newList =
(previousState.screenDefinition?.screenStructure?.content?.widgets
?: emptyList()) + event.content
previousState.copy(
screenDefinition =
AlchemistScreenDefinition(
screenStructure =
AlchemistScreenStructure(
content = AlchemistWidgetGroup(widgets = newList)
)
)
)
}
is HpEvents.UpdateScreen -> {
val newWidgets = event.data.screenStructure?.content?.widgets ?: emptyList()
val oldWidgets =
previousState.screenDefinition?.screenStructure?.content?.widgets ?: emptyList()
val updatedList = updateScreenContent(previousState, newWidgets, oldWidgets)
previousState.copy(
screenDefinition =
event.data.copy(
screenStructure =
event.data.screenStructure?.copy(
content =
event.data.screenStructure
?.content
?.copy(widgets = updatedList)
)
)
)
}
is HpEvents.UpdateScreenContentWidgetRenderState -> {
val newContent = previousState.screenDefinition?.screenStructure?.content?.widgets
newContent?.map {
if (it.widgetId == event.id) {
it.copy(widgetRenderState = event.state)
} else {
it
}
}
previousState.copy(
screenDefinition =
previousState.screenDefinition?.copy(
screenStructure =
previousState.screenDefinition.screenStructure?.copy(
content =
previousState.screenDefinition.screenStructure
?.content
?.copy(widgets = newContent)
)
)
)
}
is HpEvents.ClearScreenContent -> {
previousState.copy(screenDefinition = null)
}
is HpEvents.PrioritySectionProcessed -> {
previousState.copy(processPrioritySection = false)
}
}
}
}
@Immutable
sealed interface HpEvents : UiEvent {
data class UpdateHpDialogStateHolder(
val state: HpDialogState,
val content: AlchemistDialogStructure? = null
) : HpEvents
data class UpdateLastSelectedTab(val tab: String) : HpEvents
data class UpdateProfileDrawerState(val state: Boolean) : HpEvents
data object OnProfileIconClicked : HpEvents
data class UpdateSnackBarState(val show: Boolean) : HpEvents
data class UpdateCurrentLoadedFragmentName(val screenName: String) : HpEvents
data object TriggerLoadingState : HpEvents
data class UpdateError(val error: GenericErrorResponse? = null) : HpEvents
data object FirstLoadCompleted : HpEvents
data class UpdateShadowOnFrontLayer(val showShadowOnFrontLayer: Boolean) : HpEvents
data class UpdateHpFeatures(val homeFeatures: HomeFeatureResponse?) : HpEvents
data object TriggerErrorState : HpEvents
data class HandleAppUpdateNudgeVisibility(
val selectedTab: String,
val bottomNavBarVM: BottomNavBarVM,
val inAppUpdateVM: InAppUpdateVM
) : HpEvents
data class AddScreenContent(val content: List<AlchemistWidgetModelDefinition<UiTronResponse>>) :
HpEvents
data class UpdateScreen(val data: AlchemistScreenDefinition) : HpEvents
data class UpdateScreenContentWidgetRenderState(val id: String, val state: WidgetRenderState) :
HpEvents
data object ClearScreenContent : HpEvents
data object PrioritySectionProcessed : HpEvents
}
@Immutable
data class HpStates(
val hpDialogStateHolder: HpDialogStateHolder = HpDialogStateHolder(HpDialogState.Hidden),
val lastSelectedTab: String = BottomBarTabType.HOME.name,
val profileDrawerState: Boolean = false,
val homeScreenSnackBarState: Boolean = false,
val currentLoadedFragmentScreenName: String = NaviAnalytics.NEW_HOME,
val homeFeatures: HomeFeatureResponse? = null,
val screenDefinition: AlchemistScreenDefinition? = null,
val showShadowOnFrontLayer: Boolean = false,
val isLoading: Boolean = true,
val renderingFirstTime: Boolean = true,
val processPrioritySection: Boolean = true,
val isError: Boolean = false,
val error: GenericErrorResponse? = null
) : UiState
@Immutable
sealed interface HpEffects : UiEffect {
data class OnActionData(val actionData: UiTronActionData?) : HpEffects
data class OnUitronAction(val action: UiTronAction) : HpEffects
data class OnActionsFromJson(val handler: UiTronActionHandler?) : HpEffects
data object TrackBottomBarEvents : HpEffects
data class ShowNotificationPermissions(val manager: PermissionsManager) : HpEffects
data class OnNotificationUpdatedCount(val count: Int) : HpEffects
data object OnRenderActions : HpEffects
data class OnApiFailure(
val errorMessage: ErrorMessage?,
val errors: List<GenericErrorResponse>?
) : HpEffects
data class LogAppLaunchTime(val destination: String) : HpEffects
data object OnHideBalanceAction : HpEffects
data object OnPrioritySectionRendered : HpEffects
data class InitiatePayment(val data: InitiatePaymentFromComposeData) : HpEffects
data object FetchHomeApi : HpEffects
data class HandleCtaActionEvents(val event: CtaActionEvent) : HpEffects
}

View File

@@ -1,21 +0,0 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.ui.state
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.network.models.GenericErrorResponse
sealed interface HomeScreenState {
data object Loading : HomeScreenState
data class Success(val data: AlchemistScreenDefinition) : HomeScreenState
data class Error(
val error: GenericErrorResponse? = null,
) : HomeScreenState
}

View File

@@ -0,0 +1,70 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.usecase
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.uitron.model.UiTronResponse
import com.naviapp.network.di.DataDeserializers
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
class DeserializationUseCase
@Inject
constructor(@DataDeserializers private val deserializer: Gson) {
private val widgetTypeToken =
object : TypeToken<AlchemistWidgetModelDefinition<UiTronResponse>>() {}.type
suspend fun deserializePriorityContent(
cacheEntity: NaviCacheEntity,
startIndex: Int = 0,
endIndex: Int? = null,
onWidgetsDeserialized:
suspend (List<AlchemistWidgetModelDefinition<UiTronResponse>>) -> Unit
) =
withContext(Dispatchers.Default) {
val jsonArray = extractFrontLayerJsonArray(cacheEntity)
jsonArray?.let {
val finalEndIndex = endIndex?.coerceAtMost(jsonArray.length()) ?: jsonArray.length()
val widgets =
(startIndex until finalEndIndex)
.map { jsonArray.getString(it) }
.map {
deserializer.fromJson<AlchemistWidgetModelDefinition<UiTronResponse>>(
it,
widgetTypeToken
)
}
onWidgetsDeserialized(widgets)
}
}
suspend fun deserializeScreen(
cacheEntity: NaviCacheEntity,
onScreenDeserialized: (AlchemistScreenDefinition) -> Unit
) =
withContext(Dispatchers.Default) {
val screenDefinition =
deserializer.fromJson(cacheEntity.value, AlchemistScreenDefinition::class.java)
onScreenDeserialized(screenDefinition)
}
private fun extractFrontLayerJsonArray(cacheEntity: NaviCacheEntity?): JSONArray? {
val screenJson = cacheEntity?.value?.let { JSONObject(it) }
return screenJson
?.getJSONObject("screenStructure")
?.getJSONObject("content")
?.getJSONArray("widgets")
}
}

View File

@@ -0,0 +1,99 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.usecase
import com.google.gson.Gson
import com.navi.base.AppServiceManager
import com.navi.base.cache.model.NaviCacheAltSourceEntity
import com.navi.base.utils.ConnectivityObserver
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.alchemist.model.AlchemistScreenRequest
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.network.models.ErrorMessage
import com.navi.common.network.models.GenericErrorResponse
import com.navi.common.network.models.RepoResult
import com.naviapp.BuildConfig
import com.naviapp.app.NaviApplication
import com.naviapp.home.respository.HomeRepository
import com.naviapp.network.di.DataSerializers
import com.naviapp.utils.Constants.HOME_SCREEN_IN_CAPS
import javax.inject.Inject
class FetchHomeItemsUseCase
@Inject
constructor(
@DataSerializers private val dataSerializers: Gson,
private val homeRepository: HomeRepository
) {
suspend fun fetchHomeItemFromAPI(
density: String? = null,
connectivityType: String? = null,
availableAppVersionCode: Int?,
connectivityObserver: ConnectivityObserver,
installedModules: String,
screenHash: String? = null,
noInternetCallback: () -> Unit,
onFailure:
suspend (apiErrorMessage: ErrorMessage?, errors: List<GenericErrorResponse>?) -> Unit,
): NaviCacheAltSourceEntity {
// Query Map Adaptations
val queryMap = HashMap<String, String>()
val shouldSendScreenHashInApi =
FirebaseRemoteConfigHelper.getBoolean(
FirebaseRemoteConfigHelper.SEND_SCREEN_HASH_IN_HOME_SCREEN_API
)
val screenHashForApi = if (shouldSendScreenHashInApi) screenHash else null
val deviceInfoDetails =
(AppServiceManager.application as NaviApplication)
.naviPayManager
.get()
.deviceInfoDetails()
val response =
homeRepository.fetchHomeItems(
density = density,
connectivityType = connectivityType,
availableAppVersionCode = availableAppVersionCode,
installedModules = installedModules,
naviUpiDeviceFingerprint = deviceInfoDetails.deviceFingerPrint,
ssid = deviceInfoDetails.provider.ssid,
request =
AlchemistScreenRequest(
screenName = HOME_SCREEN_IN_CAPS,
screenHash = screenHashForApi,
inputMap = queryMap
)
)
return handleResponse(connectivityObserver, response, noInternetCallback, onFailure)
}
private suspend fun handleResponse(
connectivityObserver: ConnectivityObserver,
response: RepoResult<AlchemistScreenDefinition>,
noInternetCallback: () -> Unit,
onFailure:
suspend (apiErrorMessage: ErrorMessage?, errors: List<GenericErrorResponse>?) -> Unit
): NaviCacheAltSourceEntity {
return if (connectivityObserver.isInternetConnected()) {
response.data?.screenStructure?.let {
NaviCacheAltSourceEntity(
value = dataSerializers.toJson(response.data),
version = BuildConfig.VERSION_CODE,
isSuccess = true
)
} ?: NaviCacheAltSourceEntity(isSuccess = false)
} else {
noInternetCallback()
onFailure(response.error, response.errors)
NaviCacheAltSourceEntity(isSuccess = false)
}
}
}

View File

@@ -0,0 +1,232 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.usecase
import android.content.Intent
import android.os.Bundle
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.navi.amc.common.activity.CheckerActivity
import com.navi.amc.common.taskProcessor.AmcTaskManager
import com.navi.amc.navigator.NaviAmcDeeplinkNavigator
import com.navi.amc.utils.Constant
import com.navi.amc.utils.TempStorageHelper
import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.model.ActionData
import com.navi.base.model.CtaData
import com.navi.base.model.CtaType
import com.navi.base.utils.EMPTY
import com.navi.base.utils.orFalse
import com.navi.common.uitron.model.action.CtaAction
import com.navi.common.uitron.model.action.LambdaApiAction
import com.navi.common.utils.toActionData
import com.navi.uitron.model.action.AnalyticsAction
import com.navi.uitron.model.action.TriggerApiAction
import com.navi.uitron.model.data.UiTronAction
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.fragment.LoanRepaymentBottomSheet
import com.naviapp.common.navigator.NaviDeepLinkNavigator
import com.naviapp.home.activity.InAppNotificationActivity
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.compose.model.CtaActionEvent
import com.naviapp.home.compose.model.InitiatePaymentFromComposeData
import com.naviapp.home.model.HomeCtaTypes
import com.naviapp.part_prepayment.PartPrePaymentActivity
import com.naviapp.payment.activities.NaviPaymentActivity
import com.naviapp.payment.fragments.PaymentType
import com.naviapp.payment.models.Amount
import com.naviapp.utils.Constants
import com.naviapp.utils.Constants.FETCH_HOME_ITEMS
import com.naviapp.utils.LOAN_ACCOUNT_NUMBER
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
class HandleCtaUseCase @Inject constructor() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun onActionTriggered(
uiTronAction: UiTronAction?,
shouldFetchHomeApi: () -> Unit,
onCtaActionEvent: (CtaActionEvent) -> Unit
) {
when (uiTronAction) {
is CtaAction -> {
ctaActionCallbacks(ctaAction = uiTronAction, onCallback = onCtaActionEvent)
}
is AnalyticsAction -> {
NaviTrackEvent.trackEvent(
uiTronAction.eventName ?: "",
uiTronAction.eventProperties
)
}
is TriggerApiAction -> {
if (
uiTronAction is LambdaApiAction && uiTronAction.lambdaType == FETCH_HOME_ITEMS
) {
shouldFetchHomeApi()
}
}
else -> {
FirebaseCrashlytics.getInstance()
.log("${uiTronAction?.type} Action not handled now")
Timber.d("Action not handled now")
}
}
}
private fun ctaActionCallbacks(
ctaAction: CtaAction,
onCallback: (CtaActionEvent) -> Unit,
) {
when (HomeCtaTypes.find(ctaAction.ctaData?.type)) {
HomeCtaTypes.REDIRECTION_CTA -> {
scope.launch { onCallback(CtaActionEvent.RedirectToCta(ctaAction.ctaData)) }
}
HomeCtaTypes.COPY_TO_CLIPBOARD -> {
scope.launch {
onCallback(CtaActionEvent.CopyToClipboard(ctaAction.ctaData?.title ?: EMPTY))
}
}
HomeCtaTypes.DIALOG -> {
scope.launch { onCallback(CtaActionEvent.ShowDialog(ctaAction.ctaData?.url)) }
}
HomeCtaTypes.BOTTOM_SHEET -> {
scope.launch { onCallback(CtaActionEvent.ShowBottomSheet(ctaAction.ctaData?.url)) }
}
HomeCtaTypes.UNKNOWN -> {
FirebaseCrashlytics.getInstance().log("Unknown cta Type clicked")
}
else -> {
FirebaseCrashlytics.getInstance().log("cta not found")
}
}
}
fun handleCtaData(
naviClickAction: CtaData,
activity: HomePageActivity,
callBackToActivityScreen: (callback: HomeScreenCallbackListener) -> Unit,
onProfileIconClicked: () -> Unit
) {
if (toOpenBottomSheet(naviClickAction.url)) {
toShowBottomSheet(activity = activity, action = naviClickAction.toActionData())
return
} else if (
naviClickAction.url == CtaType.PAYMENT.name ||
naviClickAction.url == CtaType.PAYMENT_PL.name
) {
callBackToActivityScreen(
HomeScreenCallbackListener.InitiatePaymentOnHomePage(
paymentData = handlePaymentCtaData(naviClickAction)
)
)
} else if (naviClickAction.url == NaviDeepLinkNavigator.PROFILE) {
onProfileIconClicked()
} else if (naviClickAction.url == Constants.Notification.IN_APP_NOTIFICATION) {
val intent = Intent(activity, InAppNotificationActivity::class.java)
activity.startActivity(intent)
} else {
naviClickAction.analyticsEventProperties?.let { eventProperties ->
eventProperties.name?.let { eventName ->
NaviTrackEvent.trackEventOnClickStream(eventName, eventProperties.properties)
}
}
if (
naviClickAction.url
?.contains(
NaviAmcDeeplinkNavigator.AMC.plus(Constants.DIVIDER)
.plus(NaviAmcDeeplinkNavigator.KYC),
true
)
.orFalse() ||
naviClickAction.url
?.contains(CheckerActivity.HPC_PAN_REDIRECTION_PAGE)
.orFalse() ||
naviClickAction.url
?.contains(CheckerActivity.HPC_NAME_REDIRECTION_PAGE)
.orFalse()
) {
TempStorageHelper.kycSourceInfo =
mapOf(Constant.KYC_SOURCE_SCREEN to NaviAnalytics.HOME)
AmcTaskManager.requestParams[Constant.PREFETCH_SOURCE_SCREEN] = NaviAnalytics.HOME
}
NaviDeepLinkNavigator.navigate(
activity,
naviClickAction,
needsResult = naviClickAction.needsResult,
requestCode = naviClickAction.requestCode
)
}
}
private fun toOpenBottomSheet(url: String?): Boolean {
return url?.let { url.contains(Constants.SHOW_BOTTOMSHEET, true) } ?: run { false }
}
private fun toShowBottomSheet(activity: HomePageActivity, action: ActionData?) {
val splitDeepLink = action?.url?.split(Constants.SLASH)
when (splitDeepLink?.getOrNull(1)) {
NaviAnalytics.LOAN_REPAYMENT_BOTTOMSHEET -> {
val bundle =
Bundle().apply { putParcelable(LoanRepaymentBottomSheet.ACTION_DATA, action) }
val tag = LoanRepaymentBottomSheet.TAG
val fragment = LoanRepaymentBottomSheet.getInstance(bundle)
activity.safelyShowBottomSheet(fragment, tag)
}
}
}
private fun handlePaymentCtaData(naviClickAction: CtaData): InitiatePaymentFromComposeData {
val loanAccountNumber =
naviClickAction.parameters?.firstOrNull { it.key == LOAN_ACCOUNT_NUMBER }?.value
val amountData =
naviClickAction.parameters
?.firstOrNull { it.key == NaviPaymentActivity.AMOUNT_DATA }
?.value
val currency =
naviClickAction.parameters
?.firstOrNull { it.key == PartPrePaymentActivity.CURRENCY }
?.value
val symbol =
naviClickAction.parameters
?.firstOrNull { it.key == PartPrePaymentActivity.SYMBOL }
?.value
val repaymentType =
naviClickAction.parameters
?.firstOrNull { it.key == PartPrePaymentActivity.REPAYMENT_TYPE }
?.value
val paymentType: PaymentType =
PaymentType.get(
naviClickAction.parameters?.firstOrNull { it.key == Constants.PAYMENT_TYPE }?.value
)
val isPreClosure: Boolean =
(repaymentType == PaymentType.SCHEDULED_PRE_CLOSURE.name ||
repaymentType == PaymentType.PRE_CLOSURE.name)
val loanType =
naviClickAction.parameters?.firstOrNull { it.key == Constants.LOAN_TYPE }?.value
val updatePaymentType =
if (isPreClosure) {
PaymentType.PRE_LOAN_CLOSURE
} else {
paymentType
}
return InitiatePaymentFromComposeData(
amount = Amount(amountData?.toDoubleOrNull(), currency = currency, symbol = symbol),
isPreClosure = isPreClosure,
paymentType = updatePaymentType.name,
repaymentType = repaymentType,
loanType = loanType,
loanAccountNumber = loanAccountNumber
)
}
}

View File

@@ -0,0 +1,157 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.usecase
import com.navi.base.model.CtaData
import com.navi.common.uitron.model.action.CtaAction
import com.navi.pay.common.setup.NaviPayManager
import com.navi.uitron.model.action.UpdateDataAction
import com.navi.uitron.model.action.UpdateViewStateAction
import com.navi.uitron.model.data.RowData
import com.navi.uitron.model.data.TextData
import com.navi.uitron.model.data.UiTronAction
import com.navi.uitron.model.data.UiTronActionData
import com.naviapp.home.model.HomeCtaTypes
import com.naviapp.utils.Constants
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
class HandleUpiUseCase
@Inject
constructor(
val naviPayManager: NaviPayManager,
) {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun updateUPI(
onAction: (UiTronAction) -> Unit,
onActionData: (UiTronActionData) -> Unit,
) {
updateUPILiteBalanceV2(onAction)
updateUPIVpa(onAction, onActionData)
}
private fun updateUPILiteBalanceV2(
onAction: (UiTronAction) -> Unit,
) {
val liteBalance = naviPayManager.getUpiLiteBalance()
if (liteBalance.isNotEmpty()) {
onAction(
UpdateDataAction(
viewDataList =
listOf(
UpdateDataAction.ViewData(
layoutId = Constants.UPI_LITE_BALANCE_TEXT_V2,
data = TextData(text = liteBalance)
)
)
)
)
onAction(
UpdateViewStateAction(
viewStates =
mapOf(
Constants.UPI_LITE_BALANCE_TEXT_V2 to Constants.SHOW,
Constants.UPI_LITE_SUBTITLE_TEXT to Constants.HIDE
)
)
)
} else {
onAction(
UpdateViewStateAction(
viewStates =
mapOf(
Constants.UPI_LITE_BALANCE_TEXT_V2 to Constants.HIDE,
Constants.UPI_LITE_SUBTITLE_TEXT to Constants.SHOW
)
)
)
}
}
private fun updateUPIVpa(
onAction: (UiTronAction) -> Unit,
onActionData: (UiTronActionData) -> Unit
) {
scope.launch(Dispatchers.IO) {
val upiId = naviPayManager.getVpaOfPrimaryAccount().firstOrNull()
handleVpa(upiId, onAction, onActionData)
}
}
fun handleVpa(
upiId: String?,
onAction: (UiTronAction) -> Unit,
onActionData: (UiTronActionData) -> Unit
) {
if (upiId.isNullOrBlank()) {
handleVpaNotAvailable(onAction)
} else {
handleVpaAvailable(upiId, onActionData)
}
}
private fun handleVpaAvailable(upiId: String, onActionData: (UiTronActionData) -> Unit) {
val actionData =
UiTronActionData(
actions =
listOf(
UpdateViewStateAction(
viewStates =
mapOf(
Constants.GET_UPI_ID_CONTAINER to Constants.INVISIBLE,
Constants.UPI_ID_CONTAINER to Constants.VISIBLE
)
),
UpdateDataAction(
listOf(
UpdateDataAction.ViewData(
layoutId = Constants.UPI_ID_TEXT,
data = TextData(text = "UPI ID: $upiId")
),
UpdateDataAction.ViewData(
layoutId = Constants.UPI_ID_CONTAINER,
data =
RowData().apply {
onClick = getUpiIdRowClickActionData(upiId)
}
)
)
)
)
)
onActionData(actionData)
}
private fun getUpiIdRowClickActionData(upiId: String): UiTronActionData {
return UiTronActionData(
actions =
listOf(
CtaAction(
ctaData = CtaData(type = HomeCtaTypes.COPY_TO_CLIPBOARD.name, title = upiId)
)
)
)
}
private fun handleVpaNotAvailable(onAction: (UiTronAction) -> Unit) {
onAction(
UpdateViewStateAction(
viewStates =
mapOf(
Constants.GET_UPI_ID_CONTAINER to Constants.VISIBLE,
Constants.UPI_ID_CONTAINER to Constants.INVISIBLE
)
)
)
}
}

View File

@@ -24,7 +24,7 @@ import com.navi.common.utils.Constants.SCREEN_ID
import com.navi.common.utils.Constants.UPI_NUX_SCREEN
import com.naviapp.common.navigator.NaviDeepLinkNavigator
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.registration.RegistrationActivity
import com.naviapp.utils.Constants.SOURCE
import dagger.hilt.android.qualifiers.ActivityContext
@@ -40,11 +40,11 @@ constructor(@ActivityContext private val context: Context) {
return extras?.getString(REDIRECTION_URL) == UPI_NUX_SCREEN
}
fun redirectToDestination(activity: HomePageActivity, homeVM: HomeVM) {
fun redirectToDestination(homeVM: HomeViewModel) {
if (BaseUtils.isUserLoggedIn().not()) {
navigateToLogin(activity)
navigateToLogin(context as HomePageActivity)
}
val extras = activity.intent.extras
val extras = (context as HomePageActivity).intent.extras
when (extras?.getString(REDIRECTION_URL)) {
UPI_NUX_SCREEN -> {
if (homeVM.nuxHandler.isUserEligibleForNux(UPI_NUX_SCREEN)) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,371 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.viewmodel
import android.content.Context
import androidx.lifecycle.viewModelScope
import com.navi.base.cache.model.NaviCacheAltSourceEntity
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.model.NaviCacheEntityInfo
import com.navi.base.cache.repository.NaviCacheRepositoryImpl
import com.navi.base.cache.util.NaviSharedDbKeys
import com.navi.base.deeplink.util.DeeplinkConstants
import com.navi.base.model.CtaData
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.BaseUtils
import com.navi.base.utils.ConnectivityObserver
import com.navi.common.basemvi.BaseMviViewModel
import com.navi.common.uitron.helper.VideoViewHelper
import com.navi.common.utils.Constants.SCREEN_HASH
import com.navi.common.utils.Constants.UPI_NUX_SCREEN
import com.navi.common.utils.TemporaryStorageHelper
import com.navi.common.utils.getDensityName
import com.navi.common.utils.getInstalledDynamicModulesCommaSeparated
import com.navi.common.utils.getNetworkType
import com.navi.naviwidgets.utils.CURRENT_VERSION_IN_STORE
import com.navi.uitron.model.data.UiTronAction
import com.navi.uitron.model.data.UiTronActionData
import com.naviapp.BuildConfig
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.navigator.NaviDeepLinkNavigator
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.uiTron.helper.RotatingViewHelper
import com.naviapp.home.reducer.HomeReducer
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.respository.HomeRepository
import com.naviapp.home.usecase.DeserializationUseCase
import com.naviapp.home.usecase.FetchHomeItemsUseCase
import com.naviapp.home.usecase.HandleCtaUseCase
import com.naviapp.home.usecase.HandleUpiUseCase
import com.naviapp.nux.handler.NewUserExperienceHandler
import com.naviapp.utils.Constants.HomePageConstants.FETCH_HOME_ITEMS_TIMEOUT
import dagger.Lazy
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@HiltViewModel
class HomeViewModel
@Inject
constructor(
private val homeRepository: HomeRepository,
private val naviCacheRepository: NaviCacheRepositoryImpl,
private val deserializationUseCase: DeserializationUseCase,
private val fetchHomeItemsUseCase: FetchHomeItemsUseCase,
private val selectiveRefreshHandler: SelectiveRefreshHandler,
private val ctaHandler: HandleCtaUseCase,
private val connectivityObserver: ConnectivityObserver,
val nuxHandler: NewUserExperienceHandler,
private val upiUseCase: HandleUpiUseCase,
val rotatingViewHelper: Lazy<RotatingViewHelper>,
val videoViewHelper: Lazy<VideoViewHelper>,
) :
BaseMviViewModel<HpStates, HpEvents, HpEffects>(
initialState = HpStates(),
reducer = HomeReducer()
) {
var shouldRefreshHomeApi: Boolean = false
var homeTabLastUpdateTimestamp: Long = System.currentTimeMillis()
private var homePageRefreshJob: Job? = null
val naviAnalyticsEventTracker by lazy { NaviAnalytics.naviAnalytics.Home() }
private var analyticsStartTs = System.currentTimeMillis()
// Backdrop Scaffold Progress used for updating the alpha of the top nav
private val _backdropScaffoldProgress = MutableStateFlow(0f)
val backdropScaffoldProgress: StateFlow<Float> = _backdropScaffoldProgress
// Internet Connectivity
private val _internetConnectivity = MutableSharedFlow<ConnectivityObserver.Status>()
val internetConnectivity: SharedFlow<ConnectivityObserver.Status> = _internetConnectivity
private fun vmScope(context: CoroutineContext = Dispatchers.IO) =
CoroutineScope(context + SupervisorJob())
init {
vmScope().safeLaunch { observeActionCallback() }
vmScope().safeLaunch { observeInternetConnectivity() }
}
private suspend fun observeActionCallback() {
getActionCallback().collect { action ->
ctaHandler.onActionTriggered(
uiTronAction = action,
shouldFetchHomeApi = { sendEffect(vmScope()) { HpEffects.FetchHomeApi } },
onCtaActionEvent = { sendEffect(vmScope()) { HpEffects.HandleCtaActionEvents(it) } }
)
}
}
private suspend fun observeInternetConnectivity() {
connectivityObserver.observe().collectLatest { _internetConnectivity.emit(it) }
}
fun updateBackdropScaffoldProgress(offset: Float) {
viewModelScope.launch { _backdropScaffoldProgress.update { offset } }
}
fun setHomeApiRefreshFlag(shouldRefresh: Boolean) {
this.shouldRefreshHomeApi = shouldRefresh
}
suspend fun handleRemainingItemsForFirstLoad() {
val cacheEntity = naviCacheRepository.get(NaviSharedDbKeys.HOME_TAB.name)
cacheEntity?.let {
updateHomeScreen(it)
sendEvent(HpEvents.FirstLoadCompleted)
}
}
fun fetchHomeDataWithTimeout(context: Context) =
viewModelScope.safeLaunch(Dispatchers.IO) {
try {
withTimeout(FETCH_HOME_ITEMS_TIMEOUT) {
val homeData = fetchHomeDataFromApi(context)
saveHomeDataToCache(homeData)
}
} catch (e: TimeoutCancellationException) {
naviAnalyticsEventTracker.onLoginHomePageApiCallTimeout()
sendEvent(HpEvents.TriggerErrorState)
}
}
private suspend fun fetchHomeDataFromApi(context: Context): NaviCacheAltSourceEntity {
return fetchHomeItemsUseCase.fetchHomeItemFromAPI(
connectivityObserver = connectivityObserver,
density = getDensityName(context),
connectivityType = getNetworkType(context),
availableAppVersionCode =
PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
installedModules = getInstalledDynamicModulesCommaSeparated(),
screenHash = null,
onFailure = { errorMessage, errors ->
sendEffect(vmScope()) { HpEffects.OnApiFailure(errorMessage, errors) }
},
noInternetCallback = {
viewModelScope.launch {
_internetConnectivity.emit(ConnectivityObserver.Status.Unavailable)
}
}
)
}
private suspend fun saveHomeDataToCache(homeData: NaviCacheAltSourceEntity) {
naviCacheRepository.save(
NaviCacheEntity(
key = NaviSharedDbKeys.HOME_TAB.name,
value = homeData.value.orEmpty(),
version = BuildConfig.VERSION_CODE
)
)
}
fun loadHomeElements(activity: HomePageActivity, isPaymentLoaderShowing: Boolean = false) {
viewModelScope.safeLaunch(Dispatchers.IO) {
if (BaseUtils.isUserLoggedIn()) {
handleLoggedInUser(activity, isPaymentLoaderShowing)
} else {
handleLoggedOutUser(activity)
}
}
}
private fun handleLoggedInUser(activity: HomePageActivity, isPaymentLoaderShowing: Boolean) {
if (isPaymentLoaderShowing || state.value.profileDrawerState) {
val duration = System.currentTimeMillis() - analyticsStartTs
naviAnalyticsEventTracker.onHomePageInit(duration)
}
naviAnalyticsEventTracker.onHomePageApiCall()
analyticsStartTs = System.currentTimeMillis()
fetchHomeDataFromCache(
density = getDensityName(activity),
connectivityType = getNetworkType(activity),
availableAppVersionCode =
PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
installedModules = getInstalledDynamicModulesCommaSeparated()
)
}
private fun handleLoggedOutUser(activity: HomePageActivity) {
naviAnalyticsEventTracker.onFetchHomeCardsForLogoutUser(NaviAnalytics.NEW_HOME)
NaviDeepLinkNavigator.navigate(
activity,
CtaData(url = DeeplinkConstants.REGISTRATION),
clearTask = true
)
}
private fun fetchHomeDataFromCache(
density: String? = null,
connectivityType: String? = null,
availableAppVersionCode: Int?,
installedModules: String
) {
if (homePageRefreshJob?.isActive == true) return
homePageRefreshJob =
viewModelScope.launch(Dispatchers.IO) {
handleDataModification()
val screenHash = state.value.screenDefinition?.screenMetaData?.get(SCREEN_HASH)
fetchAndHandleCacheData(
density,
connectivityType,
availableAppVersionCode,
installedModules,
screenHash
)
}
}
private fun handleDataModification() {
if (TemporaryStorageHelper.isDataModified(TemporaryStorageHelper.HOME)) {
selectiveRefreshHandler.handleLoadingState(this@HomeViewModel)
}
}
private suspend fun fetchAndHandleCacheData(
density: String?,
connectivityType: String?,
availableAppVersionCode: Int?,
installedModules: String,
screenHash: String?
) {
naviCacheRepository
.getDataAndFetchFromAltSourceWithSimilarityCheck(
key = NaviSharedDbKeys.HOME_TAB.name,
version = BuildConfig.VERSION_CODE.toLong(),
getDataFromAltSource = {
fetchHomeItemsUseCase.fetchHomeItemFromAPI(
connectivityObserver = connectivityObserver,
density = density,
connectivityType = connectivityType,
availableAppVersionCode = availableAppVersionCode,
installedModules = installedModules,
screenHash = screenHash,
onFailure = { errorMessage, errors ->
sendEffect(vmScope()) { HpEffects.OnApiFailure(errorMessage, errors) }
},
noInternetCallback = {
viewModelScope.launch {
_internetConnectivity.emit(ConnectivityObserver.Status.Unavailable)
}
}
)
}
)
.collect { handleHomeCacheResponse(it) }
}
private suspend fun handleHomeCacheResponse(response: NaviCacheEntityInfo) {
when {
response.isError -> sendEvent(HpEvents.TriggerErrorState)
response.data != null -> processHomeData(response.data!!)
else -> sendEvent(HpEvents.TriggerErrorState)
}
}
private suspend fun processHomeData(data: NaviCacheEntity) {
when {
state.value.renderingFirstTime && state.value.processPrioritySection ->
processPriorityContent(data)
else -> updateHomeScreen(data)
}
finishProcessing(data)
}
private suspend fun processPriorityContent(data: NaviCacheEntity) {
deserializationUseCase.deserializePriorityContent(cacheEntity = data, endIndex = 4) {
sendEvent(HpEvents.AddScreenContent(it))
}
sendEvent(HpEvents.PrioritySectionProcessed)
}
private suspend fun updateHomeScreen(data: NaviCacheEntity) {
deserializationUseCase.deserializeScreen(data) { sendEvent(HpEvents.UpdateScreen(it)) }
}
private fun finishProcessing(data: NaviCacheEntity) {
sendEffect(vmScope(Dispatchers.Default)) { HpEffects.OnRenderActions }
updateHomeApiTimestamp()
data.updatedAt.let { homeTabLastUpdateTimestamp = it }
selectiveRefreshHandler.handleSuccessState(this@HomeViewModel)
}
private fun updateHomeApiTimestamp() {
TemporaryStorageHelper.updateApiTs(TemporaryStorageHelper.HOME)
naviAnalyticsEventTracker.onHomePageApiResponse(
System.currentTimeMillis() - analyticsStartTs
)
}
fun handleCtaData(
naviClickAction: CtaData,
activity: HomePageActivity,
callBackToActivityScreen: (callback: HomeScreenCallbackListener) -> Unit
) {
ctaHandler.handleCtaData(naviClickAction, activity, callBackToActivityScreen) {
sendEvent(HpEvents.OnProfileIconClicked)
}
}
fun fetchNuxScreenDataForEligibleUsers(navigateToNuxScreen: () -> Unit) {
vmScope().launch {
nuxHandler.fetchNuxScreenDataForEligibleUsers(UPI_NUX_SCREEN, navigateToNuxScreen)
}
}
fun fetchHomeFeature(type: String) {
viewModelScope.launch {
val response = homeRepository.fetchHomeFeature(type)
if (response.error == null && response.errors.isNullOrEmpty()) {
sendEvent(HpEvents.UpdateHpFeatures(response.data))
}
}
}
fun handleUpiAdaptations() {
upiUseCase.updateUPI(
onAction = {
sendEffect(vmScope(Dispatchers.Default)) { HpEffects.OnUitronAction(it) }
},
onActionData = {
sendEffect(vmScope(Dispatchers.Default)) { HpEffects.OnActionData(it) }
}
)
}
fun observeUPIVpa(onAction: (UiTronAction) -> Unit, onActionData: (UiTronActionData) -> Unit) {
vmScope().launch {
upiUseCase.naviPayManager.getVpaOfPrimaryAccount().collect { upiId ->
upiUseCase.handleVpa(upiId, onAction, onActionData)
}
}
}
override fun onCleared() {
super.onCleared()
videoViewHelper.get().clear()
}
}

View File

@@ -7,26 +7,24 @@
package com.naviapp.nux.model
import androidx.compose.runtime.Stable
import androidx.compose.runtime.Immutable
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.basemvi.UiEffect
import com.navi.common.basemvi.UiEvent
import com.navi.common.basemvi.UiState
sealed class NuxGenericScreenUiEvent : UiEvent {
data class RenderUI(val response: AlchemistScreenDefinition) : NuxGenericScreenUiEvent()
data object ApiFailed : NuxGenericScreenUiEvent()
data object BackPressed : NuxGenericScreenUiEvent()
}
@Stable
@Immutable
data class MainScreenUiState(
val isLoading: Boolean,
val isLoading: Boolean = true,
val screenDefinition: AlchemistScreenDefinition? = null,
) : UiState
@Immutable
sealed class NuxGenericScreenUiEvent : UiEvent {
data class RenderUI(val response: AlchemistScreenDefinition) : NuxGenericScreenUiEvent()
}
@Immutable
sealed class MainScreenUiEffect : UiEffect {
sealed class Navigation : MainScreenUiEffect() {
data object Back : Navigation()

View File

@@ -8,25 +8,20 @@
package com.naviapp.nux.reducer
import com.navi.common.basemvi.BaseReducer
import com.navi.common.basemvi.UiEvent
import com.naviapp.nux.model.MainScreenUiEffect
import com.naviapp.nux.model.MainScreenUiState
import com.naviapp.nux.model.NuxGenericScreenUiEvent
import kotlinx.coroutines.CoroutineScope
class NuxGenericScreenReducer(val coroutineScope: CoroutineScope, initialState: MainScreenUiState) :
BaseReducer<MainScreenUiState, MainScreenUiEffect>(initialState) {
class NuxGenericScreenReducer :
BaseReducer<MainScreenUiState, NuxGenericScreenUiEvent, MainScreenUiEffect> {
override fun reduce(oldState: MainScreenUiState, event: UiEvent) {
when (event) {
override fun reduce(
previousState: MainScreenUiState,
event: NuxGenericScreenUiEvent
): MainScreenUiState {
return when (event) {
is NuxGenericScreenUiEvent.RenderUI -> {
setState { oldState.copy(isLoading = false, screenDefinition = event.response) }
}
is NuxGenericScreenUiEvent.BackPressed -> {
setEffect(coroutineScope) { MainScreenUiEffect.Navigation.Back }
}
is NuxGenericScreenUiEvent.ApiFailed -> {
setEffect(coroutineScope) { MainScreenUiEffect.Navigation.Back }
previousState.copy(isLoading = false, screenDefinition = event.response)
}
}
}

View File

@@ -17,7 +17,6 @@ import com.navi.ap.common.ui.composables.GenericShimmerLoader
import com.navi.common.alchemist.model.AlchemistScreenStructure
import com.naviapp.nux.model.MainScreenUiEffect
import com.naviapp.nux.model.MainScreenUiState
import com.naviapp.nux.model.NuxGenericScreenUiEvent
import com.naviapp.nux.viewmodel.NuxViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
@@ -28,7 +27,6 @@ fun NuxGenericScreen(
state: MainScreenUiState,
effectFlow: Flow<MainScreenUiEffect>?,
getViewModel: () -> NuxViewModel,
onEventSent: (event: NuxGenericScreenUiEvent) -> Unit,
onNavigationRequested: (MainScreenUiEffect.Navigation) -> Unit
) {
LaunchedEffect(Unit) {
@@ -44,7 +42,11 @@ fun NuxGenericScreen(
}
LaunchedEffect(Unit) { getViewModel.invoke().getScreenDefinition() }
BackHandler { onEventSent(NuxGenericScreenUiEvent.BackPressed) }
BackHandler {
getViewModel().sendEffect(getViewModel().coroutineScope) {
MainScreenUiEffect.Navigation.Back
}
}
Box(modifier = Modifier.navigationBarsPadding()) {
if (state.isLoading) {

View File

@@ -27,7 +27,6 @@ fun NuxGenericScreenDestination(bundle: Bundle?, viewModel: NuxViewModel = hiltV
state = state,
effectFlow = viewModel.effect,
getViewModel = { viewModel },
onEventSent = { event -> viewModel.sendEvent(event) },
onNavigationRequested = { navigationEffect ->
if (navigationEffect is MainScreenUiEffect.Navigation.Back) {
viewModel.state.value.screenDefinition?.screenStructure?.systemBackCta?.let {

View File

@@ -19,31 +19,22 @@ import com.naviapp.nux.reducer.NuxGenericScreenReducer
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@HiltViewModel
class NuxViewModel @Inject constructor(val nuxHandler: NewUserExperienceHandler) :
BaseMviViewModel<NuxGenericScreenUiEvent, MainScreenUiState, MainScreenUiEffect>() {
BaseMviViewModel<MainScreenUiState, NuxGenericScreenUiEvent, MainScreenUiEffect>(
initialState = MainScreenUiState(),
reducer = NuxGenericScreenReducer()
) {
private var queryMap: MutableMap<String, String?> = mutableMapOf()
override val reducer = NuxGenericScreenReducer(viewModelScope, setInitialState())
override val state: StateFlow<MainScreenUiState>
get() = reducer.state
override val effect: Flow<MainScreenUiEffect>
get() = reducer.effect
override fun setInitialState() = MainScreenUiState(isLoading = true, screenDefinition = null)
fun getScreenDefinition() {
viewModelScope.safeLaunch(Dispatchers.IO) {
nuxHandler.getNuxScreenDefinition(
queryMap = queryMap,
onSuccess = { reducer.sendEvent(NuxGenericScreenUiEvent.RenderUI(it)) },
onFailure = { reducer.sendEvent(NuxGenericScreenUiEvent.ApiFailed) }
onSuccess = { sendEvent(NuxGenericScreenUiEvent.RenderUI(it)) },
onFailure = { sendEffect(coroutineScope) { MainScreenUiEffect.Navigation.Back } }
)
}
}

View File

@@ -57,7 +57,8 @@ import com.naviapp.appupdate.activities.UpdateAppActivity
import com.naviapp.dashboard.listeners.FragmentInteractionListener
import com.naviapp.databinding.RegistrationActivityBinding
import com.naviapp.deeplinkmanagement.vm.DeeplinkManagementViewModel
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.models.TruecallerAuthData
import com.naviapp.models.request.AuthDetails
import com.naviapp.models.request.LoginType
@@ -96,7 +97,7 @@ class RegistrationActivity :
LoginListener {
private lateinit var binding: RegistrationActivityBinding
private val registrationVM by lazy { ViewModelProvider(this)[RegistrationVM::class.java] }
private val homeVM by lazy { ViewModelProvider(this)[HomeVM::class.java] }
private val homeVM: HomeViewModel by viewModels()
private val otpReceiver by lazy { SmsAutoReadReceiver() }
private val registrationSharedVM by lazy {
ViewModelProvider(this)[RegistrationSharedVM::class.java]
@@ -195,7 +196,9 @@ class RegistrationActivity :
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
homeVM.logAppLaunchTimeEvent(LOGIN_SCREEN)
homeVM.sendEffect(homeVM.coroutineScope) {
HpEffects.LogAppLaunchTime(LOGIN_SCREEN)
}
content.viewTreeObserver.removeOnPreDrawListener(this)
return true
}

View File

@@ -29,7 +29,7 @@ import com.naviapp.common.navigator.NaviDeepLinkNavigator.REGISTRATION
import com.naviapp.deeplinkmanagement.analytics.NaviDeeplinkAnalytics
import com.naviapp.deeplinkmanagement.usecase.PlayStoreReferralResolverUseCase
import com.naviapp.deeplinkmanagement.vm.DeeplinkManagementViewModel
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.models.DeeplinkData
import com.naviapp.models.request.DeeplinkRequestData
import com.naviapp.models.request.OnboardingActionRequest
@@ -50,7 +50,7 @@ class LoginDeeplinkAndRedirectionHelper
constructor(
private val deferredActionUseCase: Lazy<DeferredActionUseCase>,
private val playStoreReferralResolverUseCase: Lazy<PlayStoreReferralResolverUseCase>,
private val deeplinkAnalytics: Lazy<NaviDeeplinkAnalytics>,
private val deeplinkAnalytics: Lazy<NaviDeeplinkAnalytics>
) {
private var deepLinkBundle: Bundle? = null
@@ -145,10 +145,10 @@ constructor(
fun redirectToNextScreen(
loginType: String?,
activity: RegistrationActivity,
homeVM: HomeVM,
homeVM: HomeViewModel,
registrationVM: RegistrationVM,
) {
val homePageResponseFetchJob = homeVM.fetchAndSaveHomeTabWithTimeOut(context = activity)
val homePageResponseFetchJob = homeVM.fetchHomeDataWithTimeout(context = activity)
if (deferredActionUseCase.get().isJobStarted()) {
deferredActionUseCase.get().invokeOnJobCompletion {
analyticsTracker.loginSuccess(loginType, activity.source)
@@ -200,7 +200,7 @@ constructor(
private fun asyncHomePageResponseFetchAndRedirect(
homePageResponseFetchJob: Job,
activity: RegistrationActivity,
homeVM: HomeVM,
homeVM: HomeViewModel,
nextCta: CtaData? = null,
) {
activity.lifecycleScope.launch {
@@ -213,7 +213,11 @@ constructor(
}
}
private fun goToNextScreen(nextCta: CtaData, activity: RegistrationActivity, homeVM: HomeVM) {
private fun goToNextScreen(
nextCta: CtaData,
activity: RegistrationActivity,
homeVM: HomeViewModel
) {
if (isHomePageUrl(nextCta) && homeVM.nuxHandler.canRedirectUserToNux()) {
navigate(homeVM.nuxHandler.addUpiNuxCtaParams(nextCta, REGISTRATION), activity, homeVM)
return
@@ -221,7 +225,7 @@ constructor(
navigate(nextCta, activity = activity, homeVM)
}
private fun navigate(cta: CtaData, activity: RegistrationActivity, homeVM: HomeVM) {
private fun navigate(cta: CtaData, activity: RegistrationActivity, homeVM: HomeViewModel) {
val ctaSource = cta.parameters?.firstOrNull { it.key == SOURCE }?.value
val updatedCta =
if (homeVM.nuxHandler.canRedirectUserToNux() || ctaSource.isNotNullAndNotEmpty()) {

View File

@@ -526,6 +526,11 @@ object Constants {
}
object HomePageConstants {
const val FETCH_HOME_ITEMS_TIMEOUT = 5000L
const val NAVI_APP_NAV_HOME_PAGE_VIEWED = "NaviApp_Nav_HomePage_Viewed"
const val NAVI_APP_NAV_INVESTMENT_PAGE_VIEWED = "NaviApp_Nav_Investment_Viewed"
const val NAVI_APP_NAV_LOAN_PAGE_VIEWED = "NaviApp_Nav_Loan_Viewed"
const val NAVI_APP_NAV_INSURANCE_PAGE_VIEWED = "NaviApp_Nav_Insurance_Viewed"
val HOME_BACK_LAYER_BANNER_HEIGHT = 264.dp
val HOME_APP_BAR_HEIGHT = 60.dp
val HOME_SHAPE_CURVATURE = 8.dp

View File

@@ -7,8 +7,8 @@
package com.navi.base.cache.model
data class NaviCacheEntityDetails(
data class NaviCacheEntityInfo(
val isError: Boolean = false,
val data: NaviCacheEntity? = null,
val isComingFromAltSource: Boolean? = null,
val isCurrentAndAltDataSame: Boolean? = null
val isComingFromAltSource: Boolean? = null
)

View File

@@ -10,7 +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 com.navi.base.cache.model.NaviCacheEntityInfo
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -53,7 +53,7 @@ interface NaviCacheRepository {
emitMultipleValues: Boolean = false
): Flow<NaviCacheEntity?>
fun getDataAndFetchFromAltSourceWithDetails(
fun getDataAndFetchFromAltSourceWithSimilarityCheck(
key: String,
version: Long? = null,
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
@@ -63,11 +63,9 @@ interface NaviCacheRepository {
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<NaviCacheEntityDetails?>
return currentData.version == altData.version && currentDataValue == altDataValue
}
): Flow<NaviCacheEntityInfo?>
}
class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: NaviCacheDao) :
@@ -208,59 +206,58 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi
}
}
override fun getDataAndFetchFromAltSourceWithDetails(
override fun getDataAndFetchFromAltSourceWithSimilarityCheck(
key: String,
version: Long?,
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
isCurrentAndAltDataSame:
(currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean,
emitMultipleValues: Boolean
(currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> 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
emit(
NaviCacheEntityInfo(
data = currentValueInDB,
isComingFromAltSource = false,
isError = false
)
)
}
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
naviCacheValueEntityFromAltSource.value.isNullOrEmpty()
) {
if (currentValueInDB?.value.isNullOrEmpty()) {
emit(NaviCacheEntityInfo(isError = true, data = null))
}
} else {
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()) {
save(naviCacheEntity = naviCacheEntity)
emit(
NaviCacheEntityInfo(
data = naviCacheEntity,
isError = false,
isComingFromAltSource = true
)
)
}
}
}

View File

@@ -17,6 +17,7 @@ data class AlchemistWidgetModelDefinition<T : Any?>(
val widgetId: String? = null,
val widgetName: String? = null,
val widgetData: T? = null,
val widgetRenderState: WidgetRenderState = WidgetRenderState.VISIBLE,
val widgetStates: List<AlchemistWidgetState>? = null,
val widgetOutput: List<AlchemistWidgetOutput>? = null,
val widgetRenderActions: AlchemistRenderActions? = null,
@@ -29,6 +30,12 @@ data class AlchemistWidgetState(
val eventName: String? = null
)
enum class WidgetRenderState {
VISIBLE,
NOT_VISIBLE,
NEWLY_ADDED
}
data class AlchemistWidgetOutput(
val fieldName: String? = null,
val layoutId: String? = null,

View File

@@ -8,20 +8,40 @@
package com.navi.common.basemvi
import com.navi.common.viewmodel.BaseVM
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
abstract class BaseMviViewModel<Event : UiEvent, ViewState : UiState, Effect : UiEffect> :
BaseVM() {
abstract class BaseMviViewModel<State : UiState, Event : UiEvent, Effect : UiEffect>(
initialState: State,
private val reducer: BaseReducer<State, Event, Effect>
) : BaseVM() {
abstract fun setInitialState(): ViewState
private val _state: MutableStateFlow<State> = MutableStateFlow(initialState)
val state: StateFlow<State>
get() = _state.asStateFlow()
abstract val state: Flow<ViewState>
private val _event: MutableSharedFlow<Event> = MutableSharedFlow()
val event: SharedFlow<Event>
get() = _event.asSharedFlow()
abstract val effect: Flow<Effect>
private val _effects = Channel<Effect>(capacity = Channel.CONFLATED)
val effect = _effects.receiveAsFlow()
abstract val reducer: BaseReducer<ViewState, Effect>
fun sendEffect(scope: CoroutineScope, effect: () -> Effect) {
scope.launch { _effects.send(effect()) }
}
fun sendEvent(event: Event) {
reducer.sendEvent(event)
val newState = reducer.reduce(_state.value, event)
_state.update { newState }
}
}

View File

@@ -7,38 +7,9 @@
package com.navi.common.basemvi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
interface BaseReducer<State : UiState, Event : UiEvent, Effect : UiEffect> {
abstract class BaseReducer<State : UiState, Effect : UiEffect>(initialState: State) {
private val _state: MutableStateFlow<State> = MutableStateFlow(initialState)
val state: StateFlow<State>
get() = _state
private val _effect: Channel<Effect> = Channel()
val effect = _effect.receiveAsFlow()
fun sendEvent(event: UiEvent) {
reduce(_state.value, event)
}
protected fun setState(reducer: State.() -> State) {
val newState = _state.value.reducer()
_state.update { newState }
}
protected fun setEffect(scope: CoroutineScope, builder: () -> Effect) {
val effectValue = builder()
scope.launch { _effect.send(effectValue) }
}
abstract fun reduce(oldState: State, event: UiEvent)
fun reduce(previousState: State, event: Event): State
}
interface UiEvent