NTP-8865 | AS | Feature/scratch history api revamp (#13316)

This commit is contained in:
Ayushman Sharma
2024-11-07 14:16:57 +05:30
committed by GitHub
parent 624b037011
commit 4d8bb91bbe
15 changed files with 809 additions and 82 deletions

View File

@@ -111,4 +111,13 @@ interface RetrofitService {
suspend fun getReferralData(
@Header("X-Target") target: String
): Response<GenericResponse<ReferralContactList>>
@GET("/rudolph/scratch-card/history")
@RetryPolicy
suspend fun getScratchCards(
@Header("X-Target") target: String,
@Query("pageSection") pageSection: String,
@Query("pageNumber") pageNumber: String,
@Query("pageSize") pageSize: String,
): Response<GenericResponse<ScratchCardHistoryResponse>>
}

View File

@@ -0,0 +1,212 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.coin.repo.pagingsource
import com.navi.base.utils.isNull
import com.navi.coin.models.model.ScratchCard
import com.navi.coin.models.model.ScratchCardHistoryResponse
import com.navi.coin.models.states.ScratchCardPaginatedHistoryScreenState
import com.navi.coin.repo.repository.ScratchCardHistoryScreenRepo
import com.navi.common.checkmate.model.MetricInfo
import com.navi.common.network.models.RepoResult
import com.navi.common.network.models.isSuccessWithData
import com.navi.rr.common.constants.SCRATCH_CARD_HISTORY_SCREEN
import com.navi.rr.utils.custompager.BaseLazyGridPager
import com.navi.rr.utils.custompager.CustomPagerConfig
import com.navi.rr.utils.custompager.PagerItemState
import com.navi.rr.utils.ext.isOdd
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class ScratchCardHistoryCustomPager(
private val repository: ScratchCardHistoryScreenRepo,
private val scope: CoroutineScope,
pagerConfig: CustomPagerConfig = CustomPagerConfig(),
) : BaseLazyGridPager<ScratchCard, ScratchCardHistoryResponse>(scope, pagerConfig) {
private var fetchType = FetchType.ACTIVE
private var buffer: ScratchCard? = null
private var previousAPICallIsUnScratched: Boolean = false
private val _scratchCardHistoryListData =
MutableStateFlow<ScratchCardPaginatedHistoryScreenState>(
ScratchCardPaginatedHistoryScreenState.Loading
)
val scratchCardHistoryListData = _scratchCardHistoryListData.asStateFlow()
private val DEFAULT_START_SIZE = 26
private val DEFAULT_ACTIVE_PAGE_SIZE = 50
init {
pageSize = DEFAULT_ACTIVE_PAGE_SIZE
pageNumber = 0
}
override suspend fun fetchData(
pageNumber: Int,
pageSize: Int,
metricInfo: MetricInfo<RepoResult<ScratchCardHistoryResponse>>
): RepoResult<ScratchCardHistoryResponse> {
return repository.getScratchCards(
metricInfo = metricInfo,
pageSection = getPageSectionByType(fetchType),
pageSize = pageSize,
pageNumber = pageNumber
)
}
private fun getPageSectionByType(fetchType: FetchType): String {
return when (fetchType) {
FetchType.ACTIVE -> FetchType.ACTIVE.name
FetchType.PROCESSED -> FetchType.PROCESSED.name
FetchType.EXPIRED -> FetchType.EXPIRED.name
else -> FetchType.EXPIRED.name
}
}
override fun handleIfEndOfListState(data: ScratchCardHistoryResponse?) {
if (data?.isNextPageAvailable == false) {
if (isEndOfTotalList().not()) {
handleEndCase()
} else {
super.handleIfEndOfListState(data)
if (pagerStates.value.pagingItems.size == 0)
_scratchCardHistoryListData.update {
ScratchCardPaginatedHistoryScreenState.Empty
}
}
}
}
// This function is used to set the next api to be called if we reach the end of previous api
private fun handleEndCase() {
when (fetchType) {
FetchType.ACTIVE -> {
fetchType = FetchType.PROCESSED
previousAPICallIsUnScratched = true
// set page size and number to default
pageNumber = DEFAULT_START_KEY
pageSize = DEFAULT_PAGE_SIZE
}
FetchType.PROCESSED -> {
fetchType = FetchType.EXPIRED
pageNumber = DEFAULT_START_KEY
}
FetchType.EXPIRED -> {
fetchType = FetchType.END_OF_LIST
if (buffer.isNull()) pagerItemState.update { PagerItemState.EndOfList }
}
else -> Unit
}
}
override fun performPostLoadActions(data: ScratchCardHistoryResponse?) {
handleMinimumData(data)
}
// this handles the case when the data is less than 10 and the next api call is to be made
private fun handleMinimumData(data: ScratchCardHistoryResponse?) {
if (fetchType == FetchType.END_OF_LIST) return
val uiListSize = pagerList.value.size
val dataSize = data?.scratchCardHistoryList?.size ?: 0
val isListSizeLessThanDefaultSize = uiListSize < DEFAULT_START_SIZE
val isDataSizeLessThanDefaultPageSize = dataSize < pageSize
val isUnscratchedDataOdd = previousAPICallIsUnScratched && uiListSize.isOdd()
if (
isListSizeLessThanDefaultSize ||
isDataSizeLessThanDefaultPageSize ||
isUnscratchedDataOdd
) {
previousAPICallIsUnScratched = false
loadNextPage()
}
}
override fun handleSuccessData(data: ScratchCardHistoryResponse?) {
if (
pagerList.value.isEmpty() &&
data?.scratchCardHistoryList.isNullOrEmpty() &&
fetchType == FetchType.EXPIRED
) {
_scratchCardHistoryListData.update { ScratchCardPaginatedHistoryScreenState.Empty }
} else {
val scratchCardList = alterDataIfRequired(data)
pagerList.update { currentList ->
currentList.apply { currentList.addAll(scratchCardList) }
}
_scratchCardHistoryListData.update { ScratchCardPaginatedHistoryScreenState.Success }
}
super.handleSuccessData(data)
}
// This function is used to make the history ui as symmetric as possible by keeping even size
// list
private fun alterDataIfRequired(data: ScratchCardHistoryResponse?): List<ScratchCard> {
val list = data?.scratchCardHistoryList.orEmpty().toMutableList()
// Will return if the list is of unscratched cards
if (fetchType == FetchType.ACTIVE) {
return list
}
val bufferSize = if (buffer != null) 1 else 0
val totalSizeIsOdd = (pagerList.value.size + list.size + bufferSize).isOdd()
val alterData = totalSizeIsOdd && isEndOfTotalList().not()
// add stored scratch card if present using buffer
buffer?.let {
list.add(0, it)
buffer = null
}
// if it is the end of all api calls then no need to alter data
if (fetchType == FetchType.EXPIRED && data?.isNextPageAvailable == false) return list
// alter data if total size is odd
if (alterData && list.isNotEmpty()) {
buffer = list.removeLast()
}
return list
}
override fun isEndOfTotalList() = fetchType == FetchType.END_OF_LIST
override fun refresh() {
fetchType = FetchType.ACTIVE
buffer = null
pageSize = DEFAULT_ACTIVE_PAGE_SIZE
super.refresh()
}
override fun handleErrorState(data: ScratchCardHistoryResponse?) {
_scratchCardHistoryListData.update { ScratchCardPaginatedHistoryScreenState.Error }
}
override val metricInfo: MetricInfo<RepoResult<ScratchCardHistoryResponse>>
get() =
MetricInfo.CoinMetric(
screen = SCRATCH_CARD_HISTORY_SCREEN,
isNae = { !it.isSuccessWithData() }
)
private enum class FetchType {
ACTIVE,
PROCESSED,
EXPIRED,
END_OF_LIST
}
}

View File

@@ -17,6 +17,7 @@ import com.navi.common.forge.model.ScreenDefinition
import com.navi.common.model.ModuleNameV2
import com.navi.common.network.models.RepoResult
import com.navi.common.utils.Constants.GZIP
import com.navi.rr.common.models.XTarget
import com.navi.rr.common.network.retrofit.ResponseHandler
import com.navi.rr.utils.cachemanager.CacheHandlerProxy
import javax.inject.Inject
@@ -47,6 +48,7 @@ constructor(
)
}
// Deprecated api for scratch card list
suspend fun fetchScratchCardListData(
pageNo: Int,
pageSize: Int,
@@ -64,4 +66,22 @@ constructor(
)
)
}
suspend fun getScratchCards(
pageNumber: Int,
pageSize: Int,
pageSection: String,
metricInfo: MetricInfo<RepoResult<ScratchCardHistoryResponse>>
): RepoResult<ScratchCardHistoryResponse> {
return responseHandler.handleResponse(
metricInfo = metricInfo,
response =
retrofitService.getScratchCards(
target = XTarget.RUDOLPH.name,
pageSection = pageSection,
pageNumber = pageNumber.toString(),
pageSize = pageSize.toString()
)
)
}
}

