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:
Kamalesh Garnayak
2024-12-04 16:56:19 +05:30
committed by GitHub
parent fd715b6f3d
commit 6215884454
21 changed files with 1380 additions and 71 deletions

View File

@@ -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

View File

@@ -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(

View File

@@ -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()
)
}
)
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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[

View File

@@ -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))
)
}
)

View File

@@ -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
}

View File

@@ -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
)
}
}

View File

@@ -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"

View File

@@ -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"
}

View File

@@ -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) }
}
}
}
}
}

View File

@@ -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?) {

View File

@@ -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
)
)
}
}
}
}
}
}

View File

@@ -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()) }
}
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>?,

View File

@@ -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,

View File

@@ -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(

View File

@@ -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)
}
}
}

View File

@@ -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())
)
}