NTP-2766 | Kamlesh | Coin page revamp for dx-gy-M2 (#12476)
Co-authored-by: Sohan Reddy Atukula <sohan.reddy@navi.com>
This commit is contained in:
committed by
GitHub
parent
fd715b6f3d
commit
6215884454
@@ -7,6 +7,7 @@
|
||||
|
||||
package com.navi.coin.models.states
|
||||
|
||||
import com.navi.common.alchemist.model.AlchemistScreenDefinition
|
||||
import com.navi.common.forge.model.ScreenDefinition
|
||||
import com.navi.common.network.models.ErrorMessage
|
||||
import com.navi.common.network.models.GenericErrorResponse
|
||||
@@ -17,6 +18,18 @@ sealed interface CoinHomeScreenState {
|
||||
data class Success(val data: ScreenDefinition) : CoinHomeScreenState
|
||||
}
|
||||
|
||||
sealed interface CoinHomeScreenV2State {
|
||||
data object Loading : CoinHomeScreenV2State
|
||||
|
||||
data class Success(val data: AlchemistScreenDefinition) : CoinHomeScreenV2State
|
||||
}
|
||||
|
||||
sealed interface CoinRootState {
|
||||
data object Loading : CoinRootState
|
||||
|
||||
data class Success(val data: Boolean) : CoinRootState
|
||||
}
|
||||
|
||||
sealed interface ScratchCardScreenState {
|
||||
data object Loading : ScratchCardScreenState
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@ import com.navi.coin.models.model.RedemptionStatusResponse
|
||||
import com.navi.coin.models.model.ScratchCardHistoryResponse
|
||||
import com.navi.coin.models.model.TransactionHistoryResponse
|
||||
import com.navi.coin.models.model.VpaValidation
|
||||
import com.navi.common.alchemist.model.AlchemistScreenDefinition
|
||||
import com.navi.common.forge.model.ScreenDefinition
|
||||
import com.navi.common.model.UploadDataAsyncResponse
|
||||
import com.navi.common.network.EnableResponseLogging
|
||||
import com.navi.common.network.models.GenericResponse
|
||||
import com.navi.common.network.retry.annotations.RetryPolicy
|
||||
import com.navi.rr.common.models.ABSettings
|
||||
import com.navi.rr.referral.models.ReferralContactList
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
@@ -60,6 +62,14 @@ interface RetrofitService {
|
||||
@Path("screenId") screenId: String
|
||||
): Response<GenericResponse<ScreenDefinition>>
|
||||
|
||||
@GET("/alchemist/inflate/{screenId}")
|
||||
@RetryPolicy(retryCount = 3)
|
||||
suspend fun fetchAlchemistScreenUiTronConfigs(
|
||||
@Header("Accept-Encoding") acceptEncoding: String,
|
||||
@Header("X-Target") target: String,
|
||||
@Path("screenId") screenId: String
|
||||
): Response<GenericResponse<AlchemistScreenDefinition>>
|
||||
|
||||
@GET("/reward-service/scratch-card/history")
|
||||
@RetryPolicy
|
||||
suspend fun fetchScratchCardHistoryList(
|
||||
@@ -106,6 +116,12 @@ interface RetrofitService {
|
||||
@Header("X-Target") target: String
|
||||
): Response<GenericResponse<ReferralContactList>>
|
||||
|
||||
@GET("/litmus-proxy/v1/proxy/experiment")
|
||||
suspend fun fetchABExperiment(
|
||||
@Query("name") name: String,
|
||||
@Header("X-Target") header: String
|
||||
): Response<ABSettings>
|
||||
|
||||
@GET("/referral/share-ability")
|
||||
@RetryPolicy
|
||||
suspend fun getReferralData(
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.navi.coin.models.model.VpaValidation
|
||||
import com.navi.coin.network.RetrofitService
|
||||
import com.navi.coin.utils.constant.Constants.SCREENS.COINS_LOADING_SCREEN_SCREEN_NAME
|
||||
import com.navi.coin.utils.constant.Constants.SCREENS.COINS_SCREEN_SCREEN_NAME
|
||||
import com.navi.coin.utils.constant.Constants.SCREENS.COINS_SCREEN_SCREEN_V2_NAME
|
||||
import com.navi.common.alchemist.model.AlchemistScreenDefinition
|
||||
import com.navi.common.checkmate.model.MetricInfo
|
||||
import com.navi.common.constants.DBCacheConstants
|
||||
import com.navi.common.forge.model.ScreenDefinition
|
||||
@@ -25,6 +27,7 @@ import com.navi.common.network.models.RepoResult
|
||||
import com.navi.common.utils.Constants.GZIP
|
||||
import com.navi.rr.common.network.retrofit.ResponseHandler
|
||||
import com.navi.rr.utils.cachemanager.CacheHandlerProxy
|
||||
import com.navi.rr.utils.ext.toGenericResponse
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -56,6 +59,24 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun fetchCoinHomeScreenV2(
|
||||
shouldRefresh: Boolean? = null
|
||||
): Flow<RepoResult<AlchemistScreenDefinition>> {
|
||||
return cacheHandlerProxy.fetchData<AlchemistScreenDefinition>(
|
||||
key = DBCacheConstants.COIN_HOME_SCREEN_V2_CACHE_KEY,
|
||||
shouldRefresh = shouldRefresh,
|
||||
fetchFromAlternativeSource = {
|
||||
responseHandler.handleResponse(
|
||||
retrofitService.fetchAlchemistScreenUiTronConfigs(
|
||||
acceptEncoding = GZIP,
|
||||
target = ModuleNameV2.ALCHEMIST.name,
|
||||
screenId = COINS_SCREEN_SCREEN_V2_NAME
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun validateUPIId(
|
||||
upiId: String,
|
||||
metricInfo: MetricInfo<RepoResult<UploadDataAsyncResponse>>
|
||||
@@ -125,4 +146,17 @@ constructor(
|
||||
response =
|
||||
retrofitService.getRedemptionStatus(target = ModuleNameV2.COIN.name, txnId = txnId)
|
||||
)
|
||||
|
||||
suspend fun getCoinHomeScreenVariant(expName: String) =
|
||||
cacheHandlerProxy.cacheManager.fetchAndCacheData(
|
||||
key = DBCacheConstants.COIN_HOME_SCREEN_VARIANT_CACHE_KEY,
|
||||
emitMultipleValues = false,
|
||||
fetchFromAlternativeSource = {
|
||||
responseHandler.handleResponse(
|
||||
retrofitService
|
||||
.fetchABExperiment(name = expName, header = ModuleName.LITMUS.name)
|
||||
.toGenericResponse()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.coin.ui.compose.screen
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.navi.coin.models.states.CoinRootState
|
||||
import com.navi.coin.ui.compose.common.MainScreenShimmerV2
|
||||
import com.navi.coin.vm.CoinHomeViewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
@Destination
|
||||
@RootNavGraph(start = true)
|
||||
@Composable
|
||||
fun CoinHomeScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
bundle: Bundle? = null,
|
||||
viewmodel: CoinHomeViewModel = hiltViewModel()
|
||||
) {
|
||||
val config by viewmodel.coinHomeScreenVariantState.collectAsStateWithLifecycle()
|
||||
|
||||
config.let { state ->
|
||||
when (state) {
|
||||
is CoinRootState.Loading -> {
|
||||
MainScreenShimmerV2()
|
||||
}
|
||||
is CoinRootState.Success -> {
|
||||
|
||||
if ((state).data) {
|
||||
CoinHomeScreenV2(navigator = navigator, bundle = bundle)
|
||||
} else {
|
||||
CoinHomeScreenV1(navigator = navigator, bundle = bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ import com.navi.coin.utils.constant.Constants.SCREEN_CONTENT
|
||||
import com.navi.coin.utils.constant.Constants.SHOW_CURVE_KEY
|
||||
import com.navi.coin.utils.emitEvent
|
||||
import com.navi.coin.utils.navigateTo
|
||||
import com.navi.coin.vm.CoinHomeScreenVM
|
||||
import com.navi.coin.vm.CoinHomeViewModelV1
|
||||
import com.navi.common.managers.NaviLocationManager
|
||||
import com.navi.common.navigation.NavArgs
|
||||
import com.navi.common.navigation.clearResult
|
||||
@@ -128,21 +128,17 @@ import com.navi.uitron.utils.ShapeUtil
|
||||
import com.navi.uitron.utils.conditional
|
||||
import com.navi.uitron.utils.hexToComposeColor
|
||||
import com.navi.uitron.utils.toPx
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Destination
|
||||
@RootNavGraph(start = true)
|
||||
@Composable
|
||||
fun CoinHomeScreen(
|
||||
fun CoinHomeScreenV1(
|
||||
bundle: Bundle? = null,
|
||||
navigator: DestinationsNavigator,
|
||||
coinHomeScreenVM: CoinHomeScreenVM = hiltViewModel(),
|
||||
coinHomeViewModelV1: CoinHomeViewModelV1 = hiltViewModel(),
|
||||
naviCoinsAnalytics: NaviCoinsAnalytics.BasicEvent =
|
||||
NaviCoinsAnalytics.naviCoinsAnalytics.BasicEvent(),
|
||||
) {
|
||||
@@ -158,10 +154,11 @@ fun CoinHomeScreen(
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val coinHomeScreenUiTronConfig by
|
||||
coinHomeScreenVM.coinHomeScreenState.collectAsStateWithLifecycle()
|
||||
val loadingScreenUiTronConfig by coinHomeScreenVM.nextScreenDefinitionState.collectAsState(null)
|
||||
coinHomeViewModelV1.coinHomeScreenState.collectAsStateWithLifecycle()
|
||||
val redemptionStatusScreenDefinition by
|
||||
coinHomeViewModelV1.redemptionStatusScreenDefinitionState.collectAsState(null)
|
||||
|
||||
val bottomSheetData by coinHomeScreenVM.bottomSheetData.collectAsState(null)
|
||||
val bottomSheetData by coinHomeViewModelV1.bottomSheetData.collectAsState(null)
|
||||
|
||||
val bottomSheetState =
|
||||
rememberModalBottomSheetState(
|
||||
@@ -189,15 +186,15 @@ fun CoinHomeScreen(
|
||||
}
|
||||
|
||||
fun shouldRefreshScreen() =
|
||||
(coinHomeScreenVM.handle.get<String>(BACK_FROM) == ONE_PROFILE_SCREEN ||
|
||||
coinHomeScreenVM.handle.get<String>(AUTO_REDEMPTION_STARTED) == TRUE)
|
||||
(coinHomeViewModelV1.handle.get<String>(BACK_FROM) == ONE_PROFILE_SCREEN ||
|
||||
coinHomeViewModelV1.handle.get<String>(AUTO_REDEMPTION_STARTED) == TRUE)
|
||||
.not()
|
||||
|
||||
fun renderBottomSheet(bottomSheetId: String?) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
bottomSheetId?.let {
|
||||
coinHomeScreenVM.getBottomSheetData(bottomSheetId)?.apply {
|
||||
coinHomeScreenVM.setCurrentBottomSheetData(this)
|
||||
coinHomeViewModelV1.getBottomSheetData(bottomSheetId)?.apply {
|
||||
coinHomeViewModelV1.setCurrentBottomSheetData(this)
|
||||
bottomSheetState.show()
|
||||
}
|
||||
}
|
||||
@@ -227,7 +224,7 @@ fun CoinHomeScreen(
|
||||
val screenUrl = action.ctaData?.url
|
||||
val parameters = action.ctaData?.parameters
|
||||
if (action.ctaData?.refreshScreen.orFalse()) {
|
||||
coinHomeScreenVM.fetchCoinHomeScreenUiTronConfigs(shouldRefresh = true)
|
||||
coinHomeViewModelV1.fetchCoinHomeScreenUiTronConfigs(shouldRefresh = true)
|
||||
} else {
|
||||
when (screenUrl) {
|
||||
Constants.RR_BOTTOM_SHEET -> {
|
||||
@@ -276,19 +273,19 @@ fun CoinHomeScreen(
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
bundleData?.getString(AUTO_REDEEM_KEY)?.let {
|
||||
coinHomeScreenVM.triggerAutoRedemption(it == TRUE)
|
||||
coinHomeViewModelV1.triggerAutoRedemption(it == TRUE)
|
||||
}
|
||||
coinHomeScreenVM.prefetchShareabilityImages()
|
||||
coinHomeScreenVM.prefetchReferralShareabilityImages()
|
||||
coinHomeViewModelV1.prefetchShareabilityImages()
|
||||
coinHomeViewModelV1.prefetchReferralShareabilityImages()
|
||||
naviCoinsAnalytics.sendEvent(NaviCoinsAnalytics.REWARDS_NAVI_COINS_INFO_PAGE_LANDS)
|
||||
launch {
|
||||
context.collectEvent<Int?>(TRIGGER_REDEMPTION_EVENT)?.collect { resultCode ->
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val redemptionJsonResponse =
|
||||
coinHomeScreenVM.handle.get<String?>(TRIGGER_REDEMPTION_ACTION)
|
||||
coinHomeViewModelV1.handle.get<String?>(TRIGGER_REDEMPTION_ACTION)
|
||||
val redemptionAction =
|
||||
redemptionJsonResponse?.toType<ExecuteActionsCorrespondingToKey>()
|
||||
redemptionAction?.let { coinHomeScreenVM.handleAction(redemptionAction) }
|
||||
redemptionAction?.let { coinHomeViewModelV1.handleAction(redemptionAction) }
|
||||
context.emitEvent(TRIGGER_REDEMPTION_EVENT, null)
|
||||
}
|
||||
}
|
||||
@@ -302,13 +299,13 @@ fun CoinHomeScreen(
|
||||
Lifecycle.Event.ON_START -> {
|
||||
if (
|
||||
shouldRefreshScreen() &&
|
||||
coinHomeScreenVM.apiActionState.value != ApiActionState.LOADING
|
||||
coinHomeViewModelV1.apiActionState.value != ApiActionState.LOADING
|
||||
) {
|
||||
coinHomeScreenVM.fetchCoinHomeScreenUiTronConfigs()
|
||||
coinHomeViewModelV1.fetchCoinHomeScreenUiTronConfigs()
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
coinHomeScreenVM.disableAutoRedemption()
|
||||
coinHomeViewModelV1.disableAutoRedemption()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
@@ -320,15 +317,15 @@ fun CoinHomeScreen(
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
coinHomeScreenVM.navigateToNextScreen.collect { navigator.navigate(direction = it) }
|
||||
coinHomeViewModelV1.navigateToNextScreen.collect { navigator.navigate(direction = it) }
|
||||
}
|
||||
launch {
|
||||
coinHomeScreenVM.navigateToPreviousScreen.collect { if (it) navigator.navigateUp() }
|
||||
coinHomeViewModelV1.navigateToPreviousScreen.collect { if (it) navigator.navigateUp() }
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coinHomeScreenVM.apiActionState.collect { state ->
|
||||
coinHomeViewModelV1.apiActionState.collect { state ->
|
||||
when (state) {
|
||||
ApiActionState.LOADING -> {
|
||||
context.updateLoaderState(true)
|
||||
@@ -342,11 +339,11 @@ fun CoinHomeScreen(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coinHomeScreenVM.showLoadingScreenConfig.collect { state ->
|
||||
if (state == true) {
|
||||
if (loadingScreenUiTronConfig != null) {
|
||||
coinHomeViewModelV1.showRedemptionScreen.collect { showRedemptionScreen ->
|
||||
if (showRedemptionScreen == true) {
|
||||
if (redemptionStatusScreenDefinition != null) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(SCREEN_CONTENT, loadingScreenUiTronConfig.toJson())
|
||||
bundle.putString(SCREEN_CONTENT, redemptionStatusScreenDefinition.toJson())
|
||||
val ctaData =
|
||||
CtaData(
|
||||
url = REDEMPTION_STATUS_SCREEN_CTA_URL,
|
||||
@@ -358,14 +355,14 @@ fun CoinHomeScreen(
|
||||
bundle = bundle,
|
||||
)
|
||||
)
|
||||
coinHomeScreenVM.setShowLoadingScreenConfig(false)
|
||||
coinHomeViewModelV1.setShowLoadingScreenConfig(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coinHomeScreenVM.ctaNavigation.collect { action ->
|
||||
coinHomeViewModelV1.ctaNavigation.collect { action ->
|
||||
when (action) {
|
||||
is CtaAction -> {
|
||||
handleCtaAction(action)
|
||||
@@ -375,7 +372,7 @@ fun CoinHomeScreen(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coinHomeScreenVM.screenActions.collect { action ->
|
||||
coinHomeViewModelV1.screenActions.collect { action ->
|
||||
when (action) {
|
||||
is ScrollToAction -> {
|
||||
scrollToWidget(action = action)
|
||||
@@ -387,46 +384,46 @@ fun CoinHomeScreen(
|
||||
LaunchedEffect(key1 = bottomSheetState.currentValue, bottomSheetState.targetValue) {
|
||||
if (bottomSheetState.targetValue == ModalBottomSheetValue.Hidden) {
|
||||
keyboardController?.hide()
|
||||
coinHomeScreenVM.handleActions(bottomSheetData?.widgetRenderActions?.onDisposeAction)
|
||||
coinHomeViewModelV1.handleActions(bottomSheetData?.widgetRenderActions?.onDisposeAction)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(navigateBackResult) {
|
||||
navigateBackResult?.onNavigateBack?.let {
|
||||
coinHomeScreenVM.handleActionsAsync(it)
|
||||
coinHomeViewModelV1.handleActionsAsync(it)
|
||||
navigateBackResult = null
|
||||
}
|
||||
}
|
||||
|
||||
InitActionHandler(activity = context, viewModel = coinHomeScreenVM)
|
||||
InitActionHandler(activity = context, viewModel = coinHomeViewModelV1)
|
||||
|
||||
BackHandler {
|
||||
if (coinHomeScreenVM.apiActionState.value == ApiActionState.LOADING) return@BackHandler
|
||||
if (coinHomeViewModelV1.apiActionState.value == ApiActionState.LOADING) return@BackHandler
|
||||
|
||||
if (bottomSheetState.currentValue == ModalBottomSheetValue.Expanded) {
|
||||
scope.launch {
|
||||
bottomSheetState.hide()
|
||||
coinHomeScreenVM.handleActions(
|
||||
coinHomeViewModelV1.handleActions(
|
||||
bottomSheetData?.widgetRenderActions?.onDisposeAction
|
||||
)
|
||||
}
|
||||
return@BackHandler
|
||||
}
|
||||
|
||||
coinHomeScreenVM.getSystemBackCtaAction()?.let { coinHomeScreenVM.handleActions(it) }
|
||||
coinHomeViewModelV1.getSystemBackCtaAction()?.let { coinHomeViewModelV1.handleActions(it) }
|
||||
?: run { context.finish() }
|
||||
}
|
||||
|
||||
Init(
|
||||
screenName = COIN_HOME_SCREEN,
|
||||
activity = context,
|
||||
viewModel = coinHomeScreenVM,
|
||||
viewModel = coinHomeViewModelV1,
|
||||
navigator = navigator
|
||||
)
|
||||
ModalBottomSheetLayout(
|
||||
sheetContent = {
|
||||
Column(modifier = Modifier.imePadding().background(Color.Transparent).fillMaxWidth()) {
|
||||
WidgetRenderer(widget = bottomSheetData, viewModel = coinHomeScreenVM)
|
||||
WidgetRenderer(widget = bottomSheetData, viewModel = coinHomeViewModelV1)
|
||||
}
|
||||
},
|
||||
sheetState = bottomSheetState,
|
||||
@@ -448,13 +445,13 @@ fun CoinHomeScreen(
|
||||
is CoinHomeScreenState.Success -> {
|
||||
|
||||
LaunchedEffect(state.data) {
|
||||
coinHomeScreenVM.setBottomSheetMapData(
|
||||
coinHomeViewModelV1.setBottomSheetMapData(
|
||||
state.data.screenStructure?.bottomSheets
|
||||
)
|
||||
}
|
||||
InitWidgetActions(
|
||||
screenDefinition = state.data,
|
||||
viewModel = coinHomeScreenVM
|
||||
viewModel = coinHomeViewModelV1
|
||||
)
|
||||
|
||||
var isHeroImageLoading by remember { mutableStateOf(false) }
|
||||
@@ -484,7 +481,7 @@ fun CoinHomeScreen(
|
||||
Modifier.fillMaxSize().verticalScroll(scrollState)
|
||||
) {
|
||||
CoinHomeScreenBody(
|
||||
coinHomeScreenVM = coinHomeScreenVM,
|
||||
coinHomeViewModelV1 = coinHomeViewModelV1,
|
||||
state = state,
|
||||
heroPlaceholderProperty = heroPlaceholderProperty,
|
||||
widgetYOffsetMap = widgetYOffsetMap,
|
||||
@@ -512,7 +509,7 @@ fun CoinHomeScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CoinHomeScreenHeader(
|
||||
private fun CoinHomeScreenHeader(
|
||||
context: CoinBaseActivity,
|
||||
naviCoinsAnalytics: NaviCoinsAnalytics.BasicEvent =
|
||||
NaviCoinsAnalytics.naviCoinsAnalytics.BasicEvent()
|
||||
@@ -546,7 +543,7 @@ internal fun CoinHomeScreenHeader(
|
||||
|
||||
@Composable
|
||||
private fun CoinHomeScreenBody(
|
||||
coinHomeScreenVM: CoinHomeScreenVM,
|
||||
coinHomeViewModelV1: CoinHomeViewModelV1,
|
||||
state: CoinHomeScreenState.Success,
|
||||
heroPlaceholderProperty: BaseProperty?,
|
||||
isHeroImageLoading: Boolean,
|
||||
@@ -625,7 +622,7 @@ private fun CoinHomeScreenBody(
|
||||
key(uiTronWidget?.widgetName.orElse(index.toString())) {
|
||||
WidgetRenderer(
|
||||
widget = uiTronWidget,
|
||||
viewModel = coinHomeScreenVM,
|
||||
viewModel = coinHomeViewModelV1,
|
||||
modifier =
|
||||
Modifier.onGloballyPositioned { coordinates ->
|
||||
widgetYOffsetMap[
|
||||
|
||||
@@ -0,0 +1,813 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.coin.ui.compose.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.splineBasedDecay
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.gestures.animateTo
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ModalBottomSheetLayout
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInParent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.navi.base.model.CtaData
|
||||
import com.navi.base.model.CtaType
|
||||
import com.navi.base.utils.isNotNull
|
||||
import com.navi.base.utils.orElse
|
||||
import com.navi.base.utils.orFalse
|
||||
import com.navi.coin.models.states.ApiActionState
|
||||
import com.navi.coin.models.states.CoinHomeScreenV2State
|
||||
import com.navi.coin.ui.activity.CoinBaseActivity
|
||||
import com.navi.coin.ui.compose.common.Init
|
||||
import com.navi.coin.ui.compose.common.MainScreenShimmerV2
|
||||
import com.navi.coin.utils.BackdropState
|
||||
import com.navi.coin.utils.TopNaviMiddlePillAnimation
|
||||
import com.navi.coin.utils.advancedShadow
|
||||
import com.navi.coin.utils.analytics.NaviCoinsAnalytics
|
||||
import com.navi.coin.utils.analytics.NaviCoinsAnalytics.Companion.REWARDS_NAVI_COINS_INFO_PAGE_BACK_ARROW_CLICK
|
||||
import com.navi.coin.utils.backDropNestedScrollConnection
|
||||
import com.navi.coin.utils.collectEvent
|
||||
import com.navi.coin.utils.constant.CoinHomeScreenConstants.AUTO_REDEMPTION_STARTED
|
||||
import com.navi.coin.utils.constant.Constants
|
||||
import com.navi.coin.utils.constant.Constants.BACK
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreen.BACK_FROM
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreen.ONE_PROFILE_SCREEN
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreen.OPEN_APP_SETTINGS
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreen.SHOW_BACK_LAYER
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreen.TRIGGER_REDEMPTION_ACTION
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreen.TRIGGER_REDEMPTION_EVENT
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreenV2.APP_BAR_HEIGHT
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreenV2.BACK_LAYER_BANNER_HEIGHT
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreenV2.DRAG_POSITIONAL_THRESHOLD
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreenV2.DRAG_VELOCITY_THRESHOLD
|
||||
import com.navi.coin.utils.constant.Constants.CoinHomeScreenV2.SHAPE_CURVATURE
|
||||
import com.navi.coin.utils.constant.Constants.REDEMPTION_STATUS_SCREEN_CTA_URL
|
||||
import com.navi.coin.utils.constant.Constants.SCREEN_CONTENT
|
||||
import com.navi.coin.utils.emitEvent
|
||||
import com.navi.coin.utils.getFrontLayerOffset
|
||||
import com.navi.coin.utils.navigateTo
|
||||
import com.navi.coin.vm.CoinHomeViewModelV2
|
||||
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
|
||||
import com.navi.common.extensions.conditional
|
||||
import com.navi.common.navigation.NavArgs
|
||||
import com.navi.common.navigation.NavigationAction
|
||||
import com.navi.common.navigation.clearResult
|
||||
import com.navi.common.navigation.rememberNavigatorWithResultAndParams
|
||||
import com.navi.common.uitron.model.action.CtaAction
|
||||
import com.navi.common.uitron.model.action.ExecuteActionsCorrespondingToKey
|
||||
import com.navi.common.utils.getStatusBarHeight
|
||||
import com.navi.design.utils.NoRippleIndicationSource
|
||||
import com.navi.rr.common.actions.InitActionHandler
|
||||
import com.navi.rr.common.constants.COIN_HOME_SCREEN_V2
|
||||
import com.navi.rr.common.widgetFactory.WidgetRenderer
|
||||
import com.navi.rr.uitron.model.action.NavigateBackWithResultAction
|
||||
import com.navi.rr.uitron.model.action.ScrollToAction
|
||||
import com.navi.rr.uitron.render.RRCustomUiTronRenderer
|
||||
import com.navi.rr.utils.composeutils.InitWidgetActions
|
||||
import com.navi.rr.utils.constants.Constants.AUTO_REDEEM_KEY
|
||||
import com.navi.rr.utils.constants.Constants.TRUE
|
||||
import com.navi.rr.utils.dpToPx
|
||||
import com.navi.rr.utils.ext.toJson
|
||||
import com.navi.rr.utils.ext.toType
|
||||
import com.navi.rr.utils.openSettings
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.navi.uitron.render.UiTronRenderer
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun CoinHomeScreenV2(
|
||||
bundle: Bundle? = null,
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: CoinHomeViewModelV2 = hiltViewModel(),
|
||||
eventHandler: NaviCoinsAnalytics.BasicEvent =
|
||||
NaviCoinsAnalytics.naviCoinsAnalytics.BasicEvent(),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
val widgetYOffsetMap = remember { mutableStateMapOf<String, Int>() }
|
||||
val scrollState = rememberScrollState()
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val context = LocalContext.current as CoinBaseActivity
|
||||
val bundleData = bundle ?: context.intent.extras
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val showBackScaffold by viewModel.showBackLayer.collectAsStateWithLifecycle()
|
||||
|
||||
val coinHomeScreenUiTronConfig by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val redemptionStatusScreenDefinitionState by
|
||||
viewModel.redemptionStatusScreenDefinitionState.collectAsState(null)
|
||||
|
||||
val bottomSheetData by viewModel.bottomSheetDataAlchemist.collectAsState(null)
|
||||
|
||||
val bottomSheetState =
|
||||
rememberModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Hidden,
|
||||
skipHalfExpanded = true,
|
||||
)
|
||||
|
||||
var navigateBackResult by remember { mutableStateOf<NavigateBackWithResultAction?>(null) }
|
||||
val navigateBackWithResult =
|
||||
rememberNavigatorWithResultAndParams<NavigateBackWithResultAction?>(
|
||||
context = context,
|
||||
navigate = {
|
||||
navigateTo(
|
||||
activity = context,
|
||||
navHostOwner = context,
|
||||
navArgs = it,
|
||||
)
|
||||
},
|
||||
navController = context.navController,
|
||||
) { result ->
|
||||
navigateBackResult = result
|
||||
context.navController.clearResult()
|
||||
}
|
||||
|
||||
fun shouldRefreshScreen(): Boolean {
|
||||
val backFrom = viewModel.handle.get<String>(BACK_FROM) == ONE_PROFILE_SCREEN
|
||||
val autoRedemptionStarted = viewModel.handle.get<String>(AUTO_REDEMPTION_STARTED) == TRUE
|
||||
return !(backFrom || autoRedemptionStarted)
|
||||
}
|
||||
|
||||
fun renderBottomSheet(bottomSheetId: String?) {
|
||||
if (bottomSheetId == null) return
|
||||
scope.launch(Dispatchers.Default) {
|
||||
bottomSheetId.let {
|
||||
viewModel.getBottomSheetScreenData(bottomSheetId)?.apply {
|
||||
viewModel.setCurrentBottomSheetData(this)
|
||||
bottomSheetState.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendLocationUpdates() {
|
||||
if (viewModel.locationManager.isLocationOn(context)) {
|
||||
viewModel.locationManager.requestLocationUpdates(context, postLocation = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollToWidget(action: ScrollToAction?) {
|
||||
if (action?.widgetName == null) return
|
||||
|
||||
var yOffset = widgetYOffsetMap[action.widgetName] ?: return
|
||||
|
||||
action.offset?.let { yOffset += dpToPx(it).toInt() }
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val targetOffset = maxOf(yOffset, 0)
|
||||
scrollState.animateScrollTo(targetOffset, spring(stiffness = Spring.StiffnessLow))
|
||||
}
|
||||
}
|
||||
|
||||
fun handleCtaAction(action: CtaAction) {
|
||||
val screenUrl = action.ctaData?.url
|
||||
if (action.ctaData?.refreshScreen.orFalse()) {
|
||||
viewModel.fetchScreenConfig(shouldRefresh = true)
|
||||
} else {
|
||||
when (screenUrl) {
|
||||
Constants.RR_BOTTOM_SHEET -> {
|
||||
when (action.ctaData?.action) {
|
||||
Constants.BottomSheetAction.SHOW.name -> {
|
||||
val bottomSheetName = action.ctaData?.type
|
||||
scope.launch { renderBottomSheet(bottomSheetName) }
|
||||
}
|
||||
Constants.BottomSheetAction.HIDE.name -> {
|
||||
scope.launch { bottomSheetState.hide() }
|
||||
}
|
||||
}
|
||||
}
|
||||
OPEN_APP_SETTINGS -> {
|
||||
openSettings(context)
|
||||
}
|
||||
SHOW_BACK_LAYER -> {
|
||||
viewModel.setShowBackLayer(true)
|
||||
}
|
||||
else -> {
|
||||
action.ctaData?.let {
|
||||
navigateTo(
|
||||
activity = context,
|
||||
navHostOwner = context,
|
||||
navArgs =
|
||||
NavArgs(
|
||||
ctaData = it,
|
||||
bundle = bundle,
|
||||
finish = it.finish.orFalse(),
|
||||
needsResult = it.needsResult,
|
||||
requestCode = it.requestCode,
|
||||
clearTask = it.clearTask,
|
||||
),
|
||||
navAction =
|
||||
(if (it.url == BACK) NavigationAction.Back
|
||||
else NavigationAction.Default)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
bundleData?.getString(AUTO_REDEEM_KEY)?.let { viewModel.triggerAutoRedemption(it == TRUE) }
|
||||
viewModel.prefetchShareabilityImages()
|
||||
eventHandler.sendEvent(NaviCoinsAnalytics.REWARDS_NAVI_COINS_INFO_PAGE_LANDS)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
context.collectEvent<Int?>(TRIGGER_REDEMPTION_EVENT)?.collect { resultCode ->
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val redemptionJsonResponse =
|
||||
viewModel.handle.get<String?>(TRIGGER_REDEMPTION_ACTION)
|
||||
val redemptionAction =
|
||||
redemptionJsonResponse?.toType<ExecuteActionsCorrespondingToKey>()
|
||||
redemptionAction?.let { viewModel.handleAction(redemptionAction) }
|
||||
context.emitEvent(TRIGGER_REDEMPTION_EVENT, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
sendLocationUpdates()
|
||||
}
|
||||
|
||||
DisposableEffect(key1 = lifeCycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
if (
|
||||
shouldRefreshScreen() &&
|
||||
viewModel.apiActionState.value != ApiActionState.LOADING
|
||||
) {
|
||||
viewModel.fetchScreenConfig()
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
viewModel.disableAutoRedemption()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.apiActionState.collect { state ->
|
||||
when (state) {
|
||||
ApiActionState.LOADING -> {
|
||||
context.updateLoaderState(true)
|
||||
}
|
||||
ApiActionState.SUCCESS,
|
||||
ApiActionState.FAILURE -> {
|
||||
context.updateLoaderState(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.showRedemptionScreen.collect { state ->
|
||||
if (state == true) {
|
||||
if (redemptionStatusScreenDefinitionState != null) {
|
||||
val redemptionBundle = Bundle()
|
||||
redemptionBundle.putString(
|
||||
SCREEN_CONTENT,
|
||||
redemptionStatusScreenDefinitionState.toJson()
|
||||
)
|
||||
val ctaData =
|
||||
CtaData(
|
||||
url = REDEMPTION_STATUS_SCREEN_CTA_URL,
|
||||
type = CtaType.REDIRECTION_CTA.name
|
||||
)
|
||||
navigateBackWithResult(
|
||||
NavArgs(
|
||||
ctaData = ctaData,
|
||||
bundle = redemptionBundle,
|
||||
)
|
||||
)
|
||||
viewModel.setShowLoadingScreenConfig(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.screenActions.collect { action ->
|
||||
when (action) {
|
||||
is ScrollToAction -> {
|
||||
scrollToWidget(action = action)
|
||||
}
|
||||
is CtaAction -> {
|
||||
handleCtaAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = bottomSheetState.currentValue, bottomSheetState.targetValue) {
|
||||
if (bottomSheetState.targetValue == ModalBottomSheetValue.Hidden) {
|
||||
keyboardController?.hide()
|
||||
viewModel.handleActions(bottomSheetData?.onDismissAction)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(navigateBackResult) {
|
||||
navigateBackResult?.onNavigateBack?.let {
|
||||
viewModel.handleActionsAsync(it)
|
||||
navigateBackResult = null
|
||||
}
|
||||
}
|
||||
|
||||
InitActionHandler(activity = context, viewModel = viewModel)
|
||||
|
||||
BackHandler {
|
||||
if (viewModel.apiActionState.value == ApiActionState.LOADING) return@BackHandler
|
||||
|
||||
if (bottomSheetState.currentValue == ModalBottomSheetValue.Expanded) {
|
||||
scope.launch {
|
||||
bottomSheetState.hide()
|
||||
viewModel.handleActions(bottomSheetData?.onDismissAction)
|
||||
}
|
||||
return@BackHandler
|
||||
}
|
||||
|
||||
viewModel.getSystemBackCtaAction()?.let { viewModel.handleActions(it) }
|
||||
?: run { context.finish() }
|
||||
}
|
||||
|
||||
Init(
|
||||
screenName = COIN_HOME_SCREEN_V2,
|
||||
activity = context,
|
||||
viewModel = viewModel,
|
||||
navigator = navigator
|
||||
)
|
||||
ModalBottomSheetLayout(
|
||||
sheetContent = {
|
||||
Column(modifier = Modifier.imePadding().background(Color.Transparent).fillMaxWidth()) {
|
||||
bottomSheetData
|
||||
?.content
|
||||
?.widgets
|
||||
?.filter { it.isNotNull() }
|
||||
?.forEach { widget -> WidgetRenderer(widget = widget, viewModel = viewModel) }
|
||||
}
|
||||
},
|
||||
sheetState = bottomSheetState,
|
||||
sheetBackgroundColor = Color.Transparent,
|
||||
scrimColor = Color.Black.copy(alpha = 0.9f)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color.White)) {
|
||||
coinHomeScreenUiTronConfig.let { state ->
|
||||
when (state) {
|
||||
is CoinHomeScreenV2State.Loading -> {
|
||||
MainScreenShimmerV2()
|
||||
SideEffect {
|
||||
systemUiController.setStatusBarColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = true
|
||||
)
|
||||
}
|
||||
}
|
||||
is CoinHomeScreenV2State.Success -> {
|
||||
|
||||
var backdropValue =
|
||||
remember(
|
||||
state.data.screenStructure
|
||||
?.collapsingToolbar
|
||||
?.collapsingTopNavConfig
|
||||
?.enableExpandState
|
||||
) {
|
||||
if (
|
||||
state.data.screenStructure
|
||||
?.collapsingToolbar
|
||||
?.collapsingTopNavConfig
|
||||
?.enableExpandState == true
|
||||
) {
|
||||
BackdropState.Concealed
|
||||
} else {
|
||||
BackdropState.Revealed
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.data) {
|
||||
viewModel.setBottomSheetScreenMapData(
|
||||
state.data.screenStructure?.bottomSheets
|
||||
)
|
||||
}
|
||||
InitWidgetActions(screenDefinition = state.data, viewModel = viewModel)
|
||||
|
||||
CoinHomeScreenScaffoldRoot(
|
||||
state = { state },
|
||||
scrollState = { scrollState },
|
||||
viewModel = { viewModel },
|
||||
showBackScaffold = showBackScaffold,
|
||||
backdropValue = backdropValue,
|
||||
setWidgetYPosition = { widgetName, yPosition ->
|
||||
widgetYOffsetMap[widgetName] = yPosition
|
||||
},
|
||||
) { uiTronResponse ->
|
||||
uiTronResponse?.let {
|
||||
UiTronRenderer(
|
||||
dataMap = uiTronResponse.data,
|
||||
uiTronViewModel = viewModel,
|
||||
customUiTronRenderer =
|
||||
RRCustomUiTronRenderer(
|
||||
parentScrollState = { scrollState }
|
||||
)
|
||||
)
|
||||
.Render(
|
||||
composeViews = uiTronResponse.parentComposeView.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
CoinHomeScreenHeader(context)
|
||||
SideEffect {
|
||||
systemUiController.setStatusBarColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CoinHomeScreenHeader(
|
||||
context: CoinBaseActivity,
|
||||
eventHandler: NaviCoinsAnalytics.BasicEvent = NaviCoinsAnalytics.naviCoinsAnalytics.BasicEvent()
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val statusBarHeight = remember { with(density) { getStatusBarHeight().toDp() } }
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
interactionSource = NoRippleIndicationSource(),
|
||||
indication = null,
|
||||
onClick = {
|
||||
eventHandler.sendEvent(REWARDS_NAVI_COINS_INFO_PAGE_BACK_ARROW_CLICK)
|
||||
context.finish()
|
||||
}
|
||||
)
|
||||
) {
|
||||
Box(modifier = Modifier.navigationBarsPadding().padding(top = statusBarHeight)) {
|
||||
Column(modifier = Modifier.padding(start = 16.dp, end = 40.dp, top = 12.dp)) {
|
||||
Column(
|
||||
modifier = Modifier.size(40.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.width(32.dp).height(32.dp),
|
||||
painter = painterResource(com.navi.rr.R.drawable.translucent_arrow),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CoinHomeScreenScaffoldRoot(
|
||||
state: () -> CoinHomeScreenV2State.Success,
|
||||
scrollState: () -> ScrollState,
|
||||
viewModel: () -> CoinHomeViewModelV2,
|
||||
showBackScaffold: Boolean = false,
|
||||
backdropValue: BackdropState,
|
||||
setWidgetYPosition: (String, Int) -> Unit,
|
||||
coinHomeWidgetRenderer: @Composable (UiTronResponse?) -> Unit
|
||||
) {
|
||||
state().data.screenStructure?.collapsingToolbar?.toolBarNav?.uiTronResponse?.let {
|
||||
val density = LocalDensity.current
|
||||
val statusBarHeight = remember { with(density) { getStatusBarHeight().toDp() } }
|
||||
val appBarHeight = remember { APP_BAR_HEIGHT + statusBarHeight }
|
||||
val backLayerHeight = remember { BACK_LAYER_BANNER_HEIGHT + appBarHeight }
|
||||
val frontLayerShape = remember {
|
||||
RoundedCornerShape(topStart = SHAPE_CURVATURE, topEnd = SHAPE_CURVATURE)
|
||||
}
|
||||
Box(Modifier.fillMaxSize().navigationBarsPadding()) {
|
||||
BackLayerContent(
|
||||
backLayerHeight = backLayerHeight,
|
||||
coinHomeWidgetRenderer = coinHomeWidgetRenderer,
|
||||
backLayerData =
|
||||
(state().data.screenStructure?.collapsingToolbar?.collapsingTopNav)
|
||||
?.uiTronResponse
|
||||
)
|
||||
|
||||
FrontLayerRoot(
|
||||
state = state,
|
||||
scrollState = scrollState,
|
||||
viewModel = viewModel,
|
||||
frontLayerShape = frontLayerShape,
|
||||
appBarHeight = appBarHeight,
|
||||
backLayerHeight = backLayerHeight,
|
||||
showBackScaffold = showBackScaffold,
|
||||
backdropValue = backdropValue,
|
||||
setWidgetYPosition = setWidgetYPosition
|
||||
)
|
||||
|
||||
CoinHomeTopBar(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
appBarHeight = appBarHeight,
|
||||
statusBarHeight = statusBarHeight,
|
||||
topBarContent =
|
||||
(state().data.screenStructure?.collapsingToolbar?.toolBarNav)?.uiTronResponse,
|
||||
coinHomeWidgetRenderer = coinHomeWidgetRenderer,
|
||||
scrollState = scrollState
|
||||
)
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
Column(Modifier.fillMaxHeight().background(Color.White).verticalScroll(scrollState())) {
|
||||
(state().data)
|
||||
.screenStructure
|
||||
?.content
|
||||
?.widgets
|
||||
?.filter { it.isNotNull() }
|
||||
?.forEachIndexed { index, widget ->
|
||||
key(widget.widgetName.orElse(index.toString())) {
|
||||
WidgetRenderer(
|
||||
widget = widget,
|
||||
viewModel = viewModel(),
|
||||
scrollState = scrollState,
|
||||
modifier =
|
||||
Modifier.onGloballyPositioned { coordinates ->
|
||||
setWidgetYPosition(
|
||||
widget.widgetName.orElse(index.toString()),
|
||||
coordinates.positionInParent().y.toInt()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun FrontLayerRoot(
|
||||
state: () -> CoinHomeScreenV2State.Success,
|
||||
scrollState: () -> ScrollState,
|
||||
viewModel: () -> CoinHomeViewModelV2,
|
||||
frontLayerShape: RoundedCornerShape,
|
||||
appBarHeight: Dp,
|
||||
backLayerHeight: Dp,
|
||||
showBackScaffold: Boolean,
|
||||
backdropValue: BackdropState,
|
||||
setWidgetYPosition: (String, Int) -> Unit
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
val backdropHeightInPx = remember { with(density) { backLayerHeight.toPx() } }
|
||||
val appBarHeightInPx = remember { with(density) { appBarHeight.toPx() } }
|
||||
val positionalThreshold = remember { with(density) { DRAG_POSITIONAL_THRESHOLD.toPx() } }
|
||||
val velocityThreshold = remember { with(density) { DRAG_VELOCITY_THRESHOLD.toPx() } }
|
||||
|
||||
var initialDraggableState = rememberSaveable { mutableStateOf(backdropValue) }
|
||||
|
||||
val draggableState = remember {
|
||||
AnchoredDraggableState(
|
||||
initialValue = initialDraggableState.value,
|
||||
anchors =
|
||||
DraggableAnchors {
|
||||
BackdropState.Concealed at appBarHeightInPx
|
||||
BackdropState.Revealed at backdropHeightInPx
|
||||
},
|
||||
positionalThreshold = { positionalThreshold },
|
||||
velocityThreshold = { velocityThreshold },
|
||||
snapAnimationSpec = tween(durationMillis = 100, easing = FastOutSlowInEasing),
|
||||
decayAnimationSpec = splineBasedDecay(density),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(showBackScaffold) {
|
||||
if (showBackScaffold) {
|
||||
scrollState().animateScrollTo(0, tween(durationMillis = 100))
|
||||
draggableState.animateTo(BackdropState.Revealed)
|
||||
viewModel().setShowBackLayer(false)
|
||||
}
|
||||
}
|
||||
|
||||
FrontLayerContent(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.offset { getFrontLayerOffset({ draggableState }, appBarHeightInPx) }
|
||||
.nestedScroll(backDropNestedScrollConnection(draggableState))
|
||||
.anchoredDraggable(state = draggableState, orientation = Orientation.Vertical)
|
||||
.padding(bottom = appBarHeight),
|
||||
widgets = state().data.screenStructure?.content?.widgets,
|
||||
frontLayerShape = frontLayerShape,
|
||||
scrollState = scrollState,
|
||||
viewModel = viewModel,
|
||||
setWidgetYPosition = setWidgetYPosition
|
||||
)
|
||||
|
||||
LaunchedEffect(draggableState.settledValue) {
|
||||
initialDraggableState.value = draggableState.settledValue
|
||||
}
|
||||
|
||||
TopNaviMiddlePillAnimation(viewModel = viewModel, backdropState = { draggableState })
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackLayerContent(
|
||||
backLayerHeight: Dp,
|
||||
backLayerData: UiTronResponse?,
|
||||
coinHomeWidgetRenderer: @Composable (UiTronResponse?) -> Unit
|
||||
) {
|
||||
Column(Modifier.requiredHeight(backLayerHeight + SHAPE_CURVATURE).fillMaxWidth()) {
|
||||
coinHomeWidgetRenderer(backLayerData)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CoinHomeTopBar(
|
||||
modifier: Modifier,
|
||||
appBarHeight: Dp,
|
||||
statusBarHeight: Dp,
|
||||
topBarContent: UiTronResponse?,
|
||||
coinHomeWidgetRenderer: @Composable (UiTronResponse) -> Unit,
|
||||
scrollState: () -> ScrollState
|
||||
) {
|
||||
val isScrollingUp by remember { derivedStateOf { scrollState().value.dp < (appBarHeight) } }
|
||||
// Set the modifier for the top bar based on scrolling direction
|
||||
val topBarModifier =
|
||||
remember(isScrollingUp) {
|
||||
modifier
|
||||
.conditional(!isScrollingUp) { requiredHeight(appBarHeight + 6.dp) }
|
||||
.applyBottomShadow(showShadow = isScrollingUp.not(), elevation = 24f)
|
||||
.drawBehind {
|
||||
drawRect(color = if (isScrollingUp) Color.Transparent else Color(0xFF400455))
|
||||
}
|
||||
}
|
||||
|
||||
// Render the top bar
|
||||
Row(modifier = topBarModifier, verticalAlignment = Alignment.Top) {
|
||||
Row(Modifier.padding(top = statusBarHeight)) {
|
||||
topBarContent?.let { coinHomeWidgetRenderer(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
|
||||
fun FrontLayerContent(
|
||||
modifier: Modifier,
|
||||
frontLayerShape: Shape,
|
||||
widgets: List<AlchemistWidgetModelDefinition<UiTronResponse>>?,
|
||||
scrollState: () -> ScrollState,
|
||||
viewModel: () -> CoinHomeViewModelV2,
|
||||
setWidgetYPosition: (String, Int) -> Unit
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
modifier.advancedShadow(
|
||||
color = Color(0xFFFFFFFF),
|
||||
alpha = 0.3f,
|
||||
cornersRadius = 16.dp,
|
||||
shadowBlurRadius = 16.dp,
|
||||
offsetY = (-6).dp,
|
||||
offsetX = 0.dp,
|
||||
)
|
||||
|
||||
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
|
||||
Column(
|
||||
modifier
|
||||
.advancedShadow(
|
||||
color = Color(0xFFFFFFFF),
|
||||
alpha = 0.3f,
|
||||
cornersRadius = 16.dp,
|
||||
shadowBlurRadius = 16.dp,
|
||||
offsetY = (-6).dp,
|
||||
offsetX = 0.dp,
|
||||
)
|
||||
.background(Color.White, frontLayerShape)
|
||||
.verticalScroll(scrollState())
|
||||
) {
|
||||
widgets
|
||||
?.filter { it.isNotNull() }
|
||||
?.forEachIndexed { index, widget ->
|
||||
key(widget.widgetName.orElse(index.toString())) {
|
||||
WidgetRenderer(
|
||||
widget = widget,
|
||||
viewModel = viewModel(),
|
||||
scrollState = scrollState,
|
||||
modifier =
|
||||
Modifier.onGloballyPositioned { coordinates ->
|
||||
setWidgetYPosition(
|
||||
widget.widgetName.orElse(index.toString()),
|
||||
coordinates.positionInParent().y.toInt()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.applyBottomShadow(
|
||||
showShadow: Boolean = true,
|
||||
elevation: Float = 24f
|
||||
): Modifier =
|
||||
this.then(
|
||||
Modifier.drawBehind {
|
||||
drawRect(
|
||||
brush =
|
||||
when (showShadow) {
|
||||
true ->
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color.Black.copy(0.09f), Color.Transparent),
|
||||
startY = size.height,
|
||||
endY = size.height + elevation.times(2)
|
||||
)
|
||||
false ->
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color.Transparent, Color.Transparent)
|
||||
)
|
||||
},
|
||||
topLeft = Offset(0f, size.height),
|
||||
size = Size(size.width, elevation.times(2))
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.coin.utils
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.navi.coin.vm.CoinHomeViewModelV2
|
||||
import com.navi.common.utils.Constants.SCROLL_FADE
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TopNaviMiddlePillAnimation(
|
||||
viewModel: () -> CoinHomeViewModelV2,
|
||||
backdropState: () -> AnchoredDraggableState<BackdropState>
|
||||
) {
|
||||
LaunchedEffect(
|
||||
backdropState().progress(from = BackdropState.Concealed, to = BackdropState.Revealed)
|
||||
) {
|
||||
viewModel().viewModelScope.launch {
|
||||
viewModel().handle[SCROLL_FADE] =
|
||||
1 -
|
||||
backdropState()
|
||||
.progress(from = BackdropState.Concealed, to = BackdropState.Revealed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun getFrontLayerOffset(
|
||||
draggableState: () -> AnchoredDraggableState<BackdropState>,
|
||||
defaultOffset: Float
|
||||
): IntOffset {
|
||||
val draggableOffset = draggableState().offset
|
||||
val yOffset =
|
||||
if (draggableOffset.isNaN()) {
|
||||
defaultOffset
|
||||
} else draggableOffset
|
||||
return IntOffset(x = 0, y = yOffset.roundToInt())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun backDropNestedScrollConnection(
|
||||
state: AnchoredDraggableState<BackdropState>,
|
||||
) =
|
||||
object : NestedScrollConnection {
|
||||
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.toFloat()
|
||||
return if (delta < 0 && source == NestedScrollSource.UserInput) {
|
||||
state.dispatchRawDelta(delta).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
return if (source == NestedScrollSource.UserInput) {
|
||||
state.dispatchRawDelta(available.toFloat()).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val toFling = available.toFloat()
|
||||
val currentOffset = state.requireOffset()
|
||||
return if (toFling < 0 && currentOffset > state.anchors.minAnchor()) {
|
||||
state.settle(toFling)
|
||||
available
|
||||
} else {
|
||||
Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
state.settle(velocity = available.toFloat())
|
||||
return available
|
||||
}
|
||||
|
||||
private fun Float.toOffset(): Offset = Offset(x = 0f, y = this)
|
||||
|
||||
@JvmName("velocityToFloat") private fun Velocity.toFloat() = y
|
||||
|
||||
@JvmName("offsetToFloat") private fun Offset.toFloat(): Float = y
|
||||
}
|
||||
|
||||
enum class BackdropState {
|
||||
Concealed,
|
||||
Revealed
|
||||
}
|
||||
@@ -7,10 +7,17 @@
|
||||
|
||||
package com.navi.coin.utils
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
@@ -127,3 +134,36 @@ suspend fun cacheListOfImageUrls(imageUrls: List<String>) =
|
||||
.allowHardware(false)
|
||||
imageUrls.forEach { context.imageLoader.enqueue(request.data(it).build()) }
|
||||
}
|
||||
|
||||
fun Modifier.advancedShadow(
|
||||
color: Color = Color.Black,
|
||||
alpha: Float = 1f,
|
||||
cornersRadius: Dp = 0.dp,
|
||||
shadowBlurRadius: Dp = 0.dp,
|
||||
offsetY: Dp = 0.dp,
|
||||
offsetX: Dp = 0.dp
|
||||
) = drawBehind {
|
||||
val shadowColor = color.copy(alpha = alpha).toArgb()
|
||||
val transparentColor = color.copy(alpha = 0f).toArgb()
|
||||
|
||||
drawIntoCanvas {
|
||||
val paint = Paint()
|
||||
val frameworkPaint = paint.asFrameworkPaint()
|
||||
frameworkPaint.color = transparentColor
|
||||
frameworkPaint.setShadowLayer(
|
||||
shadowBlurRadius.toPx(),
|
||||
offsetX.toPx(),
|
||||
offsetY.toPx(),
|
||||
shadowColor
|
||||
)
|
||||
it.drawRoundRect(
|
||||
0f,
|
||||
0f,
|
||||
this.size.width,
|
||||
this.size.height,
|
||||
cornersRadius.toPx(),
|
||||
cornersRadius.toPx(),
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
package com.navi.coin.utils.constant
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object Constants {
|
||||
|
||||
const val COINS_SCREEN = "coins_screen_lands"
|
||||
@@ -71,6 +73,7 @@ object Constants {
|
||||
|
||||
object SCREENS {
|
||||
const val COINS_SCREEN_SCREEN_NAME = "COINS_HOME_SCREEN"
|
||||
const val COINS_SCREEN_SCREEN_V2_NAME = "COINS_HOME_SCREEN_V2"
|
||||
const val TRANSACTION_HISTORY_COINS_SCREEN_NAME = "TRANSACTION_HISTORY_COIN_BALANCE_SCREEN"
|
||||
const val TRANSACTION_HISTORY_CASH_SCREEN_NAME = "TRANSACTION_HISTORY_CASH_BALANCE_SCREEN"
|
||||
const val REWARDS_SCRATCH_CARD_SUMMARY_SCREEN_NAME = "REWARDS_SCRATCH_CARD_SUMMARY"
|
||||
@@ -86,6 +89,16 @@ object Constants {
|
||||
const val ONE_PROFILE_VERIFICATION_REQUEST_CODE = 1001
|
||||
const val ONE_PROFILE_SCREEN = "ONE_PROFILE_SCREEN"
|
||||
const val BACK_FROM = "back_from"
|
||||
const val SHOW_BACK_LAYER = "SHOW_BACK_LAYER"
|
||||
}
|
||||
|
||||
object CoinHomeScreenV2 {
|
||||
val BACK_LAYER_BANNER_HEIGHT = 264.dp
|
||||
val APP_BAR_HEIGHT = 60.dp
|
||||
val SHAPE_CURVATURE = 8.dp
|
||||
val FRONT_LAYER_ELEVATION = 20.dp
|
||||
val DRAG_POSITIONAL_THRESHOLD = 56.dp
|
||||
val DRAG_VELOCITY_THRESHOLD = 125.dp
|
||||
}
|
||||
|
||||
const val CLOSE = "close"
|
||||
|
||||
@@ -9,4 +9,5 @@ package com.navi.coin.utils.constant
|
||||
|
||||
object CoinHomeScreenConstants {
|
||||
const val AUTO_REDEMPTION_STARTED = "auto_redemption_started"
|
||||
const val COIN_HOME_SCREEN_VARIANT_EXPERIMENT_NAME = "rewards-coin-home-screen-v2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.coin.vm
|
||||
|
||||
import com.navi.base.utils.orFalse
|
||||
import com.navi.coin.models.states.CoinRootState
|
||||
import com.navi.coin.repo.repository.CoinHomeScreenRepo
|
||||
import com.navi.coin.utils.constant.CoinHomeScreenConstants.COIN_HOME_SCREEN_VARIANT_EXPERIMENT_NAME
|
||||
import com.navi.common.utils.isValidResponse
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
@HiltViewModel
|
||||
class CoinHomeViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val coinHomeScreenRepo: CoinHomeScreenRepo,
|
||||
) : CoinBaseVM() {
|
||||
|
||||
private val _coinHomeScreenVariantState = MutableStateFlow<CoinRootState>(CoinRootState.Loading)
|
||||
val coinHomeScreenVariantState = _coinHomeScreenVariantState.asStateFlow()
|
||||
|
||||
init {
|
||||
launch {
|
||||
coinHomeScreenRepo
|
||||
.getCoinHomeScreenVariant(COIN_HOME_SCREEN_VARIANT_EXPERIMENT_NAME)
|
||||
.collect { response ->
|
||||
if (response.isValidResponse()) {
|
||||
_coinHomeScreenVariantState.update {
|
||||
CoinRootState.Success(response.data?.result.orFalse())
|
||||
}
|
||||
} else {
|
||||
_coinHomeScreenVariantState.update { CoinRootState.Success(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ import kotlinx.coroutines.flow.update
|
||||
import org.json.JSONObject
|
||||
|
||||
@HiltViewModel
|
||||
class CoinHomeScreenVM
|
||||
open class CoinHomeViewModelV1
|
||||
@Inject
|
||||
constructor(
|
||||
private val coinHomeScreenRepo: CoinHomeScreenRepo,
|
||||
@@ -110,11 +110,11 @@ constructor(
|
||||
private val _bottomSheetData = MutableSharedFlow<WidgetModelDefinition<UiTronResponse>>()
|
||||
val bottomSheetData = _bottomSheetData.asSharedFlow()
|
||||
|
||||
private val _nextScreenDefinitionState = MutableStateFlow<ScreenDefinition?>(null)
|
||||
val nextScreenDefinitionState = _nextScreenDefinitionState.asStateFlow()
|
||||
private val _redemptionStatusScreenDefinitionState = MutableStateFlow<ScreenDefinition?>(null)
|
||||
val redemptionStatusScreenDefinitionState = _redemptionStatusScreenDefinitionState.asStateFlow()
|
||||
|
||||
private val _showLoadingScreenConfig = MutableStateFlow<Boolean?>(false)
|
||||
val showLoadingScreenConfig = _showLoadingScreenConfig.asStateFlow()
|
||||
private val _showRedemptionScreen = MutableStateFlow<Boolean?>(false)
|
||||
val showRedemptionScreen = _showRedemptionScreen.asStateFlow()
|
||||
|
||||
private var hasAutoRedeemTriggered = false
|
||||
|
||||
@@ -186,7 +186,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun navigateToScreen(url: String, action: UiTronAction?) {
|
||||
protected suspend fun navigateToScreen(url: String, action: UiTronAction?) {
|
||||
when (url) {
|
||||
CoinNavigationActions.BACK.name -> {
|
||||
_navigateToPreviousScreen.emit(true)
|
||||
@@ -199,7 +199,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleCoinHomeScreenActions(action: UiTronAction?) {
|
||||
suspend fun handleCoinHomeScreenActions(action: UiTronAction?) {
|
||||
when (action) {
|
||||
is CtaAction -> {
|
||||
action.ctaData?.url?.let { navigateToScreen(it, action) }
|
||||
@@ -215,7 +215,7 @@ constructor(
|
||||
launch { cacheListOfImageUrls(REFERRAL_SHAREABILITY_URLS) }
|
||||
}
|
||||
|
||||
private fun initCoinHomeScreenImageCaching(
|
||||
fun initCoinHomeScreenImageCaching(
|
||||
screenDefinitionStructure: ScreenStructure?,
|
||||
) {
|
||||
screenDefinitionStructure?.let { screenStructure ->
|
||||
@@ -229,7 +229,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateUPIId(upiId: String, apiAction: TriggerApiAction) {
|
||||
fun validateUPIId(upiId: String, apiAction: TriggerApiAction) {
|
||||
launch {
|
||||
eventPublisher.sendEvent(DEV_VALIDATE_UPI_ID_EVENT)
|
||||
_apiActionState.emit(ApiActionState.LOADING)
|
||||
@@ -282,7 +282,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUpiIdValidationStatus(
|
||||
suspend fun getUpiIdValidationStatus(
|
||||
requestId: String,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onFailure: suspend () -> Unit,
|
||||
@@ -302,7 +302,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun redeemCoin(
|
||||
fun redeemCoin(
|
||||
upiId: String,
|
||||
apiAction: TriggerApiAction,
|
||||
) {
|
||||
@@ -342,7 +342,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initiateRedemption(
|
||||
suspend fun initiateRedemption(
|
||||
upiId: String,
|
||||
onSuccess:
|
||||
suspend (
|
||||
@@ -462,7 +462,7 @@ constructor(
|
||||
?.collect {}
|
||||
}
|
||||
|
||||
private suspend fun checkForRedemptionStatus(
|
||||
suspend fun checkForRedemptionStatus(
|
||||
txnId: String,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onFailure:
|
||||
@@ -487,7 +487,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchRedemptionLoadingScreenDefinition(
|
||||
suspend fun fetchRedemptionLoadingScreenDefinition(
|
||||
templateKey: String,
|
||||
dataMap: Map<String, Any?>,
|
||||
apiAction: TriggerApiAction,
|
||||
@@ -505,8 +505,8 @@ constructor(
|
||||
screenDefinition =
|
||||
getGsonBuilders().fromJson(jsonObject.toString(), ScreenDefinition::class.java)
|
||||
if (screenDefinition != null) {
|
||||
_nextScreenDefinitionState.emit(screenDefinition)
|
||||
_showLoadingScreenConfig.emit(true)
|
||||
_redemptionStatusScreenDefinitionState.emit(screenDefinition)
|
||||
_showRedemptionScreen.emit(true)
|
||||
}
|
||||
} else {
|
||||
handleActions(apiAction.onFailure)
|
||||
@@ -516,7 +516,7 @@ constructor(
|
||||
}
|
||||
|
||||
fun setShowLoadingScreenConfig(response: Boolean) {
|
||||
launch { _showLoadingScreenConfig.update { response } }
|
||||
launch { _showRedemptionScreen.update { response } }
|
||||
}
|
||||
|
||||
fun triggerAutoRedemption(shouldTrigger: Boolean?) {
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.coin.vm
|
||||
|
||||
import com.navi.base.utils.orFalse
|
||||
import com.navi.coin.models.states.CoinHomeScreenV2State
|
||||
import com.navi.coin.repo.repository.CoinHistoryScreenRepo
|
||||
import com.navi.coin.repo.repository.CoinHomeScreenRepo
|
||||
import com.navi.common.alchemist.model.AlchemistBottomSheetStructure
|
||||
import com.navi.common.alchemist.model.AlchemistScreenDefinition
|
||||
import com.navi.common.managers.NaviLocationManager
|
||||
import com.navi.common.network.ApiConstants
|
||||
import com.navi.common.utils.isValidResponse
|
||||
import com.navi.rr.common.constants.COIN_HOME_SCREEN_V2
|
||||
import com.navi.rr.common.models.RRErrorData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
@HiltViewModel
|
||||
class CoinHomeViewModelV2
|
||||
@Inject
|
||||
constructor(
|
||||
private val coinHomeScreenRepo: CoinHomeScreenRepo,
|
||||
coinHistoryScreenRepo: CoinHistoryScreenRepo,
|
||||
) : CoinHomeViewModelV1(coinHomeScreenRepo, coinHistoryScreenRepo) {
|
||||
|
||||
private val _screenData = MutableStateFlow<CoinHomeScreenV2State>(CoinHomeScreenV2State.Loading)
|
||||
val screenState = _screenData.asStateFlow()
|
||||
|
||||
private var _showBackLayer = MutableStateFlow(false)
|
||||
val showBackLayer = _showBackLayer.asStateFlow()
|
||||
|
||||
private val _bottomSheetDataAlchemist = MutableSharedFlow<AlchemistBottomSheetStructure>()
|
||||
val bottomSheetDataAlchemist = _bottomSheetDataAlchemist.asSharedFlow()
|
||||
|
||||
private var _bottomSheetScreenMap = mutableMapOf<String, AlchemistBottomSheetStructure>()
|
||||
|
||||
val locationManager = NaviLocationManager()
|
||||
|
||||
fun setBottomSheetScreenMapData(bottomSheets: List<AlchemistBottomSheetStructure>?) {
|
||||
_bottomSheetScreenMap =
|
||||
bottomSheets
|
||||
?.filter { it.screenId != null }
|
||||
?.associate { it.screenId.orEmpty() to it }
|
||||
?.toMutableMap() ?: mutableMapOf()
|
||||
}
|
||||
|
||||
fun getBottomSheetScreenData(key: String) = _bottomSheetScreenMap[key]
|
||||
|
||||
fun setShowBackLayer(value: Boolean) {
|
||||
_showBackLayer.update { value }
|
||||
}
|
||||
|
||||
suspend fun setCurrentBottomSheetData(bottomSheetData: AlchemistBottomSheetStructure) {
|
||||
_bottomSheetDataAlchemist.emit(bottomSheetData)
|
||||
}
|
||||
|
||||
fun fetchScreenConfig(shouldRefresh: Boolean? = null) {
|
||||
launch {
|
||||
if (shouldRefresh.orFalse()) {
|
||||
_screenData.update { CoinHomeScreenV2State.Loading }
|
||||
}
|
||||
coinHomeScreenRepo.fetchCoinHomeScreenV2(shouldRefresh.orFalse()).collect { result ->
|
||||
val response = result.data
|
||||
when {
|
||||
result.isValidResponse() -> {
|
||||
if (result.isFromCache) {
|
||||
setCachePresent()
|
||||
}
|
||||
_screenData.update {
|
||||
CoinHomeScreenV2State.Success(
|
||||
response?.copy() ?: AlchemistScreenDefinition()
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
showError(
|
||||
RRErrorData(
|
||||
statusCode = result.statusCode ?: ApiConstants.API_CODE_ERROR,
|
||||
methodName = ::fetchScreenConfig.name,
|
||||
error = result.error,
|
||||
errors = result.errors,
|
||||
screenName = COIN_HOME_SCREEN_V2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import com.navi.coin.repo.pagingsource.CashHistoryListSource
|
||||
import com.navi.coin.repo.pagingsource.CoinHistoryListSource
|
||||
import com.navi.coin.repo.repository.CoinHistoryScreenRepo
|
||||
import com.navi.coin.repo.repository.CoinHomeScreenRepo
|
||||
import com.navi.coin.vm.CoinHomeScreenVM
|
||||
import com.navi.coin.vm.CoinHomeViewModelV1
|
||||
import com.navi.coins.common.TestDispatcher
|
||||
import com.navi.common.forge.model.ScreenDefinition
|
||||
import com.navi.common.forge.model.ScreenStructure
|
||||
@@ -38,7 +38,7 @@ import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
class CoinHomeScreenVMTest {
|
||||
class CoinHomeViewModelV1Test {
|
||||
|
||||
@RelaxedMockK private lateinit var coinHistoryScreenRepo: CoinHistoryScreenRepo
|
||||
@RelaxedMockK private lateinit var coinHistoryListSource: CoinHistoryListSource
|
||||
@@ -71,8 +71,8 @@ class CoinHomeScreenVMTest {
|
||||
coinHomeScreenRepo.fetchCoinHomeScreenUiTronConfigs(metricInfo = any())
|
||||
} returns flow { emit(response) }
|
||||
coEvery { naviCacheRepository.save(any()) } returns Unit
|
||||
val coinHomeScreenVM = CoinHomeScreenVM(coinHomeScreenRepo, coinHistoryScreenRepo)
|
||||
coinHomeScreenVM.updateCoroutineScope(this)
|
||||
val coinHomeViewModelV1 = CoinHomeViewModelV1(coinHomeScreenRepo, coinHistoryScreenRepo)
|
||||
coinHomeViewModelV1.updateCoroutineScope(this)
|
||||
coVerify(exactly = 0) { naviCacheRepository.save(any()) }
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ object DBCacheConstants {
|
||||
const val PD_QUESTIONNAIRE_PAGE = "PD_QUESTIONNAIRE_PAGE"
|
||||
const val GI_PRE_DIAB_QUESTION_CACHE_KEY = "GI_PRE_DIAB_QUESTION_CACHE_KEY"
|
||||
const val COIN_HOME_SCREEN_CACHE_KEY = "COIN_HOME_SCREEN_CACHE_KEY"
|
||||
const val COIN_HOME_SCREEN_VARIANT_CACHE_KEY = "COIN_HOME_SCREEN_VARIANT_CACHE_KEY"
|
||||
const val COIN_HOME_SCREEN_V2_CACHE_KEY = "COIN_HOME_SCREEN_V2_CACHE_KEY"
|
||||
const val LEADERBOARD_SCREEN_CACHE_KEY = "LEADERBOARD_SCREEN_CACHE_KEY"
|
||||
const val LEADERBOARD_LIST_CACHE_KEY = "LEADERBOARD_LIST_CACHE_KEY"
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const val FORGE_LEADERBOARD_SCREEN = "REFERRAL_LEADERBOARD_SCREEN"
|
||||
const val SCRATCH_CARD_GRATIFICATION_SCREEN = "SCRATCH_CARD_GRATIFICATION_SCREEN"
|
||||
const val SCRATCH_CARD_HISTORY_SCREEN = "SCRATCH_CARD_HISTORY_SCREEN"
|
||||
const val COIN_HOME_SCREEN = "COIN_HOME_SCREEN"
|
||||
const val COIN_HOME_SCREEN_V2 = "COIN_HOME_SCREEN_V2"
|
||||
const val COIN_HISTORY_SCREEN = "COIN_HISTORY_SCREEN"
|
||||
const val VERIFY_POLLING_DETAILS_SCREEN = "VERIFY_POLLING_DETAILS"
|
||||
const val KYC_VERIFY_SCREEN = "KYC_VERIFY_SCREEN"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package com.navi.rr.common.widgetFactory
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -22,6 +23,7 @@ import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
|
||||
import com.navi.common.forge.model.WidgetModelDefinition
|
||||
import com.navi.common.forge.model.WidgetTypes
|
||||
import com.navi.rr.common.models.ItemListData
|
||||
@@ -112,6 +114,30 @@ fun WidgetRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WidgetRenderer(
|
||||
modifier: Modifier = Modifier,
|
||||
widget: AlchemistWidgetModelDefinition<UiTronResponse>?,
|
||||
viewModel: UiTronViewModel,
|
||||
scrollState: (() -> ScrollState)? = null
|
||||
) {
|
||||
if (widget == null) return
|
||||
return when (widget.widgetType) {
|
||||
WidgetTypes.UI_TRON_WIDGET.name -> {
|
||||
UiTronRenderer(
|
||||
dataMap = widget.widgetData?.data,
|
||||
uiTronViewModel = viewModel,
|
||||
customUiTronRenderer = RRCustomUiTronRenderer(parentScrollState = scrollState)
|
||||
)
|
||||
.Render(
|
||||
composeViews = widget.widgetData?.parentComposeView.orEmpty(),
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FooterWithShadow(
|
||||
widget: WidgetModelDefinition<UiTronResponse>?,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package com.navi.rr.uitron.render
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.navi.common.uitron.render.CommonCustomUiTronRenderer
|
||||
@@ -23,7 +24,8 @@ import com.navi.uitron.model.data.UiTronData
|
||||
import com.navi.uitron.model.ui.UiTronView
|
||||
import com.navi.uitron.viewmodel.UiTronViewModel
|
||||
|
||||
class RRCustomUiTronRenderer : CommonCustomUiTronRenderer() {
|
||||
class RRCustomUiTronRenderer(parentScrollState: (() -> ScrollState)? = null) :
|
||||
CommonCustomUiTronRenderer(parentScrollState = parentScrollState) {
|
||||
@Composable
|
||||
override fun Render(
|
||||
composeView: UiTronView,
|
||||
|
||||
@@ -34,31 +34,37 @@ constructor(
|
||||
suspend inline fun <reified T> fetchAndCacheData(
|
||||
key: String,
|
||||
shouldRefresh: Boolean? = null,
|
||||
emitMultipleValues: Boolean = true,
|
||||
noinline fetchFromAlternativeSource: suspend () -> RepoResult<T>,
|
||||
) =
|
||||
cacheRepository.getAndFetchDataFromAlternativeSource(
|
||||
key = key,
|
||||
shouldRefresh = shouldRefresh,
|
||||
emitMultipleValues = emitMultipleValues,
|
||||
fetchFromAlternativeSource = fetchFromAlternativeSource
|
||||
)
|
||||
|
||||
suspend inline fun <reified T> NaviCacheRepositoryImpl.getAndFetchDataFromAlternativeSource(
|
||||
key: String,
|
||||
shouldRefresh: Boolean? = null,
|
||||
emitMultipleValues: Boolean = true,
|
||||
noinline fetchFromAlternativeSource: suspend () -> RepoResult<T>,
|
||||
) = flow {
|
||||
var isValueEmitted = false
|
||||
|
||||
// Checking if the current value exists in the database
|
||||
if (shouldRefresh.orFalse().not()) {
|
||||
val cachedValue = get(key = key)
|
||||
if (cachedValue != null) {
|
||||
emit(RepoResult(data = cachedValue.value.toType<T>(), isFromCache = true))
|
||||
isValueEmitted = true
|
||||
}
|
||||
}
|
||||
// Fetching data from alternate source
|
||||
val fetchResult = fetchFromAlternativeSource()
|
||||
emit(fetchResult)
|
||||
|
||||
if (emitMultipleValues || isValueEmitted.not()) {
|
||||
emit(fetchResult)
|
||||
}
|
||||
// Creating an entity to store data from the alternative source
|
||||
val altSourceEntity =
|
||||
NaviCacheAltSourceEntity(
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.navi.alfred.AlfredManager
|
||||
import com.navi.base.utils.isNotNull
|
||||
import com.navi.common.alchemist.model.AlchemistScreenDefinition
|
||||
import com.navi.common.constants.SCREEN_NAME
|
||||
import com.navi.common.forge.model.ScreenDefinition
|
||||
import com.navi.common.utils.ERROR
|
||||
@@ -225,3 +226,33 @@ fun InitWidgetActions(
|
||||
?.forEach { viewModel.handleActionsAsync(it.widgetRenderActions?.postRenderAction) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InitWidgetActions(
|
||||
screenDefinition: AlchemistScreenDefinition,
|
||||
viewModel: UiTronViewModel,
|
||||
) {
|
||||
LaunchedEffect(screenDefinition) {
|
||||
screenDefinition.screenStructure?.renderActions?.postRenderAction?.let {
|
||||
viewModel.handleActionsAsync(it)
|
||||
}
|
||||
screenDefinition.screenStructure
|
||||
?.header
|
||||
?.widgets
|
||||
?.filter { it.isNotNull() }
|
||||
?.forEach { viewModel.handleActionsAsync(it.widgetRenderActions?.postRenderAction) }
|
||||
screenDefinition.screenStructure
|
||||
?.footer
|
||||
?.widgets
|
||||
?.filter { it.isNotNull() }
|
||||
?.forEach { viewModel.handleActionsAsync(it.widgetRenderActions?.postRenderAction) }
|
||||
screenDefinition.screenStructure
|
||||
?.content
|
||||
?.widgets
|
||||
?.filter { it.isNotNull() }
|
||||
?.forEach { viewModel.handleActionsAsync(it.widgetRenderActions?.postRenderAction) }
|
||||
screenDefinition.screenStructure?.renderActions?.preRenderAction?.let {
|
||||
viewModel.handleActionsAsync(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
package com.navi.rr.utils.ext
|
||||
|
||||
import com.navi.base.utils.orElse
|
||||
import com.navi.common.network.models.GenericResponse
|
||||
import com.navi.rr.utils.getGsonBuilders
|
||||
import retrofit2.Response
|
||||
|
||||
fun <T> List<T>.secondOrNull(): T? {
|
||||
return if (size >= 2) get(1) else null
|
||||
@@ -23,3 +25,9 @@ fun <T> T?.toJson(): String = getGsonBuilders().toJson(this).orElse("{}")
|
||||
fun Int.isEven(): Boolean = this % 2 == 0
|
||||
|
||||
fun Int.isOdd(): Boolean = this % 2 != 0
|
||||
|
||||
fun <T> Response<T>.toGenericResponse(): Response<GenericResponse<T>> {
|
||||
return Response.success(
|
||||
GenericResponse(statusCode = this.code(), message = this.message(), data = this.body())
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user