NTP-8836 New App Update for homepage UAT changes (#14023)

This commit is contained in:
Aparna Vadlamani
2024-12-06 18:51:03 +05:30
committed by GitHub
parent 6124121f31
commit c97429db0e
7 changed files with 212 additions and 116 deletions

View File

@@ -9,19 +9,14 @@ package com.naviapp.home.compose.model
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.ui.BaseProperty
import com.naviapp.home.model.ToastData
data class ProfileAppUpdateSettingData(
val leftIconUrl: String? = null,
val titleText: String? = null,
val updateCtaText: String? = null,
val installCtaText: String? = null,
val toastData: AppUpdateToastData? = null
val toastData: ToastData? = null
) : UiTronData()
data class AppUpdateToastData(
val message: String? = null,
val leftIconUrl: String? = null,
val dismissIconUrl: String? = null
)
class ProfileAppUpdateSettingProperty : BaseProperty()

View File

@@ -7,8 +7,6 @@
package com.naviapp.home.compose.profile
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -24,18 +22,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.navi.common.utils.Constants.PROFILE_SCREEN
import com.navi.elex.atoms.ElexAsyncImage
import com.navi.elex.atoms.ElexText
@@ -44,33 +34,41 @@ import com.naviapp.R
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.listeners.InAppUpdateBridge
import com.naviapp.common.model.AppUpdateState
import com.naviapp.home.compose.model.AppUpdateToastData
import com.naviapp.home.compose.model.ProfileAppUpdateSettingData
import com.naviapp.home.model.CustomDismissibleToastState
@Composable
fun ProfileAppUpdateWidget(
state: CustomDismissibleToastState,
data: ProfileAppUpdateSettingData,
appUpdateState: AppUpdateState,
inAppUpdateBridge: InAppUpdateBridge,
appUpdateAnalytics: NaviAnalytics.InAppUpdate =
NaviAnalytics.naviAnalytics.InAppUpdate(PROFILE_SCREEN)
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.fillMaxWidth()
.clickable(enabled = (appUpdateState == AppUpdateState.UPDATE_NOT_AVAILABLE)) {
appUpdateAnalytics.appUpdateUnavailableClickEvent()
showToast(
content = { toast: Toast? -> AppUpdateToastUI(data.toastData, toast) },
context = context,
lifecycleOwner = lifecycleOwner,
savedStateRegistryOwner = savedStateRegistryOwner
)
.clickable(enabled = (appUpdateState != AppUpdateState.UPDATE_DOWNLOADING)) {
when (appUpdateState) {
AppUpdateState.UPDATE_AVAILABLE,
AppUpdateState.UPDATE_FAILED,
AppUpdateState.UPDATE_PENDING -> {
appUpdateAnalytics.appUpdateNudgeClickEvent()
inAppUpdateBridge.downloadUpdate()
}
AppUpdateState.UPDATE_DOWNLOADED -> {
appUpdateAnalytics.appInstallNudgeClickEvent()
inAppUpdateBridge.installUpdate()
}
AppUpdateState.UPDATE_NOT_AVAILABLE -> {
appUpdateAnalytics.appUpdateUnavailableClickEvent()
data.toastData?.let { state.showToast(it) }
}
AppUpdateState.UPDATE_DOWNLOADING -> {}
}
}
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp, end = 16.dp)
) {
@@ -98,10 +96,7 @@ fun ProfileAppUpdateWidget(
AppUpdateState.UPDATE_AVAILABLE,
AppUpdateState.UPDATE_FAILED,
AppUpdateState.UPDATE_PENDING -> {
UpdateButton(text = data.updateCtaText.orEmpty()) {
appUpdateAnalytics.appUpdateNudgeClickEvent()
inAppUpdateBridge.downloadUpdate()
}
UpdateButton(text = data.updateCtaText.orEmpty())
}
AppUpdateState.UPDATE_DOWNLOADING -> {
ElexText(
@@ -114,10 +109,7 @@ fun ProfileAppUpdateWidget(
)
}
AppUpdateState.UPDATE_DOWNLOADED -> {
UpdateButton(text = data.installCtaText.orEmpty()) {
appUpdateAnalytics.appInstallNudgeClickEvent()
inAppUpdateBridge.installUpdate()
}
UpdateButton(text = data.installCtaText.orEmpty())
}
AppUpdateState.UPDATE_NOT_AVAILABLE -> {}
}
@@ -125,14 +117,13 @@ fun ProfileAppUpdateWidget(
}
@Composable
private fun UpdateButton(text: String, onClick: () -> Unit) {
private fun UpdateButton(text: String) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.clip(RoundedCornerShape(4.dp))
.background(color = Color(0xFFF5F5F5), shape = RoundedCornerShape(4.dp))
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
ElexText(
@@ -145,52 +136,3 @@ private fun UpdateButton(text: String, onClick: () -> Unit) {
)
}
}
private fun showToast(
content: @Composable (toast: Toast?) -> Unit,
context: Context,
lifecycleOwner: LifecycleOwner,
savedStateRegistryOwner: SavedStateRegistryOwner
) {
var toast: Toast? = null
toast =
Toast(context).apply {
duration = Toast.LENGTH_SHORT
val views = ComposeView(context).apply { setContent { content(toast) } }
views.setViewTreeLifecycleOwner(lifecycleOwner)
views.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
view = views
}
toast.show()
}
@Composable
private fun AppUpdateToastUI(data: AppUpdateToastData?, toast: Toast?) {
Row(
modifier =
Modifier.background(color = Color(0xFFEDECFF), shape = RoundedCornerShape(4.dp))
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
ElexAsyncImage(
icon = data?.leftIconUrl,
contentDescription = "",
modifier = Modifier.height(16.dp).width(16.dp)
)
ElexText(
text = data?.message.orEmpty(),
color = Color(0xFF191919),
fontSize = 14.sp,
fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR,
lineHeight = 22.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
ElexAsyncImage(
icon = data?.dismissIconUrl,
contentDescription = "",
modifier = Modifier.height(16.dp).width(16.dp).clickable { toast?.cancel() }
)
}
}

View File

@@ -17,9 +17,11 @@ import com.navi.uitron.viewmodel.UiTronViewModel
import com.naviapp.common.listeners.InAppUpdateBridge
import com.naviapp.common.model.AppUpdateState
import com.naviapp.home.compose.model.ProfileAppUpdateSettingData
import com.naviapp.home.model.CustomDismissibleToastState
import com.naviapp.home.model.HomeCustomWidgetType
class ProfileCustomUiTronRenderer(
private val state: CustomDismissibleToastState,
private val appUpdateState: AppUpdateState,
private val inAppUpdateBridge: InAppUpdateBridge,
) : CommonCustomUiTronRenderer() {
@@ -38,6 +40,7 @@ class ProfileCustomUiTronRenderer(
dataMap?.getOrElse(APP_UPDATE_DATA) { null } as? ProfileAppUpdateSettingData
if (data != null) {
ProfileAppUpdateWidget(
state = state,
data = data,
appUpdateState = appUpdateState,
inAppUpdateBridge = inAppUpdateBridge

View File

@@ -16,6 +16,7 @@ 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -50,6 +51,8 @@ import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.forge.model.WidgetModelDefinition
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.model.ProfileHeaderWidgetData
import com.naviapp.home.model.CustomDismissibleToastState
import com.naviapp.home.ui.CustomDismissibleToast
import com.naviapp.home.ui.state.ProfileScreenState
import com.naviapp.home.viewmodel.ProfileVM
import com.naviapp.home.viewmodel.SharedVM
@@ -63,6 +66,7 @@ fun ProfileScreen(
activity: HomePageActivity,
sharedVM: SharedVM
) {
val state = remember { CustomDismissibleToastState() }
val profileItems = profileVM.profileScreenDataState.collectAsStateWithLifecycle()
val appUpdateState by sharedVM.appUpdateState.collectAsStateWithLifecycle()
val scrollState = rememberScrollState()
@@ -87,42 +91,47 @@ fun ProfileScreen(
}
is ProfileScreenState.Success -> {
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Column(
modifier =
Modifier.fillMaxSize()
.background(
profileData.data.screenStructure
?.content
?.backgroundColor
?.hexToComposeColor ?: Color.White
)
.navigationBarsPadding()
) {
// header widget
profileData.data.screenStructure?.header?.let {
ProfileScreenWidgetRenderer(
widget = it,
viewModel = profileVM,
drawerState = drawerState,
appUpdateState = appUpdateState,
inAppUpdateBridge = activity
)
}
// content widgets
Content(
scrollState = { scrollState },
Box(Modifier.fillMaxSize().navigationBarsPadding()) {
Column(
modifier =
Modifier.fillMaxSize()
.background(
profileData.data.screenStructure
?.content
?.backgroundColor
?.hexToComposeColor ?: Color.White
)
) {
profileData.data.screenStructure?.content?.widgets?.forEach { widget ->
// header widget
profileData.data.screenStructure?.header?.let {
ProfileScreenWidgetRenderer(
widget = widget,
state = state,
widget = it,
viewModel = profileVM,
drawerState = drawerState,
appUpdateState = appUpdateState,
inAppUpdateBridge = activity
)
}
// content widgets
Content(
scrollState = { scrollState },
) {
profileData.data.screenStructure?.content?.widgets?.forEach { widget
->
ProfileScreenWidgetRenderer(
state = state,
widget = widget,
viewModel = profileVM,
drawerState = drawerState,
appUpdateState = appUpdateState,
inAppUpdateBridge = activity
)
}
}
}
CustomDismissibleToast(state)
}
}
}

View File

@@ -17,12 +17,14 @@ import com.naviapp.common.listeners.InAppUpdateBridge
import com.naviapp.common.model.AppUpdateState
import com.naviapp.forge.model.WidgetModelDefinition
import com.naviapp.forge.model.WidgetTypes
import com.naviapp.home.model.CustomDismissibleToastState
import com.naviapp.home.model.HomeCustomWidgetType
import com.naviapp.home.viewmodel.ProfileVM
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProfileScreenWidgetRenderer(
state: CustomDismissibleToastState,
widget: WidgetModelDefinition<UiTronResponse>?,
viewModel: ProfileVM,
drawerState: () -> DrawerState,
@@ -36,7 +38,7 @@ fun ProfileScreenWidgetRenderer(
dataMap = widget.widgetData?.data,
uiTronViewModel = viewModel,
customUiTronRenderer =
ProfileCustomUiTronRenderer(appUpdateState, inAppUpdateBridge)
ProfileCustomUiTronRenderer(state, appUpdateState, inAppUpdateBridge)
)
.Render(composeViews = widget.widgetData?.parentComposeView.orEmpty())
}

View File

@@ -0,0 +1,38 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.model
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class CustomDismissibleToastState {
var updateState by mutableStateOf(false)
private set
private val _toastData = mutableStateOf<ToastData?>(null)
val toastData: MutableState<ToastData?> = _toastData
fun showToast(toastData: ToastData) {
_toastData.value = toastData
updateState = !updateState
}
fun isNotEmpty(): Boolean {
return _toastData.value != null
}
}
data class ToastData(
val message: String? = null,
val leftIconUrl: String? = null,
val dismissIconUrl: String? = null,
val backgroundColor: String? = null
)

View File

@@ -0,0 +1,107 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.elex.atoms.ElexAsyncImage
import com.navi.elex.atoms.ElexText
import com.navi.elex.font.FontWeightEnum
import com.navi.uitron.utils.hexToComposeColor
import com.naviapp.home.model.CustomDismissibleToastState
import java.util.Timer
import kotlin.concurrent.schedule
@Composable
fun CustomDismissibleToast(state: CustomDismissibleToastState) {
val showToast = remember { mutableStateOf(false) }
val data by rememberUpdatedState(newValue = state.toastData)
val timer = Timer("Animation Timer", true)
DisposableEffect(key1 = state.updateState) {
showToast.value = true
timer.schedule(3000L) { showToast.value = false }
onDispose {
timer.cancel()
timer.purge()
}
}
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
AnimatedVisibility(
visible = state.isNotEmpty() && showToast.value,
enter = fadeIn(),
exit = fadeOut()
) {
Row(
modifier =
Modifier.padding(start = 16.dp, end = 16.dp, bottom = 32.dp)
.background(
color =
data.value?.backgroundColor?.hexToComposeColor ?: Color.Black,
shape = RoundedCornerShape(4.dp)
)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
ElexAsyncImage(
icon = data.value?.leftIconUrl,
contentDescription = "",
modifier = Modifier.size(16.dp)
)
ElexText(
text = data.value?.message.orEmpty(),
color = Color(0xFF191919),
fontSize = 14.sp,
fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR,
lineHeight = 22.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
ElexAsyncImage(
icon = data.value?.dismissIconUrl,
contentDescription = "",
modifier =
Modifier.size(16.dp).clickable {
showToast.value = false
timer.cancel()
timer.purge()
}
)
}
}
}
}
}