View File

@@ -8,12 +8,14 @@
package com.navi.coin.ui.compose.common
import android.annotation.SuppressLint
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@@ -30,9 +32,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -42,10 +45,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -58,8 +63,6 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
@@ -88,6 +91,7 @@ import com.navi.coin.utils.constant.Constants.SCRATCHED_SCRATCH_CARD_URL_KEY
import com.navi.coin.utils.constant.Constants.SCRATCH_CARD_EXPIRED_BOTTOM_SHEET
import com.navi.coin.utils.constant.Constants.SCRATCH_CARD_IN_PROGRESS_BOTTOM_SHEET
import com.navi.coin.utils.constant.Constants.SCRATCH_CARD_RECEIVED_BOTTOM_SHEET
import com.navi.coin.utils.constant.Constants.ScratchCardHistoryScreen.VIEW_MORE_SCROLL_DOWN_IN_DP
import com.navi.coin.utils.ext.scaledSp
import com.navi.coin.utils.formatDuration
import com.navi.coin.vm.ScratchCardScreenVM
@@ -101,8 +105,13 @@ import com.navi.rr.common.widgetFactory.WidgetRenderer
import com.navi.rr.utils.NaviRRAnalytics
import com.navi.rr.utils.constants.EventConstants.BOTTOM_SHEET_TYPE
import com.navi.rr.utils.constants.EventConstants.SCRATCH_CARD_HISTORY_BOTTOM_SHEET_VISIBLE_EVENT
import com.navi.rr.utils.custompager.PagerItemState
import com.navi.rr.utils.custompager.PagerStateHolder
import com.navi.uitron.model.UiTronResponse
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@SuppressLint("MutableCollectionMutableState")
@OptIn(ExperimentalFoundationApi::class)
@@ -111,17 +120,23 @@ fun ScratchCardListRenderer(
modifier: Modifier,
content: List<WidgetModelDefinition<UiTronResponse>>,
viewModel: ScratchCardScreenVM,
scratchHistoryCardList: LazyPagingItems<ScratchCard>,
metadata: Map<String?, String?>?,
renderBottomSheet: ((String, ScratchCard) -> Unit)?,
showScratchCard: (ScratchCard) -> Unit,
isScratchCardDisplayed: Boolean,
setScratchCardDisplayed: (Boolean) -> Unit,
pagerStates: PagerStateHolder<ScratchCard>,
) {
val analyticsHandler by remember { mutableStateOf(viewModel.analyticsHandler.Rewards()) }
val paginatedHistoryScreenState =
viewModel.scratchCardHistoryListSource.scratchCardHistoryListData
.collectAsStateWithLifecycle()
viewModel.scratchCardPager.scratchCardHistoryListData.collectAsStateWithLifecycle()
var hasMoreItemsToLoad by remember { mutableStateOf(true) }
var isLoading by remember { mutableStateOf(false) }
val localDensity = LocalDensity.current
val scrollToInPixel = remember { with(localDensity) { VIEW_MORE_SCROLL_DOWN_IN_DP.dp.toPx() } }
val coroutineScope = rememberCoroutineScope()
var clickedViewMore by remember { mutableStateOf(false) }
BoxWithConstraints {
val screenHeight = maxHeight
Column(modifier = Modifier.fillMaxSize()) {
@@ -136,24 +151,29 @@ fun ScratchCardListRenderer(
) {
when (paginatedHistoryScreenState.value) {
is ScratchCardPaginatedHistoryScreenState.Success -> {
LaunchedEffect(
scratchHistoryCardList.itemCount,
scratchHistoryCardList
) {
LaunchedEffect(pagerStates.pagingItems.size) {
viewModel.onScratchCardListChanged(
scratchHistoryCardList,
pagerStates.pagingItems,
isScratchCardDisplayed,
setScratchCardDisplayed
)
if (clickedViewMore) {
animateScrollDown(
coroutineScope = coroutineScope,
lazyGridState = viewModel.scratchCardPager.state,
scrollToInPixel = scrollToInPixel
)
clickedViewMore = false
}
}
}
else -> Unit
}
val lazyGridState = rememberLazyGridState()
LazyVerticalGrid(
columns = GridCells.Fixed(2),
state = lazyGridState,
state = viewModel.scratchCardPager.state,
) {
items(count = content.size, span = { GridItemSpan(2) }) {
WidgetRenderer(widget = content[it], viewModel = viewModel)
@@ -196,26 +216,48 @@ fun ScratchCardListRenderer(
lazyGridScope = this@LazyVerticalGrid,
viewModel = viewModel,
paginatedHistoryScreenState = paginatedHistoryScreenState.value,
scratchHistoryCardList = scratchHistoryCardList,
metadata = metadata,
renderBottomSheet = renderBottomSheet,
showScratchCard = showScratchCard,
analyticsHandler = analyticsHandler
analyticsHandler = analyticsHandler,
pagerStates = pagerStates
)
}
when (pagerStates.pagerLoadingItemState) {
is PagerItemState.Loading -> {
hasMoreItemsToLoad = true
isLoading = true
}
is PagerItemState.Error -> {
hasMoreItemsToLoad = false
clickedViewMore = false
item(span = { GridItemSpan(2) }) {
RewardPaginationErrorComponent() {
viewModel.scratchCardPager.retry()
}
}
}
is PagerItemState.Loaded -> {
hasMoreItemsToLoad = true
isLoading = false
}
is PagerItemState.EndOfList -> {
hasMoreItemsToLoad = false
}
else -> {}
}
scratchHistoryCardList.apply {
when {
loadState.refresh is LoadState.Loading -> {
RewardHistoryLoadingComponent()
}
loadState.refresh is LoadState.Error -> {
val error =
scratchHistoryCardList.loadState.refresh as LoadState.Error
RewardPaginationErrorComponent(scratchHistoryCardList::retry)
}
loadState.append is LoadState.Error -> {
RewardPaginationErrorComponent(scratchHistoryCardList::retry)
item(span = { GridItemSpan(2) }) {
if (hasMoreItemsToLoad) {
if (pagerStates.pagingItems.size > 0) {
ViewMoreButton(isLoading) {
if (isLoading.not()) {
viewModel.scratchCardPager.loadNextPage()
clickedViewMore = true
}
}
} else {
RewardHistoryLoadingComponent()
}
}
}
}
@@ -226,15 +268,81 @@ fun ScratchCardListRenderer(
}
}
private fun animateScrollDown(
coroutineScope: CoroutineScope,
lazyGridState: LazyGridState,
scrollToInPixel: Float
) {
coroutineScope.launch {
delay(300)
lazyGridState.animateScrollBy(
value = scrollToInPixel,
animationSpec = tween(durationMillis = 300)
)
}
}
@Composable
private fun ViewMoreButton(loading: Boolean, onClick: () -> Unit) {
Box(modifier = Modifier, contentAlignment = Alignment.Center) {
Box(
modifier =
Modifier.padding(top = 24.dp, bottom = 32.dp)
.shadow(
elevation = 8.dp,
spotColor = Color(0xFFB0C0D9).copy(0.3f),
clip = true,
shape = RoundedCornerShape(4.dp)
)
.width(156.dp)
.height(32.dp)
.background(color = Color.White, shape = RoundedCornerShape(4.dp))
.clickable(interactionSource = NoRippleIndicationSource(), indication = null) {
onClick.invoke()
},
contentAlignment = Alignment.Center
) {
if (loading) {
NaviCoinLottieAnimation(
modifier = Modifier.width(32.dp),
lottie = R.raw.progress_loader_purple_dotted,
isRemoteLottie = false
)
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "View more",
textAlign = TextAlign.Center,
style =
TextStyle(
fontSize = 12.scaledSp(),
lineHeight = 18.scaledSp(),
fontFamily = ttComposeFontFamily,
fontWeight = getFontWeight(FontWeightEnum.TT_SEMI_BOLD),
color = ctaTertiary,
),
modifier = Modifier.padding(end = 8.dp)
)
Icon(
painter = painterResource(WidgetsR.drawable.ic_arrow_black_down),
contentDescription = "",
modifier = Modifier.size(14.dp)
)
}
}
}
}
}
fun scratchCardList(
lazyGridScope: LazyGridScope,
viewModel: ScratchCardScreenVM,
paginatedHistoryScreenState: ScratchCardPaginatedHistoryScreenState,
scratchHistoryCardList: LazyPagingItems<ScratchCard>,
metadata: Map<String?, String?>?,
renderBottomSheet: ((String, ScratchCard) -> Unit)?,
showScratchCard: (ScratchCard) -> Unit,
analyticsHandler: NaviRRAnalytics.Rewards
analyticsHandler: NaviRRAnalytics.Rewards,
pagerStates: PagerStateHolder<ScratchCard>
) {
if (paginatedHistoryScreenState is ScratchCardPaginatedHistoryScreenState.Empty) {
@@ -259,11 +367,11 @@ fun scratchCardList(
)
}
}
} else if (paginatedHistoryScreenState is ScratchCardPaginatedHistoryScreenState.Success) {
lazyGridScope.items(count = scratchHistoryCardList.itemCount) { index ->
val item = scratchHistoryCardList[index]
} else {
lazyGridScope.items(count = pagerStates.pagingItems.size) { index ->
val item = pagerStates.pagingItems[index]
val firstElement = index % 2 == 0
item?.let {
item.let {
if (
item.status == ScratchCardStatus.LOCKED.name ||
item.status == ScratchCardStatus.EXPIRED.name

View File

@@ -35,6 +35,7 @@ import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
@@ -57,8 +58,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.deeplink.util.DeeplinkConstants.PRODUCT_HELP_PAGE
@@ -106,6 +105,7 @@ import com.navi.rr.scratchcard.model.ScratchCardResponse
import com.navi.rr.scratchcard.ui.compose.ScratchCardRenderer
import com.navi.rr.utils.NaviRRAnalytics
import com.navi.rr.utils.constants.EventConstants
import com.navi.rr.utils.custompager.PagerStateHolder
import com.navi.rr.utils.ext.clickable
import com.navi.rr.utils.filterAndPrioritize
import com.navi.uitron.utils.setShimmerEffect
@@ -130,9 +130,7 @@ fun ScratchCardHistoryScreen(
var isScratchCardDisplayed by remember { mutableStateOf(false) }
val scratchCardHistoryScreenState =
viewModel.scratchCardHistoryScreenState.collectAsStateWithLifecycle()
val scratchCardHistoryList = viewModel.scratchCardHistoryListPager.collectAsLazyPagingItems()
val pagerStates by viewModel.scratchCardPager.pagerStates.collectAsState()
val currentScratchCard by
viewModel.scratchCardUseCases.scratchCard.collectAsStateWithLifecycle()
val nextScratchCard by
@@ -191,13 +189,13 @@ fun ScratchCardHistoryScreen(
}
}
LaunchedEffect(Unit) { viewModel.refreshData(scratchCardHistoryList) }
LaunchedEffect(Unit) { viewModel.refreshData(pagerStates.pagingItems) }
LaunchedEffect(Unit) {
viewModel.scratchCardUseCases.scratchingComplete.collect {
if (it) {
viewModel.clearScratchCardList()
viewModel.refreshData(scratchCardHistoryList)
viewModel.refreshData(pagerStates.pagingItems)
viewModel.scratchCardUseCases.setScratchingComplete(false)
}
}
@@ -251,14 +249,14 @@ fun ScratchCardHistoryScreen(
state = state,
viewModel = viewModel,
navigator = navigator,
scratchCardHistoryList = scratchCardHistoryList,
pagerStates = pagerStates,
renderBottomSheet = { bottomSheetId, scratchCard ->
renderBottomSheet(bottomSheetId, scratchCard)
},
showScratchCard = { startingCard ->
viewModel.setScratchCardList(
filterAndPrioritize(
list = scratchCardHistoryList.itemSnapshotList.items,
list = pagerStates.pagingItems,
condition = { it.status == ScratchCardStatus.LOCKED.name },
valueToStartCondition = { startingCard.refId == it.refId }
)
@@ -364,7 +362,7 @@ fun ScratchCardHistoryScreen(
onBackPress = {
viewModel.clearScratchCardList()
if (viewModel.userHasScratchedCard) {
viewModel.refreshData(scratchCardHistoryList)
viewModel.refreshData(pagerStates.pagingItems)
}
}
) { response ->
@@ -414,7 +412,7 @@ fun ScratchCardHistoryScreen(
.orFalse(),
onBackPress = {
viewModel.clearScratchCardList()
viewModel.refreshData(scratchCardHistoryList)
viewModel.refreshData(pagerStates.pagingItems)
}
) { response ->
scratchAnalyticsTrigger(response, analyticsHandler)
@@ -446,7 +444,7 @@ fun ScratchCardHistoryScreen(
Modifier.clickable(disableRipple = true) {
viewModel.clearScratchCardList()
if (viewModel.userHasScratchedCard)
viewModel.refreshData(scratchCardHistoryList)
viewModel.refreshData(pagerStates.pagingItems)
},
painter = painterResource(CommonR.drawable.ic_close_cross_white),
contentDescription = null
@@ -471,11 +469,11 @@ fun ScratchCardHistorySuccessScreen(
state: ScratchCardHistoryScreenState.Success,
viewModel: ScratchCardScreenVM,
navigator: DestinationsNavigator,
scratchCardHistoryList: LazyPagingItems<ScratchCard>,
renderBottomSheet: ((String, ScratchCard) -> Unit)?,
showScratchCard: (ScratchCard) -> Unit,
isScratchCardDisplayed: Boolean,
setScratchCardDisplayed: (Boolean) -> Unit,
pagerStates: PagerStateHolder<ScratchCard>,
) {
val systemUiController = rememberSystemUiController()
val uiTronTopConfigHeightInPx = dpToPx(396)
@@ -506,12 +504,12 @@ fun ScratchCardHistorySuccessScreen(
modifier = Modifier.padding(it),
content = state.data.screenStructure?.content?.widgets ?: listOf(),
viewModel = viewModel,
scratchHistoryCardList = scratchCardHistoryList,
renderBottomSheet = renderBottomSheet,
showScratchCard = showScratchCard,
metadata = state.data.metaData,
isScratchCardDisplayed = isScratchCardDisplayed,
setScratchCardDisplayed = setScratchCardDisplayed,
pagerStates = pagerStates
)
},
backgroundColor = Color.Transparent

View File

@@ -111,6 +111,10 @@ object Constants {
const val EARN_UPTO = "Earn Upto"
const val FOR_ = "for "
}
object ScratchCardHistoryScreen {
const val VIEW_MORE_SCROLL_DOWN_IN_DP = 115
}
}
sealed class Status(val value: String) {

View File

@@ -8,16 +8,12 @@
package com.navi.coin.vm
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import androidx.paging.compose.LazyPagingItems
import com.navi.base.utils.orZero
import com.navi.coin.models.model.ScratchCard
import com.navi.coin.models.states.ScratchCardHistoryScreenState
import com.navi.coin.models.states.ScratchCardStatus
import com.navi.coin.navigator.CoinNavigationActions
import com.navi.coin.repo.pagingsource.ScratchCardHistoryListSource
import com.navi.coin.repo.pagingsource.ScratchCardHistoryCustomPager
import com.navi.coin.repo.repository.ScratchCardHistoryScreenRepo
import com.navi.coin.usecase.ScratchCardUseCases
import com.navi.coin.utils.constant.Constants
@@ -32,6 +28,7 @@ import com.navi.rr.common.constants.SCRATCH_CARD_HISTORY_SCREEN
import com.navi.rr.common.di.qualifiers.CountDownHelperQualifier
import com.navi.rr.common.models.RRErrorData
import com.navi.rr.utils.cachingPngs
import com.navi.rr.utils.custompager.CustomPagerConfig
import com.navi.rr.utils.facade.ICountDownHelper
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.model.action.SchedulerAction
@@ -44,10 +41,12 @@ import com.navi.uitron.model.data.UiTronActionData
import com.ramcosta.composedestinations.spec.Direction
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -56,10 +55,20 @@ class ScratchCardScreenVM
@Inject
constructor(
private val scratchCardHistoryScreenRepo: ScratchCardHistoryScreenRepo,
var scratchCardHistoryListSource: ScratchCardHistoryListSource,
@CountDownHelperQualifier val countDownHelper: ICountDownHelper,
scratchCardUseCasesFactory: ScratchCardUseCases.Factory, // Inject the factory
) : CoinBaseVM() {
val scratchCardPager =
ScratchCardHistoryCustomPager(
repository = scratchCardHistoryScreenRepo,
scope = viewModelScope.plus(SupervisorJob()),
pagerConfig =
CustomPagerConfig(
autoLoadEnabled = false,
prefetchDistance = 0,
triggerInitialLoad = false
)
)
val scratchCardUseCases: ScratchCardUseCases = scratchCardUseCasesFactory.create(viewModelScope)
@@ -88,27 +97,9 @@ constructor(
}
}
val scratchCardHistoryListPager =
Pager(
config =
PagingConfig(
pageSize = scratchCardHistoryListSource.DEFAULT_PAGE_SIZE,
prefetchDistance = scratchCardHistoryListSource.DEFAULT_PAGE_SIZE,
initialLoadSize = scratchCardHistoryListSource.DEFAULT_PAGE_SIZE * 5
),
pagingSourceFactory = {
ScratchCardHistoryListSource(
scratchCardHistoryScreenRepo = scratchCardHistoryScreenRepo
)
.also { scratchCardHistoryListSource = it }
}
)
.flow
.cachedIn(scope = viewModelScope)
fun refreshData(scratchCardHistoryList: LazyPagingItems<ScratchCard>) {
fun refreshData(scratchCardHistoryList: List<ScratchCard>) {
launch {
scratchCardHistoryListSource.refresh()
scratchCardPager.refresh()
fetchScratchCardTopUiTronConfigs()
userHasScratchedCard = false
cancelAllTimer(scratchCardHistoryList)
@@ -265,8 +256,8 @@ constructor(
handle[key] = value
}
fun createAllTimers(scratchCardList: LazyPagingItems<ScratchCard>) {
scratchCardList.itemSnapshotList
fun createAllTimers(scratchCardList: List<ScratchCard>) {
scratchCardList
.filter {
it?.status == ScratchCardStatus.LOCKED.name &&
it.expiresAfter.orZero() < ONE_DAY_IN_MILLI_SECONDS
@@ -285,10 +276,9 @@ constructor(
}
}
private fun cancelAllTimer(scratchCardList: LazyPagingItems<ScratchCard>) {
scratchCardList.itemSnapshotList
.filter { it?.status == ScratchCardStatus.LOCKED.name }
.filterNotNull()
private fun cancelAllTimer(scratchCardList: List<ScratchCard>) {
scratchCardList
.filter { it.status == ScratchCardStatus.LOCKED.name }
.forEach { handle[it.refId.toString()] = null }
launch { countDownHelper.cancelAllTimers() }
}
@@ -353,13 +343,13 @@ constructor(
}
fun onScratchCardListChanged(
scratchHistoryCardList: LazyPagingItems<ScratchCard>,
scratchHistoryCardList: List<ScratchCard>,
isScratchCardDisplayed: Boolean,
setScratchCardDisplayed: (Boolean) -> Unit,
) {
createAllTimers(scratchHistoryCardList)
if (!isScratchCardDisplayed) {
scratchHistoryCardList.itemSnapshotList
scratchHistoryCardList
.filterNotNull()
.filter { it.status == ScratchCardStatus.LOCKED.name }
.let {

View File

@@ -0,0 +1,12 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.rr.common.models
enum class XTarget {
RUDOLPH,
}

View File

@@ -0,0 +1,41 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.rr.utils.custompager
import androidx.compose.foundation.lazy.grid.LazyGridState
import kotlinx.coroutines.CoroutineScope
/**
* Base class for paginating a lazy grid in Jetpack Compose. This class extends [BaseLazyPager] to
* handle paginated data loading with support for a Compose [LazyGridState].
*
* @param IndividualResponseType The type of individual items in the list.
* @param NetworkResponseType The type of the network response received when loading data.
* @param scope [CoroutineScope] used to observe scroll state and trigger data loading.
* @param config Configuration object that determines the pager's behavior, such as prefetch
* distance and auto-loading.
*/
abstract class BaseLazyGridPager<IndividualResponseType : Any, NetworkResponseType : Any>(
private val scope: CoroutineScope,
private val config: CustomPagerConfig
) : BaseLazyPager<IndividualResponseType, NetworkResponseType>(scope = scope, config = config) {
/** The state object that keeps track of the scroll state in the lazy grid. */
val state by lazy { LazyGridState() }
/**
* Determines the index of the last visible item in the lazy grid. Adjusts the value based on
* the offset.
*
* @return The index of the last visible item relative to the currently loaded pages.
*/
override fun getLastPageVisibleItemIndex(): Int {
val lastVisibleItemIndex = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
return (lastVisibleItemIndex - offset).coerceAtLeast(0)
}
}

View File

@@ -0,0 +1,41 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.rr.utils.custompager
import androidx.compose.foundation.lazy.LazyListState
import kotlinx.coroutines.CoroutineScope
/**
* Base class for paginating a lazy list in Jetpack Compose. This class extends [BaseLazyPager] to
* handle paginated data loading with support for a Compose [LazyListState].
*
* @param IndividualResponseType The type of individual items in the list.
* @param NetworkResponseType The type of the network response received when loading data.
* @param scope [CoroutineScope] used to observe scroll state and trigger data loading.
* @param config Configuration object that determines the pager's behavior, such as prefetch
* distance and auto-loading.
*/
abstract class BaseLazyListPager<IndividualResponseType : Any, NetworkResponseType : Any>(
private val scope: CoroutineScope,
private val config: CustomPagerConfig
) : BaseLazyPager<IndividualResponseType, NetworkResponseType>(scope = scope, config = config) {
/** The state object that keeps track of the scroll state in the lazy list. */
val state by lazy { LazyListState() }
/**
* Determines the index of the last visible item in the lazy list. Adjusts the value based on
* the offset.
*
* @return The index of the last visible item relative to the currently loaded pages.
*/
override fun getLastPageVisibleItemIndex(): Int {
val lastVisibleItemIndex = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
return (lastVisibleItemIndex - offset).coerceAtLeast(0)
}
}

View File

@@ -0,0 +1,52 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.rr.utils.custompager
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* Base class for lazy pagination in Jetpack Compose. This class extends [BasePager] to handle
* paginated data loading.
*
* @param IndividualResponseType The type of individual items in the list.
* @param NetworkResponseType The type of the network response received when loading data.
* @param scope [CoroutineScope] used to observe scroll state and trigger data loading.
* @param config Configuration object that determines the pager's behavior, such as prefetch
* distance and auto-loading.
*/
abstract class BaseLazyPager<IndividualResponseType : Any, NetworkResponseType : Any>(
private val scope: CoroutineScope,
config: CustomPagerConfig
) : BasePager<IndividualResponseType, NetworkResponseType>(scope = scope, config = config) {
/**
* Initializes the pager. If auto-load is enabled in the [config], it will start observing the
* scroll state to load pages automatically.
*/
init {
if (config.autoLoadEnabled) observeScrollState()
}
/**
* Observes the scroll state of the lazy grid/list using [snapshotFlow]. When the last visible
* item is near the end, it triggers loading the next page.
*/
private fun observeScrollState() {
scope.launch {
snapshotFlow { getLastPageVisibleItemIndex() }
.collect { lastPageVisibleItem ->
if (lastPageVisibleItem != 0) {
handleScrollState(lastPageVisibleItem)
}
}
}
}
abstract fun getLastPageVisibleItemIndex(): Int
}

View File

@@ -0,0 +1,197 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.rr.utils.custompager
import com.navi.common.checkmate.model.MetricInfo
import com.navi.common.network.models.RepoResult
import com.navi.common.utils.isValidResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
* Base class for a custom pager implementation that supports paginated data loading.
*
* @param IndividualResponseType The type of each item in the list.
* @param NetworkResponseType The type of the response received from the network.
* @param scope CoroutineScope used to launch coroutines for data loading operations.
* @param config Configuration parameters for the pager such as prefetch distance.
*/
abstract class BasePager<IndividualResponseType : Any, NetworkResponseType : Any>(
private val scope: CoroutineScope,
private val config: CustomPagerConfig
) {
protected val DEFAULT_PAGE_SIZE: Int = 10
protected val DEFAULT_START_KEY: Int = 0
protected var pageSize: Int = DEFAULT_PAGE_SIZE
protected var pageNumber: Int = DEFAULT_START_KEY
/** Offset for calculating the last visible item index of the last fetched page */
protected var offset = 0
private val isReadyToLoad: Boolean
get() = pagerItemState.value == PagerItemState.Loaded
/**
* MutableStateFlow representing the current state of the pager item (Idle, Loading, Loaded,
* Error, EndOfList)
*/
protected val pagerItemState = MutableStateFlow<PagerItemState>(PagerItemState.Idle)
/** MutableStateFlow holding the list of items that have been loaded */
protected val pagerList = MutableStateFlow<MutableList<IndividualResponseType>>(mutableListOf())
/**
* Combines the list of items and pager item state into a [PagerStateHolder]. Provides a stream
* of pager states and the list of loaded items.
*/
val pagerStates: StateFlow<PagerStateHolder<IndividualResponseType>> =
combine(
pagerList,
pagerItemState,
) { pagingItems, pagerItemState ->
PagerStateHolder(
pagingItems = pagingItems,
pagerLoadingItemState = pagerItemState,
)
}
.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue =
PagerStateHolder(
pagingItems = pagerList.value,
pagerLoadingItemState = pagerItemState.value,
)
)
init {
if (config.triggerInitialLoad || config.autoLoadEnabled) startInitialLoading()
}
/**
* Handles the scroll state and triggers loading of the next page if necessary.
*
* @param lastPageVisibleItemIndex The index of the last visible item in the list.
*/
protected fun handleScrollState(lastPageVisibleItemIndex: Int) {
if (shouldLoadNextPage(lastPageVisibleItemIndex)) {
loadNextPage()
}
}
/**
* Determines whether the next page should be loaded based on the scroll state.
*
* @param lastVisibleItemIndex The index of the last visible item of the last page fetched.
* @return true if the next page should be loaded, false otherwise.
*/
private fun shouldLoadNextPage(lastVisibleItemIndex: Int): Boolean {
if (config.prefetchDistance == 0) return false
val scrollConditionSatisfied = (lastVisibleItemIndex >= config.prefetchDistance)
return scrollConditionSatisfied && isReadyToLoad
}
/** Loads the next page of data. Can be overridden to provide custom load behavior. */
open fun loadNextPage() {
if (pagerStates.value.pagerLoadingItemState == PagerItemState.Loading) return
scope.launch { load() }
}
/** Retries loading the next page in case of an error. */
open fun retry() {
loadNextPage()
}
/**
* Loads data for the next page. This function is responsible for managing the loading, success,
* error, and end-of-list states.
*/
private suspend fun load() {
handleLoadingState()
updateState(PagerItemState.Loading)
val response = fetchData(pageNumber, pageSize, metricInfo)
if (response.isValidResponse()) {
pageNumber++
handleSuccessData(response.data)
updateState(PagerItemState.Loaded)
handleIfEndOfListState(response.data)
performPostLoadActions(response.data)
} else {
handleErrorState(response.data)
updateState(PagerItemState.Error("Something went wrong"))
}
}
protected fun updateState(pagerItem: PagerItemState) {
pagerItemState.update { pagerItem }
}
protected fun handleLoadingState() {
pagerItemState.update { PagerItemState.Loading }
}
/**
* Abstract function to fetch the page data from the network. Must be implemented by subclasses.
*
* @param pageNumber The current page number.
* @param pageSize The number of items per page.
* @param metricInfo Metric information to track the pager's performance.
* @return [RepoResult] containing the network response.
*/
abstract suspend fun fetchData(
pageNumber: Int,
pageSize: Int,
metricInfo: MetricInfo<RepoResult<NetworkResponseType>>,
): RepoResult<NetworkResponseType>
protected open fun handleSuccessData(data: NetworkResponseType?) {
offset = pagerList.value.size // Update offset to reflect the size of the list
}
protected open fun handleIfEndOfListState(data: NetworkResponseType?) {
if (isEndOfTotalList()) {
updateState(PagerItemState.EndOfList)
}
}
abstract fun handleErrorState(data: NetworkResponseType?)
open fun refresh() {
scope.launch {
offset = 0
pageNumber = DEFAULT_START_KEY
pagerItemState.update { PagerItemState.Idle } // Reset to idle before reloading
pagerList.update { mutableListOf() }
load()
}
}
private fun startInitialLoading() {
loadNextPage()
}
/**
* Metric information to track the pager's performance, such as the number of pages or items
* loaded.
*/
abstract val metricInfo: MetricInfo<RepoResult<NetworkResponseType>>
/**
* Performs any post-load actions after the data has been loaded. Can be overridden to provide
*/
abstract fun performPostLoadActions(data: NetworkResponseType?)
abstract fun isEndOfTotalList(): Boolean
}

View File

@@ -0,0 +1,19 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.rr.utils.custompager
data class CustomPagerConfig(
val triggerInitialLoad: Boolean = true,
var autoLoadEnabled: Boolean = false,
val prefetchDistance: Int = 5,
)
data class PagerStateHolder<T>(
val pagingItems: MutableList<T>,
val pagerLoadingItemState: PagerItemState,
)

View File

@@ -0,0 +1,20 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.rr.utils.custompager
sealed interface PagerItemState {
object Loaded : PagerItemState
object Loading : PagerItemState
object EndOfList : PagerItemState
class Error(message: String) : PagerItemState
object Idle : PagerItemState
}

View File

@@ -19,3 +19,7 @@ fun <T> List<T>.thirdOrNull(): T? {
}
fun <T> T?.toJson(): String = getGsonBuilders().toJson(this).orElse("{}")
fun Int.isEven(): Boolean = this % 2 == 0
fun Int.isOdd(): Boolean = this % 2 != 0