diff --git a/android/navi-coin/src/main/java/com/navi/coin/models/states/CoinHomeScreenState.kt b/android/navi-coin/src/main/java/com/navi/coin/models/states/CoinHomeScreenState.kt index 660e06de3f..0189fd2784 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/models/states/CoinHomeScreenState.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/models/states/CoinHomeScreenState.kt @@ -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 diff --git a/android/navi-coin/src/main/java/com/navi/coin/network/RetrofitService.kt b/android/navi-coin/src/main/java/com/navi/coin/network/RetrofitService.kt index d860a81cfc..a02e3b597e 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/network/RetrofitService.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/network/RetrofitService.kt @@ -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> + @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> + @GET("/reward-service/scratch-card/history") @RetryPolicy suspend fun fetchScratchCardHistoryList( @@ -106,6 +116,12 @@ interface RetrofitService { @Header("X-Target") target: String ): Response> + @GET("/litmus-proxy/v1/proxy/experiment") + suspend fun fetchABExperiment( + @Query("name") name: String, + @Header("X-Target") header: String + ): Response + @GET("/referral/share-ability") @RetryPolicy suspend fun getReferralData( diff --git a/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt b/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt index 929ccdad43..c06b12adad 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt @@ -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> { + return cacheHandlerProxy.fetchData( + 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> @@ -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() + ) + } + ) } diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeRootComposable.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeRootComposable.kt new file mode 100644 index 0000000000..a02de9e853 --- /dev/null +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeRootComposable.kt @@ -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) + } + } + } + } +} diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt index b711e49e89..cd68a723b5 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt @@ -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(BACK_FROM) == ONE_PROFILE_SCREEN || - coinHomeScreenVM.handle.get(AUTO_REDEMPTION_STARTED) == TRUE) + (coinHomeViewModelV1.handle.get(BACK_FROM) == ONE_PROFILE_SCREEN || + coinHomeViewModelV1.handle.get(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(TRIGGER_REDEMPTION_EVENT)?.collect { resultCode -> if (resultCode == Activity.RESULT_OK) { val redemptionJsonResponse = - coinHomeScreenVM.handle.get(TRIGGER_REDEMPTION_ACTION) + coinHomeViewModelV1.handle.get(TRIGGER_REDEMPTION_ACTION) val redemptionAction = redemptionJsonResponse?.toType() - 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[ diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreenV2.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreenV2.kt new file mode 100644 index 0000000000..cfc1cd942c --- /dev/null +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreenV2.kt @@ -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() } + 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(null) } + val navigateBackWithResult = + rememberNavigatorWithResultAndParams( + 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(BACK_FROM) == ONE_PROFILE_SCREEN + val autoRedemptionStarted = viewModel.handle.get(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(TRIGGER_REDEMPTION_EVENT)?.collect { resultCode -> + if (resultCode == Activity.RESULT_OK) { + val redemptionJsonResponse = + viewModel.handle.get(TRIGGER_REDEMPTION_ACTION) + val redemptionAction = + redemptionJsonResponse?.toType() + 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>?, + 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)) + ) + } + ) diff --git a/android/navi-coin/src/main/java/com/navi/coin/utils/CoinHomeInteractionUtils.kt b/android/navi-coin/src/main/java/com/navi/coin/utils/CoinHomeInteractionUtils.kt new file mode 100644 index 0000000000..80b3b3b85e --- /dev/null +++ b/android/navi-coin/src/main/java/com/navi/coin/utils/CoinHomeInteractionUtils.kt @@ -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 +) { + 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, + 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, +) = + 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 +} diff --git a/android/navi-coin/src/main/java/com/navi/coin/utils/CommonUtils.kt b/android/navi-coin/src/main/java/com/navi/coin/utils/CommonUtils.kt index ab2ca45002..94bd5f5294 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/utils/CommonUtils.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/utils/CommonUtils.kt @@ -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) = .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 + ) + } +} diff --git a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt index 91eda6bb56..ad31701138 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt @@ -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" diff --git a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/ScreenConstants.kt b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/ScreenConstants.kt index c6688ab84c..ff571da5d6 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/ScreenConstants.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/ScreenConstants.kt @@ -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" } diff --git a/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModel.kt b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModel.kt new file mode 100644 index 0000000000..39e4c0ba88 --- /dev/null +++ b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModel.kt @@ -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.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) } + } + } + } + } +} diff --git a/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeScreenVM.kt b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV1.kt similarity index 95% rename from android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeScreenVM.kt rename to android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV1.kt index 745d79136f..01bfd72339 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeScreenVM.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV1.kt @@ -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>() val bottomSheetData = _bottomSheetData.asSharedFlow() - private val _nextScreenDefinitionState = MutableStateFlow(null) - val nextScreenDefinitionState = _nextScreenDefinitionState.asStateFlow() + private val _redemptionStatusScreenDefinitionState = MutableStateFlow(null) + val redemptionStatusScreenDefinitionState = _redemptionStatusScreenDefinitionState.asStateFlow() - private val _showLoadingScreenConfig = MutableStateFlow(false) - val showLoadingScreenConfig = _showLoadingScreenConfig.asStateFlow() + private val _showRedemptionScreen = MutableStateFlow(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, 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?) { diff --git a/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV2.kt b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV2.kt new file mode 100644 index 0000000000..d47bf6385d --- /dev/null +++ b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV2.kt @@ -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.Loading) + val screenState = _screenData.asStateFlow() + + private var _showBackLayer = MutableStateFlow(false) + val showBackLayer = _showBackLayer.asStateFlow() + + private val _bottomSheetDataAlchemist = MutableSharedFlow() + val bottomSheetDataAlchemist = _bottomSheetDataAlchemist.asSharedFlow() + + private var _bottomSheetScreenMap = mutableMapOf() + + val locationManager = NaviLocationManager() + + fun setBottomSheetScreenMapData(bottomSheets: List?) { + _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 + ) + ) + } + } + } + } + } +} diff --git a/android/navi-coin/src/test/java/com/navi/coins/vm/CoinHomeScreenVMTest.kt b/android/navi-coin/src/test/java/com/navi/coins/vm/CoinHomeViewModelV1Test.kt similarity index 92% rename from android/navi-coin/src/test/java/com/navi/coins/vm/CoinHomeScreenVMTest.kt rename to android/navi-coin/src/test/java/com/navi/coins/vm/CoinHomeViewModelV1Test.kt index f87ee149ed..735d0ca010 100644 --- a/android/navi-coin/src/test/java/com/navi/coins/vm/CoinHomeScreenVMTest.kt +++ b/android/navi-coin/src/test/java/com/navi/coins/vm/CoinHomeViewModelV1Test.kt @@ -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()) } } } diff --git a/android/navi-common/src/main/java/com/navi/common/constants/DBCacheConstants.kt b/android/navi-common/src/main/java/com/navi/common/constants/DBCacheConstants.kt index dd97a0ccba..1c7ba01e5b 100644 --- a/android/navi-common/src/main/java/com/navi/common/constants/DBCacheConstants.kt +++ b/android/navi-common/src/main/java/com/navi/common/constants/DBCacheConstants.kt @@ -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" diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/constants/ScreenNames.kt b/android/navi-rr/src/main/java/com/navi/rr/common/constants/ScreenNames.kt index a2373b299e..66a65c3d19 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/constants/ScreenNames.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/constants/ScreenNames.kt @@ -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" diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt b/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt index d907fb00d5..f6107d1516 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt @@ -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?, + 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?, diff --git a/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt b/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt index 1fe0541607..670f2db578 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt @@ -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, diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/cachemanager/CacheManager.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/cachemanager/CacheManager.kt index e08ec6d5c7..064fb13db5 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/utils/cachemanager/CacheManager.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/cachemanager/CacheManager.kt @@ -34,31 +34,37 @@ constructor( suspend inline fun fetchAndCacheData( key: String, shouldRefresh: Boolean? = null, + emitMultipleValues: Boolean = true, noinline fetchFromAlternativeSource: suspend () -> RepoResult, ) = cacheRepository.getAndFetchDataFromAlternativeSource( key = key, shouldRefresh = shouldRefresh, + emitMultipleValues = emitMultipleValues, fetchFromAlternativeSource = fetchFromAlternativeSource ) suspend inline fun NaviCacheRepositoryImpl.getAndFetchDataFromAlternativeSource( key: String, shouldRefresh: Boolean? = null, + emitMultipleValues: Boolean = true, noinline fetchFromAlternativeSource: suspend () -> RepoResult, ) = 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(), 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( diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/composeutils/ComposableUtils.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/composeutils/ComposableUtils.kt index d86d84b859..a84b4d2b3e 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/utils/composeutils/ComposableUtils.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/composeutils/ComposableUtils.kt @@ -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) + } + } +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/ext/GenericExt.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/ext/GenericExt.kt index f8debba374..aecfa26ade 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/utils/ext/GenericExt.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/ext/GenericExt.kt @@ -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 List.secondOrNull(): T? { return if (size >= 2) get(1) else null @@ -23,3 +25,9 @@ fun T?.toJson(): String = getGsonBuilders().toJson(this).orElse("{}") fun Int.isEven(): Boolean = this % 2 == 0 fun Int.isOdd(): Boolean = this % 2 != 0 + +fun Response.toGenericResponse(): Response> { + return Response.success( + GenericResponse(statusCode = this.code(), message = this.message(), data = this.body()) + ) +}