NTP-70595 | Weekly Hisaab Implementation (#16581)
Co-authored-by: Sohan Reddy Atukula <sohan.reddy@navi.com>
This commit is contained in:
@@ -142,6 +142,8 @@ object FirebaseRemoteConfigHelper {
|
||||
const val NAVI_PAY_ENABLE_DARK_KNIGHT = "NAVI_PAY_ENABLE_DARK_KNIGHT"
|
||||
const val NAVI_PAY_UPI_GLOBAL_ENABLED = "NAVI_PAY_UPI_GLOBAL_ENABLED"
|
||||
const val MM_DISCOVERABILITY_ENABLED = "MM_DISCOVERABILITY_ENABLED"
|
||||
const val MM_WEEKLY_HISAAB_ENABLED = "MM_WEEKLY_HISAAB_ENABLED"
|
||||
const val MM_UPI_SYNC_AND_CONFIG_API_TIMEOUT_MS = "MM_UPI_SYNC_AND_CONFIG_API_TIMEOUT_MS"
|
||||
const val NAVI_PAY_EMI_CONVERSION_ENABLED = "NAVI_PAY_EMI_CONVERSION_ENABLED"
|
||||
const val SHOULD_USE_GLOBAL_SCOPE_FOR_DASHBOARD_SYNC =
|
||||
"SHOULD_USE_GLOBAL_SCOPE_FOR_DASHBOARD_SYNC"
|
||||
|
||||
@@ -1096,3 +1096,34 @@ interface MMDiscoverabilityEventTracker {
|
||||
@EventName("mm_discoverability_entry_point_load_triggered")
|
||||
fun onMMDiscoverabilityEntryPointLoadTriggered()
|
||||
}
|
||||
|
||||
@AutoGenerate
|
||||
interface MMWeeklyHisaabEventTracker {
|
||||
|
||||
@EventName("mm_hisaab_widget_visible") fun onMMHisaabWidgetVisible()
|
||||
|
||||
@EventName("mm_mrr_current_month_spends_clicked") fun onMMMRRCurrentMonthSpendsClicked()
|
||||
|
||||
@EventName("mm_mrr_previous_month_spends_clicked") fun onMMMRRPreviousMonthSpendsClicked()
|
||||
|
||||
@EventName("mm_mrr_spends_comparison_balance_insight_clicked")
|
||||
fun onMMWeeklyHisaabBalanceInsightClicked()
|
||||
|
||||
@EventName("mm_mrr_spends_comparison_balance_insight")
|
||||
fun onMMWeeklyHisaabBalanceInsight(state: String, color: String)
|
||||
|
||||
@EventName("mm_weekly_hisaab_remote_config_disabled")
|
||||
fun onMMWeeklyHisaabDisabledViaRemoteConfig()
|
||||
|
||||
@EventName("mm_weekly_hisaab_config_api_called") fun onMMWeeklyHisaabConfigApiCalled()
|
||||
|
||||
@EventName("mm_weekly_hisaab_litmus_enabled") fun onMMWeeklyHisaablitmusEnabled()
|
||||
|
||||
@EventName("mm_weekly_hisaab_litmus_disabled")
|
||||
fun onMMWeeklyHisaabLitmusDisabled(reason: String)
|
||||
|
||||
@EventName("mm_weekly_hisaab_config_api_failed")
|
||||
fun onMMWeeklyHisaabConfigApiFailed(errorMessage: String, statusCode: String)
|
||||
|
||||
@EventName("mm_upi_txn_sync_skipped_already_running") fun onUpiSyncSkippedAlreadyRunning()
|
||||
}
|
||||
|
||||
@@ -15,7 +15,13 @@ import com.navi.moneymanager.R
|
||||
import com.navi.moneymanager.common.illustration.model.IllustrationSource
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.CHECK_BALANCE_LOCK_ICON
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.SPIKE_ICON_URL
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.WHITE_ARROW_DOWN
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.YELLOW_ARROW_DIAGONAL_RIGHT_UP
|
||||
import com.navi.moneymanager.common.model.CategorySummary
|
||||
import com.navi.moneymanager.common.model.SpendComparisonType
|
||||
import com.navi.moneymanager.common.model.SpendTrendColor
|
||||
import com.navi.moneymanager.common.model.SpendingTrend
|
||||
import com.navi.moneymanager.common.model.database.AccountOverview
|
||||
import com.navi.moneymanager.common.navigation.navigator.MMDeeplinkNavigator.MONEY_MANAGER_ACTIVITY
|
||||
import com.navi.moneymanager.common.network.model.CategoryItemData
|
||||
@@ -41,6 +47,7 @@ import com.navi.moneymanager.entry.model.checkbalance.CheckBalanceMMEntryPointSt
|
||||
import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE
|
||||
import com.navi.naviwidgets.utils.ZERO
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CheckBalanceScreenEntryPointHelper @Inject constructor() {
|
||||
|
||||
@@ -196,4 +203,49 @@ class CheckBalanceScreenEntryPointHelper @Inject constructor() {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun evaluateSpendingTrend(
|
||||
currentMonthTotalSpend: Double,
|
||||
previousMonthTotalSpend: Double,
|
||||
): SpendingTrend? {
|
||||
if (currentMonthTotalSpend == 0.0 || previousMonthTotalSpend == 0.0) return null
|
||||
|
||||
val percentChange =
|
||||
(((currentMonthTotalSpend - previousMonthTotalSpend) / previousMonthTotalSpend) * 100)
|
||||
.roundToInt()
|
||||
|
||||
return when {
|
||||
percentChange > 50 ->
|
||||
SpendingTrend(
|
||||
SpendComparisonType.HIGHER,
|
||||
SpendTrendColor.RED,
|
||||
percentChange,
|
||||
iconUrl = SPIKE_ICON_URL,
|
||||
)
|
||||
|
||||
percentChange in 1..50 ->
|
||||
SpendingTrend(
|
||||
SpendComparisonType.HIGHER,
|
||||
SpendTrendColor.AMBER,
|
||||
percentChange,
|
||||
iconUrl = SPIKE_ICON_URL,
|
||||
)
|
||||
|
||||
percentChange < 0 ->
|
||||
SpendingTrend(
|
||||
SpendComparisonType.LESS,
|
||||
SpendTrendColor.GREEN,
|
||||
kotlin.math.abs(percentChange),
|
||||
iconUrl = WHITE_ARROW_DOWN,
|
||||
)
|
||||
|
||||
else ->
|
||||
SpendingTrend(
|
||||
SpendComparisonType.SIMILAR,
|
||||
SpendTrendColor.GREEN,
|
||||
0,
|
||||
iconUrl = YELLOW_ARROW_DIAGONAL_RIGHT_UP,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.navi.moneymanager.common.dataprovider.utils.isSameAccount
|
||||
import com.navi.moneymanager.common.datasync.model.DataSyncState
|
||||
import com.navi.moneymanager.common.db.database.MMDatabase
|
||||
import com.navi.moneymanager.common.model.CategorySummary
|
||||
import com.navi.moneymanager.common.model.SyncStatusBundle
|
||||
import com.navi.moneymanager.common.model.database.AccountOverview
|
||||
import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider
|
||||
import com.navi.moneymanager.common.network.model.MMConfigResponse
|
||||
@@ -300,10 +301,3 @@ constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SyncStatusBundle(
|
||||
val isFirstMonthSynced: Boolean,
|
||||
val isTotalSyncCompleted: Boolean,
|
||||
val syncState: DataSyncState,
|
||||
val linkedAccounts: List<AccountOverview?>,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.common.dataprovider.data.checkbalance.provider
|
||||
|
||||
import com.navi.base.utils.formatToInrWithDecimals
|
||||
import com.navi.common.utils.monthAbbreviations
|
||||
import com.navi.moneymanager.common.dataprovider.data.checkbalance.helper.CheckBalanceScreenEntryPointHelper
|
||||
import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMWeeklyHisaabConfigResponseHelper
|
||||
import com.navi.moneymanager.common.dataprovider.domain.upi.CheckBalanceScreenEntryPointUPIDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.utils.MMQueryExecutor.Companion.executeQueryFlow
|
||||
import com.navi.moneymanager.common.db.upi.database.UpiSpendDatabase
|
||||
import com.navi.moneymanager.common.model.DateInfo
|
||||
import com.navi.moneymanager.common.model.SelectedMonth
|
||||
import com.navi.moneymanager.common.utils.Constants.DEBIT
|
||||
import com.navi.moneymanager.common.utils.MonthConstants
|
||||
import com.navi.moneymanager.common.utils.combineWithFlatMapLatest
|
||||
import com.navi.moneymanager.common.utils.getDateInfoFromTimestamp
|
||||
import com.navi.moneymanager.common.utils.getPreviousMonthDayLimit
|
||||
import com.navi.moneymanager.entry.model.checkbalance.MMWeeklyHisaabWidgetData
|
||||
import com.navi.moneymanager.entry.model.checkbalance.SpendComparisonData
|
||||
import com.navi.moneymanager.entry.model.checkbalance.SpendComparisonViewState
|
||||
import com.navi.moneymanager.entry.model.checkbalance.SpendItemData
|
||||
import dagger.Lazy
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class CheckBalanceScreenEntryPointUPIDataProviderImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val upiSpendDatabase: Lazy<UpiSpendDatabase>,
|
||||
private val entryPointHelper: CheckBalanceScreenEntryPointHelper,
|
||||
private val mmWeeklyHisaabConfigResponseHelper: MMWeeklyHisaabConfigResponseHelper,
|
||||
) : CheckBalanceScreenEntryPointUPIDataProvider {
|
||||
|
||||
override suspend fun getMMWeeklyHisaabWidgetData(): Flow<MMWeeklyHisaabWidgetData> {
|
||||
val dateInfo = getDateInfoFromTimestamp(System.currentTimeMillis())
|
||||
|
||||
val currentMonth = dateInfo.month
|
||||
val currentYear = dateInfo.year
|
||||
val currentDay = dateInfo.day
|
||||
val previousMonthDay = getPreviousMonthDayLimit(dateInfo)
|
||||
|
||||
val previousMonth = if (currentMonth == 0) 11 else currentMonth - 1
|
||||
val previousYear = if (currentMonth == 0) currentYear - 1 else currentYear
|
||||
|
||||
return combineWithFlatMapLatest(
|
||||
flow1 =
|
||||
getMonthlyTransactionTotal(
|
||||
month = currentMonth,
|
||||
year = currentYear,
|
||||
dayLimit = currentDay,
|
||||
transactionType = DEBIT,
|
||||
),
|
||||
flow2 =
|
||||
getMonthlyTransactionTotal(
|
||||
month = previousMonth,
|
||||
year = previousYear,
|
||||
dayLimit = previousMonthDay,
|
||||
transactionType = DEBIT,
|
||||
),
|
||||
combineTransform = { currentMonthTotalSpend, previousMonthTotalSpend ->
|
||||
Pair(currentMonthTotalSpend, previousMonthTotalSpend)
|
||||
},
|
||||
flatMapLatestTransform = { (currentMonthTotalSpend, previousMonthTotalSpend) ->
|
||||
emit(
|
||||
MMWeeklyHisaabWidgetData(
|
||||
monthName = MonthConstants.monthNames[currentMonth],
|
||||
currentMonthSpend =
|
||||
SpendItemData(
|
||||
monthName = monthAbbreviations[currentMonth],
|
||||
selectedMonth =
|
||||
SelectedMonth(month = currentMonth, year = currentYear),
|
||||
currentDay = currentDay,
|
||||
spendAmount =
|
||||
currentMonthTotalSpend.formatToInrWithDecimals(
|
||||
showDecimalOnlyForFractional = true
|
||||
),
|
||||
),
|
||||
previousMonthSpend =
|
||||
SpendItemData(
|
||||
monthName = monthAbbreviations[previousMonth],
|
||||
selectedMonth =
|
||||
SelectedMonth(month = previousMonth, year = previousYear),
|
||||
currentDay = previousMonthDay,
|
||||
spendAmount =
|
||||
previousMonthTotalSpend.formatToInrWithDecimals(
|
||||
showDecimalOnlyForFractional = true
|
||||
),
|
||||
),
|
||||
spendComparison =
|
||||
getSpendComparisonViewState(
|
||||
currentMonthTotalSpend = currentMonthTotalSpend,
|
||||
previousMonthTotalSpend = previousMonthTotalSpend,
|
||||
dateInfo = dateInfo,
|
||||
entryPointHelper = entryPointHelper,
|
||||
mmWeeklyHisaabConfigResponseHelper =
|
||||
mmWeeklyHisaabConfigResponseHelper,
|
||||
),
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getSpendComparisonViewState(
|
||||
currentMonthTotalSpend: Double,
|
||||
previousMonthTotalSpend: Double,
|
||||
dateInfo: DateInfo,
|
||||
entryPointHelper: CheckBalanceScreenEntryPointHelper,
|
||||
mmWeeklyHisaabConfigResponseHelper: MMWeeklyHisaabConfigResponseHelper,
|
||||
): SpendComparisonViewState {
|
||||
val isUserNotUpiLoyal =
|
||||
mmWeeklyHisaabConfigResponseHelper
|
||||
.getWeeklyHisaabConfig()
|
||||
?.isUserUPILoyalInPreviousMonth
|
||||
?.not() == true
|
||||
|
||||
if (isUserNotUpiLoyal) return SpendComparisonViewState.Hide
|
||||
|
||||
val spendingTrend =
|
||||
entryPointHelper.evaluateSpendingTrend(
|
||||
currentMonthTotalSpend = currentMonthTotalSpend,
|
||||
previousMonthTotalSpend = previousMonthTotalSpend,
|
||||
)
|
||||
|
||||
return spendingTrend?.let {
|
||||
SpendComparisonViewState.Show(
|
||||
SpendComparisonData(
|
||||
percentage = spendingTrend.percentChange,
|
||||
changeType = spendingTrend.text,
|
||||
currentMonthName = MonthConstants.monthNames[dateInfo.month],
|
||||
daysLeft = dateInfo.remainingDaysInMonth,
|
||||
iconUrl = spendingTrend.iconUrl,
|
||||
color = spendingTrend.color,
|
||||
selectedMonth = SelectedMonth(month = dateInfo.month, year = dateInfo.year),
|
||||
)
|
||||
)
|
||||
} ?: SpendComparisonViewState.Hide
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches total UPI transaction amount for a given month, year, and transaction type, limited
|
||||
* up to the specified day (e.g., current date in month).
|
||||
*
|
||||
* This function directly queries the database and emits the total sum as a Flow<Double>, and
|
||||
* will emit updates only when the sum changes.
|
||||
*
|
||||
* @param month The target month (0-based: January = 0)
|
||||
* @param year The target year (e.g., 2025)
|
||||
* @param dayLimit The last day of transaction range (e.g., current day in month)
|
||||
* @param transactionType The type of transaction to include (e.g., "DEBIT" or "CREDIT")
|
||||
* @return A Flow emitting the total transaction amount (in INR) for the given period.
|
||||
*/
|
||||
suspend fun getMonthlyTransactionTotal(
|
||||
month: Int,
|
||||
year: Int,
|
||||
dayLimit: Int,
|
||||
transactionType: String,
|
||||
): Flow<Double> {
|
||||
return executeQueryFlow(
|
||||
queryName =
|
||||
upiSpendDatabase.get().transactionsDao()::fetchUpiTransactionsByMonthAndType
|
||||
.name,
|
||||
methodName = ::getMonthlyTransactionTotal.name,
|
||||
flow =
|
||||
upiSpendDatabase
|
||||
.get()
|
||||
.transactionsDao()
|
||||
.fetchUpiTransactionsByMonthAndType(
|
||||
year = year,
|
||||
month = month,
|
||||
currentDate = dayLimit,
|
||||
transactionType = transactionType,
|
||||
),
|
||||
)
|
||||
// Convert the list of transactions into a sum of amounts
|
||||
.map { txnList -> txnList.sumOf { it.txnAmount ?: 0.0 } }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.common.dataprovider.data.dashboard.helper
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.navi.base.cache.model.NaviCacheEntity
|
||||
import com.navi.base.cache.repository.NaviCacheRepository
|
||||
import com.navi.moneymanager.common.network.di.MoneyManagerGsonDeserializer
|
||||
import com.navi.moneymanager.common.network.di.MoneyManagerGsonSerializer
|
||||
import com.navi.moneymanager.common.network.model.MMWeeklHisaabResponse
|
||||
import com.navi.moneymanager.common.utils.DbCacheConstants.MONEY_MANAGER_WEEKLY_HISAAB_CONFIG_RESPONSE_KEY
|
||||
import dagger.Lazy
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/**
|
||||
* Helper class for managing the weekly Hisaab config response, including caching in memory and
|
||||
* saving to persistent storage via NaviCacheRepository.
|
||||
*/
|
||||
@Singleton
|
||||
class MMWeeklyHisaabConfigResponseHelper
|
||||
@Inject
|
||||
constructor(
|
||||
private val naviCacheRepository: Lazy<NaviCacheRepository>,
|
||||
@MoneyManagerGsonDeserializer private val gsonDeserializer: Gson,
|
||||
@MoneyManagerGsonSerializer private val gsonSerializer: Gson,
|
||||
) {
|
||||
|
||||
// In-memory cache of the weekly Hisaab config to avoid repeated DB reads
|
||||
private var weeklyHisaabConfigResponse: MMWeeklHisaabResponse? = null
|
||||
|
||||
/**
|
||||
* Saves the given weekly Hisaab config both in memory and in the Navi cache repository.
|
||||
*
|
||||
* @param data The response object to be saved.
|
||||
*/
|
||||
suspend fun saveWeeklyHisaabConfigToDB(data: MMWeeklHisaabResponse) {
|
||||
|
||||
weeklyHisaabConfigResponse = data
|
||||
|
||||
val dataJson = gsonSerializer.toJson(data)
|
||||
|
||||
val cacheEntity =
|
||||
NaviCacheEntity(
|
||||
key = MONEY_MANAGER_WEEKLY_HISAAB_CONFIG_RESPONSE_KEY,
|
||||
value = dataJson,
|
||||
version = 1,
|
||||
)
|
||||
|
||||
naviCacheRepository.get().save(cacheEntity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the weekly Hisaab config either from memory (if cached), or from the database if not
|
||||
* previously loaded.
|
||||
*
|
||||
* @return MMWeeklHisaabResponse if found, or null otherwise.
|
||||
*/
|
||||
suspend fun getWeeklyHisaabConfig(): MMWeeklHisaabResponse? {
|
||||
return if (weeklyHisaabConfigResponse == null) {
|
||||
getWeeklyHisaabConfigFromDB()
|
||||
.filterNotNull() //
|
||||
.firstOrNull() //
|
||||
?.also { response -> weeklyHisaabConfigResponse = response }
|
||||
} else {
|
||||
weeklyHisaabConfigResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cold flow that emits the weekly Hisaab config from the cache repository,
|
||||
* deserialized from JSON.
|
||||
*
|
||||
* @return Flow<MMWeeklHisaabResponse?> emitting the config when available.
|
||||
*/
|
||||
private fun getWeeklyHisaabConfigFromDB() = flow {
|
||||
val cacheEntity =
|
||||
naviCacheRepository.get().getAsFlow(MONEY_MANAGER_WEEKLY_HISAAB_CONFIG_RESPONSE_KEY)
|
||||
|
||||
cacheEntity.collect { entity ->
|
||||
emit(
|
||||
entity?.value?.let { value ->
|
||||
gsonDeserializer.fromJson(value, MMWeeklHisaabResponse::class.java)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import com.navi.moneymanager.common.network.model.CustomerProfileData
|
||||
import com.navi.moneymanager.common.network.model.FetchConsentUrlResponse
|
||||
import com.navi.moneymanager.common.network.model.FinarkeinDataResponse
|
||||
import com.navi.moneymanager.common.network.model.MMConfigResponse
|
||||
import com.navi.moneymanager.common.network.model.MMWeeklHisaabResponse
|
||||
import com.navi.moneymanager.common.network.model.PollingStatusResponse
|
||||
import com.navi.moneymanager.common.network.model.RefreshDataResponse
|
||||
import com.navi.moneymanager.common.network.model.SpendGoalData
|
||||
@@ -284,4 +285,14 @@ class RemoteDataProviderImpl @Inject constructor(private val retrofitService: Re
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun fetchMMWeeklyHisaabData(
|
||||
screenName: String
|
||||
): RepoResult<MMWeeklHisaabResponse> {
|
||||
return apiResponseCallback(
|
||||
response = retrofitService.fetchMMWeeklyHisaabData(),
|
||||
metricInfo =
|
||||
MetricInfo.AppMetric(screen = screenName, isNae = { !it.isSuccessWithData() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.navi.moneymanager.common.dataprovider.data.addcategory.provider.AddCa
|
||||
import com.navi.moneymanager.common.dataprovider.data.addcategory.provider.UpiSpendAddCategoryDataProviderImpl
|
||||
import com.navi.moneymanager.common.dataprovider.data.bankaccounts.BankAccountsDataProviderImpl
|
||||
import com.navi.moneymanager.common.dataprovider.data.checkbalance.provider.CheckBalanceScreenEntryPointDataProviderImpl
|
||||
import com.navi.moneymanager.common.dataprovider.data.checkbalance.provider.CheckBalanceScreenEntryPointUPIDataProviderImpl
|
||||
import com.navi.moneymanager.common.dataprovider.data.dashboard.provider.DashboardDataProviderImpl
|
||||
import com.navi.moneymanager.common.dataprovider.data.dashboard.provider.UpiSpendDashboardDataProviderImpl
|
||||
import com.navi.moneymanager.common.dataprovider.data.remote.LocalDataSyncManagerImpl
|
||||
@@ -40,6 +41,7 @@ import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.SpendAnalysisLocalDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.TransactionDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.TransactionHistoryDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.upi.CheckBalanceScreenEntryPointUPIDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.upi.TxnHistoryUpiSpendDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.upi.UpiSpendAddCategoryDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.upi.UpiSpendAnalysisLocalDataProvider
|
||||
@@ -147,6 +149,11 @@ abstract class DataProviderModule {
|
||||
abstract fun bindCheckBalanceScreenEntryPointDataProvider(
|
||||
checkBalanceScreenEntryPointDataProvider: CheckBalanceScreenEntryPointDataProviderImpl
|
||||
): CheckBalanceScreenEntryPointDataProvider
|
||||
|
||||
@Binds
|
||||
abstract fun bindCheckBalanceScreenEntryPointUPIDataProvider(
|
||||
checkBalanceScreenEntryPointUPIDataProvider: CheckBalanceScreenEntryPointUPIDataProviderImpl
|
||||
): CheckBalanceScreenEntryPointUPIDataProvider
|
||||
}
|
||||
|
||||
@Module
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.navi.moneymanager.common.network.model.CustomerProfileData
|
||||
import com.navi.moneymanager.common.network.model.FetchConsentUrlResponse
|
||||
import com.navi.moneymanager.common.network.model.FinarkeinDataResponse
|
||||
import com.navi.moneymanager.common.network.model.MMConfigResponse
|
||||
import com.navi.moneymanager.common.network.model.MMWeeklHisaabResponse
|
||||
import com.navi.moneymanager.common.network.model.OnboardingStatusResponse
|
||||
import com.navi.moneymanager.common.network.model.PollingStatusResponse
|
||||
import com.navi.moneymanager.common.network.model.RefreshDataResponse
|
||||
@@ -94,4 +95,6 @@ interface RemoteDataProvider {
|
||||
requestBody: AddBillRequestBody,
|
||||
billId: String,
|
||||
): RepoResult<BillCalendarResponse>
|
||||
|
||||
suspend fun fetchMMWeeklyHisaabData(screenName: String): RepoResult<MMWeeklHisaabResponse>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.common.dataprovider.domain.upi
|
||||
|
||||
import com.navi.moneymanager.common.dataprovider.domain.LocalDataProvider
|
||||
import com.navi.moneymanager.entry.model.checkbalance.MMWeeklyHisaabWidgetData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface CheckBalanceScreenEntryPointUPIDataProvider : LocalDataProvider {
|
||||
suspend fun getMMWeeklyHisaabWidgetData(): Flow<MMWeeklyHisaabWidgetData>
|
||||
}
|
||||
@@ -23,6 +23,11 @@ import com.navi.moneymanager.common.network.model.UpiSpendTransactionData
|
||||
import dagger.Lazy
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
@Singleton
|
||||
class UpiSpendDBSyncExecutor
|
||||
@@ -33,23 +38,47 @@ constructor(
|
||||
private val upiSpendDatabase: Lazy<UpiSpendDatabase>,
|
||||
private val mmConfigResponseHelper: MMConfigResponseHelper,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private val _syncState = MutableSharedFlow<DataSyncState>(replay = 1)
|
||||
private val syncState: SharedFlow<DataSyncState> = _syncState.asSharedFlow()
|
||||
private var isExecuting = false
|
||||
|
||||
suspend fun execute(screenName: String): DataSyncState {
|
||||
val config = fetchAndSaveConfigResponse(screenName) ?: return DataSyncState.Failed
|
||||
suspend fun execute(screenName: String): SharedFlow<DataSyncState> {
|
||||
mutex.withLock {
|
||||
if (!isExecuting) {
|
||||
isExecuting = true
|
||||
executeInternal(screenName)
|
||||
isExecuting = false
|
||||
}
|
||||
}
|
||||
return syncState
|
||||
}
|
||||
|
||||
private suspend fun executeInternal(screenName: String) {
|
||||
val config =
|
||||
fetchAndSaveConfigResponse(screenName)
|
||||
?: run {
|
||||
_syncState.emit(DataSyncState.Failed)
|
||||
return
|
||||
}
|
||||
|
||||
val cutoffTimestamp =
|
||||
config.timestampConfig?.twelveMonthsOldTimestamp ?: return DataSyncState.Failed
|
||||
config.timestampConfig?.twelveMonthsOldTimestamp
|
||||
?: run {
|
||||
_syncState.emit(DataSyncState.Failed)
|
||||
return
|
||||
}
|
||||
|
||||
val startTimestamp =
|
||||
upiSpendLocalDataSyncManager.getMostRecentTransactionTimestamp(cutoffTimestamp)
|
||||
|
||||
return if (syncTransactions(startTimestamp)) {
|
||||
if (syncTransactions(startTimestamp)) {
|
||||
upiSpendLocalDataSyncManager.updateTotalSyncCompleteFlag()
|
||||
UpiTransactionHistorySpendAnalyserEventTrackerImpl().onDataSyncCompleted()
|
||||
DataSyncState.Completed
|
||||
_syncState.emit(DataSyncState.Completed)
|
||||
} else {
|
||||
UpiTransactionHistorySpendAnalyserEventTrackerImpl().onDataSyncFailed()
|
||||
DataSyncState.Failed
|
||||
_syncState.emit(DataSyncState.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,4 +100,16 @@ interface UpiSpendTransactionDao {
|
||||
startDate: Long,
|
||||
endDate: Long,
|
||||
): Flow<List<UpiTransactionSummaryData>>
|
||||
|
||||
@Query(
|
||||
"SELECT upiTxnId, naviUpiTxnId, txnTimestamp, txnMonth, txnAmount, type, counterPartyName, finalCategory " +
|
||||
"FROM $UPI_SPEND_TRANSACTION_TABLE " +
|
||||
"WHERE txnYear = :year AND txnMonth = :month AND txnDay <= :currentDate AND type = :transactionType "
|
||||
)
|
||||
fun fetchUpiTransactionsByMonthAndType(
|
||||
year: Int,
|
||||
month: Int,
|
||||
currentDate: Int,
|
||||
transactionType: String,
|
||||
): Flow<List<UpiTransactionSummaryData>>
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import com.navi.common.usecase.LitmusExperimentsUseCase
|
||||
import com.navi.common.utils.Constants.LITMUS_EXPERIMENT_CHECK_BALANCE_MM_EXPERIENCE
|
||||
import com.navi.common.utils.Constants.LITMUS_EXPERIMENT_HPC_MM_ROLLOUT
|
||||
import com.navi.moneymanager.common.dataprovider.domain.CheckBalanceScreenEntryPointDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.upi.CheckBalanceScreenEntryPointUPIDataProvider
|
||||
import com.navi.moneymanager.common.datasync.model.DataSyncState
|
||||
import com.navi.moneymanager.common.network.model.MMConfigResponse
|
||||
import com.navi.moneymanager.entry.model.checkbalance.CheckBalanceMMEntryPointData
|
||||
import com.navi.moneymanager.entry.model.checkbalance.MMWeeklyHisaabWidgetData
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -23,6 +25,7 @@ class CheckBalanceMMWidgetHelper
|
||||
constructor(
|
||||
private val litmusExperimentsUseCase: LitmusExperimentsUseCase,
|
||||
private val checkBalanceDataProvider: CheckBalanceScreenEntryPointDataProvider,
|
||||
private val checkBalanceUPIDataProvider: CheckBalanceScreenEntryPointUPIDataProvider,
|
||||
) {
|
||||
suspend fun getCheckBalanceScreenEntryPointData(
|
||||
npciBankCode: String,
|
||||
@@ -38,6 +41,10 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getMMWeeklyHisaabWidgetData(): Flow<MMWeeklyHisaabWidgetData> {
|
||||
return checkBalanceUPIDataProvider.getMMWeeklyHisaabWidgetData()
|
||||
}
|
||||
|
||||
suspend fun isMMDiscoverabilityExperimentEnabled(): Boolean {
|
||||
val experimentInfo =
|
||||
litmusExperimentsUseCase.execute(
|
||||
|
||||
@@ -82,6 +82,7 @@ class ImageRepository : IllustrationRepository {
|
||||
const val WHITE_ARROW_DOWN = "WHITE_ARROW_DOWN"
|
||||
const val DOUBLE_RIGHT_ARROW_BLUE = "DOUBLE_RIGHT_ARROW_BLUE"
|
||||
const val CONTACTS_ACCESS_GREY_BG = "CONTACTS_ACCESS_GREY_BG"
|
||||
const val YELLOW_ARROW_DIAGONAL_RIGHT_UP = "YELLOW_ARROW_DIAGONAL_RIGHT_UP"
|
||||
}
|
||||
|
||||
override fun getResourceId(illustrationName: String, isDarkThemeEnabled: Boolean): Int {
|
||||
@@ -292,6 +293,8 @@ class ImageRepository : IllustrationRepository {
|
||||
"https://public-assets.prod.navi-sa.in/money-manager/svg/category-details/ic_double_right_arrow_blue.svg"
|
||||
CONTACTS_ACCESS_GREY_BG ->
|
||||
"https://public-assets.prod.navi-sa.in/money-manager/svg/category-details/ic_contact_access_grey_bg.svg"
|
||||
YELLOW_ARROW_DIAGONAL_RIGHT_UP ->
|
||||
"https://public-assets.prod.navi-sa.in/money-manager/svg/common/ic_yellow_arrow_diagonal_right_up.svg"
|
||||
else -> EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.common.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.navi.moneymanager.common.datasync.model.DataSyncState
|
||||
import com.navi.moneymanager.common.model.database.AccountOverview
|
||||
|
||||
data class SpendingTrend(
|
||||
val text: SpendComparisonType,
|
||||
val color: SpendTrendColor,
|
||||
val percentChange: Int,
|
||||
val iconUrl: String,
|
||||
)
|
||||
|
||||
data class SyncStatusBundle(
|
||||
val isFirstMonthSynced: Boolean,
|
||||
val isTotalSyncCompleted: Boolean,
|
||||
val syncState: DataSyncState,
|
||||
val linkedAccounts: List<AccountOverview?>,
|
||||
)
|
||||
|
||||
enum class SpendComparisonType {
|
||||
HIGHER,
|
||||
LESS,
|
||||
SIMILAR,
|
||||
}
|
||||
|
||||
enum class SpendTrendColor {
|
||||
RED,
|
||||
AMBER,
|
||||
GREEN,
|
||||
}
|
||||
|
||||
fun SpendTrendColor.toColor(): Color {
|
||||
return when (this) {
|
||||
SpendTrendColor.RED -> Color(0xFFCA6100)
|
||||
SpendTrendColor.AMBER -> Color(0xFF906D00)
|
||||
SpendTrendColor.GREEN -> Color(0xFF1B8332)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.common.model
|
||||
|
||||
data class DateInfo(val day: Int, val month: Int, val year: Int, val remainingDaysInMonth: Int)
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.common.network.model
|
||||
|
||||
data class MMWeeklHisaabResponse(
|
||||
val isHisaabEnabled: Boolean?,
|
||||
val isUserUPILoyalInPreviousMonth: Boolean?,
|
||||
)
|
||||
@@ -19,6 +19,7 @@ import com.navi.moneymanager.common.network.model.EmptyRequestBody
|
||||
import com.navi.moneymanager.common.network.model.FetchConsentUrlResponse
|
||||
import com.navi.moneymanager.common.network.model.FinarkeinDataResponse
|
||||
import com.navi.moneymanager.common.network.model.MMConfigResponse
|
||||
import com.navi.moneymanager.common.network.model.MMWeeklHisaabResponse
|
||||
import com.navi.moneymanager.common.network.model.OnboardingStatusResponse
|
||||
import com.navi.moneymanager.common.network.model.PollingStatusResponse
|
||||
import com.navi.moneymanager.common.network.model.RefreshDataResponse
|
||||
@@ -167,6 +168,10 @@ interface RetrofitService {
|
||||
@Query("configType") configType: String = BILL_CALENDAR
|
||||
): Response<GenericResponse<BillCalendarConfigResponse>>
|
||||
|
||||
@RetryPolicy
|
||||
@GET("/money-manager/core/insights/hisaab")
|
||||
suspend fun fetchMMWeeklyHisaabData(): Response<GenericResponse<MMWeeklHisaabResponse>>
|
||||
|
||||
private companion object {
|
||||
const val MONEY_MANAGER_INSIGHTS = "MONEY_MANAGER_INSIGHTS"
|
||||
const val MONEY_MANAGER = "MONEY_MANAGER"
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.common.ui.composable.checkbalance
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.navi.base.utils.EMPTY
|
||||
import com.navi.base.utils.orZero
|
||||
import com.navi.common.utils.CommonUtils.getDisplayableAmount
|
||||
import com.navi.common.utils.PERCENT
|
||||
import com.navi.common.utils.onClickWithDebounce
|
||||
import com.navi.design.font.FontWeightEnum
|
||||
import com.navi.design.font.getFontWeight
|
||||
import com.navi.design.font.naviFontFamily
|
||||
import com.navi.design.utils.BackgroundDrawableData
|
||||
import com.navi.design.utils.DrawableShape
|
||||
import com.navi.moneymanager.R
|
||||
import com.navi.moneymanager.common.analytics.MMWeeklyHisaabEventTrackerImpl
|
||||
import com.navi.moneymanager.common.illustration.model.IllustrationSource
|
||||
import com.navi.moneymanager.common.illustration.model.IllustrationType
|
||||
import com.navi.moneymanager.common.illustration.model.ImageProperties
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.CHEVRON_PURPLE_RIGHT_ICON
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL
|
||||
import com.navi.moneymanager.common.illustration.ui.Illustration
|
||||
import com.navi.moneymanager.common.model.SelectedMonth
|
||||
import com.navi.moneymanager.common.model.SpendComparisonType
|
||||
import com.navi.moneymanager.common.model.toColor
|
||||
import com.navi.moneymanager.common.ui.composable.base.MMText
|
||||
import com.navi.moneymanager.entry.model.checkbalance.MMWeeklyHisaabWidgetData
|
||||
import com.navi.moneymanager.entry.model.checkbalance.SpendComparisonData
|
||||
import com.navi.moneymanager.entry.model.checkbalance.SpendComparisonViewState
|
||||
import com.navi.moneymanager.entry.model.checkbalance.SpendItemData
|
||||
import com.navi.naviwidgets.extensions.CustomTooltip
|
||||
import com.navi.naviwidgets.models.response.TooltipProperties
|
||||
import com.navi.uitron.model.ui.PeakDirection
|
||||
import com.navi.uitron.model.ui.PeakPosition
|
||||
import com.navi.uitron.model.ui.ToolTipShapeData
|
||||
|
||||
@Composable
|
||||
fun MMWeeklyHisaabWidget(
|
||||
data: MMWeeklyHisaabWidgetData,
|
||||
currentBalance: String,
|
||||
hideBalance: Boolean,
|
||||
isBalanceFetchLoading: Boolean,
|
||||
onSpendAmountClick: (selectedMonth: SelectedMonth?) -> Unit,
|
||||
getEventTracker: () -> MMWeeklyHisaabEventTrackerImpl,
|
||||
) {
|
||||
LaunchedEffect(Unit) { getEventTracker().onMMHisaabWidgetVisible() }
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.background(Color(0xFFF5F5F5))
|
||||
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.background(color = Color(0xFFFFFFFF), shape = MaterialTheme.shapes.extraSmall)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color(0xFFEBEBEB),
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
)
|
||||
.wrapContentHeight()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
MMText(
|
||||
text = stringResource(R.string.monthly_overview_title, data.monthName.orEmpty()),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD,
|
||||
color = Color(0xFF000000),
|
||||
lineHeight = 20.sp,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
SpendItem(
|
||||
spendData = data.currentMonthSpend,
|
||||
onSpendAmountClick = { selectedMonth ->
|
||||
getEventTracker().onMMMRRCurrentMonthSpendsClicked()
|
||||
onSpendAmountClick(selectedMonth)
|
||||
},
|
||||
)
|
||||
when (data.spendComparison) {
|
||||
is SpendComparisonViewState.Show -> {
|
||||
LaunchedEffect(Unit) {
|
||||
getEventTracker()
|
||||
.onMMWeeklyHisaabBalanceInsight(
|
||||
state = "show",
|
||||
color = data.spendComparison.spendComparisonData.color.name,
|
||||
)
|
||||
}
|
||||
CustomTooltip(
|
||||
properties =
|
||||
TooltipProperties(
|
||||
background =
|
||||
BackgroundDrawableData(
|
||||
shape = DrawableShape.RECTANGLE.name,
|
||||
cornerRadius = 6,
|
||||
peakAspectRatio = 2.1f,
|
||||
backgroundColor = "#F7F6F8",
|
||||
),
|
||||
tooltipShapeProperties =
|
||||
ToolTipShapeData(
|
||||
peakHeight = 8,
|
||||
peakDirection = PeakDirection.TOP,
|
||||
peakPosition = PeakPosition.CENTER,
|
||||
peakRadius = 4,
|
||||
bottomPeakRadius = 0,
|
||||
offsetPercentage = 0.73f,
|
||||
),
|
||||
),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 4.dp, bottom = 10.dp),
|
||||
) {
|
||||
SpendComparisonBox(
|
||||
data = data.spendComparison.spendComparisonData,
|
||||
currentBalance = currentBalance,
|
||||
hideBalance = hideBalance,
|
||||
isBalanceFetchLoading = isBalanceFetchLoading,
|
||||
onClicked = { selectedMonth ->
|
||||
getEventTracker().onMMWeeklyHisaabBalanceInsightClicked()
|
||||
onSpendAmountClick(selectedMonth)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpendComparisonViewState.Hide -> {
|
||||
LaunchedEffect(Unit) {
|
||||
getEventTracker()
|
||||
.onMMWeeklyHisaabBalanceInsight(state = "hide", color = EMPTY)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
SpendItem(
|
||||
spendData = data.previousMonthSpend,
|
||||
onSpendAmountClick = { selectedMonth ->
|
||||
getEventTracker().onMMMRRPreviousMonthSpendsClicked()
|
||||
onSpendAmountClick(selectedMonth)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpendItem(
|
||||
spendData: SpendItemData?,
|
||||
onSpendAmountClick: (selectedMonth: SelectedMonth?) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
MMText(
|
||||
text = stringResource(R.string.spends),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeightEnum.NAVI_BODY_REGULAR,
|
||||
color = Color(0xFF141414),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
MMText(
|
||||
text =
|
||||
spendDateRangeText(
|
||||
month = spendData?.monthName.orEmpty(),
|
||||
currentDay = spendData?.currentDay.orZero(),
|
||||
),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR,
|
||||
color = Color(0xFF141414),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier.onClickWithDebounce(
|
||||
indication = null,
|
||||
onClick = { onSpendAmountClick(spendData?.selectedMonth) },
|
||||
),
|
||||
) {
|
||||
MMText(
|
||||
text = spendData?.spendAmount.orEmpty(),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR,
|
||||
color = Color(0xFF191919),
|
||||
lineHeight = 22.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Illustration(
|
||||
illustrationType =
|
||||
IllustrationType.Image(
|
||||
source =
|
||||
IllustrationSource.Remote(
|
||||
CHEVRON_PURPLE_RIGHT_ICON,
|
||||
placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM,
|
||||
)
|
||||
),
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpendComparisonBox(
|
||||
data: SpendComparisonData,
|
||||
currentBalance: String,
|
||||
hideBalance: Boolean,
|
||||
isBalanceFetchLoading: Boolean,
|
||||
onClicked: (SelectedMonth) -> Unit,
|
||||
) {
|
||||
val percentChangeText =
|
||||
getSpendChangeText(percentage = data.percentage, changeType = data.changeType)
|
||||
|
||||
val monthComparisonText =
|
||||
stringResource(R.string.spend_change_month_comparison, data.currentMonthName)
|
||||
|
||||
val balanceLeftText =
|
||||
getBalanceLeftText(
|
||||
hideBalance = hideBalance,
|
||||
currentBalance = currentBalance,
|
||||
daysLeft = data.daysLeft,
|
||||
currentMonthName = data.currentMonthName,
|
||||
isBalanceFetchLoading = isBalanceFetchLoading,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.onClickWithDebounce(indication = null) { onClicked(data.selectedMonth) }
|
||||
.padding(start = 6.dp, end = 6.dp, bottom = 9.dp, top = 17.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Illustration(
|
||||
illustrationType =
|
||||
IllustrationType.Image(
|
||||
source =
|
||||
IllustrationSource.Remote(
|
||||
url = data.iconUrl,
|
||||
placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL,
|
||||
),
|
||||
properties =
|
||||
ImageProperties(colorFilter = ColorFilter.tint(data.color.toColor())),
|
||||
),
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
MMText(
|
||||
text =
|
||||
buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
color = data.color.toColor(),
|
||||
fontSize = 14.sp,
|
||||
fontFamily = naviFontFamily,
|
||||
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD),
|
||||
)
|
||||
) {
|
||||
append(percentChangeText)
|
||||
}
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
color = Color(0xFF1C1C1E),
|
||||
fontSize = 12.sp,
|
||||
fontFamily = naviFontFamily,
|
||||
fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
|
||||
)
|
||||
) {
|
||||
append(" ")
|
||||
append(monthComparisonText)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.wrapContentWidth().padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
MMText(
|
||||
text = balanceLeftText,
|
||||
color = data.color.toColor(),
|
||||
fontSize = 12.sp,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR,
|
||||
modifier = Modifier.wrapContentWidth().padding(start = 18.dp, top = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun spendDateRangeText(month: String, currentDay: Int): String {
|
||||
return if (currentDay == 1) {
|
||||
stringResource(id = R.string.spend_date_single, month, currentDay)
|
||||
} else {
|
||||
stringResource(id = R.string.spend_date_range, month, currentDay)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpendComparisonType.toLocalizedString(): String {
|
||||
return when (this) {
|
||||
SpendComparisonType.HIGHER -> stringResource(id = R.string.higher)
|
||||
SpendComparisonType.LESS -> stringResource(id = R.string.less)
|
||||
SpendComparisonType.SIMILAR -> stringResource(id = R.string.similar_spends)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getBalanceLeftText(
|
||||
hideBalance: Boolean,
|
||||
isBalanceFetchLoading: Boolean,
|
||||
currentBalance: String,
|
||||
daysLeft: Int,
|
||||
currentMonthName: String,
|
||||
): String {
|
||||
val balanceDisplay =
|
||||
if (hideBalance || isBalanceFetchLoading) ". . . ."
|
||||
else currentBalance.getDisplayableAmount()
|
||||
val dayText =
|
||||
pluralStringResource(
|
||||
id = R.plurals.balance_day_left,
|
||||
count = daysLeft,
|
||||
formatArgs = arrayOf(daysLeft),
|
||||
)
|
||||
|
||||
return stringResource(R.string.budget_remaining_info, balanceDisplay, dayText, currentMonthName)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getSpendChangeText(percentage: Int, changeType: SpendComparisonType): String {
|
||||
return if (percentage == 0 && changeType == SpendComparisonType.SIMILAR) {
|
||||
stringResource(R.string.similar_spends)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.spend_change_percent_label,
|
||||
"$percentage$PERCENT",
|
||||
changeType.toLocalizedString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ package com.navi.moneymanager.common.ui.composable.upi
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -53,7 +55,11 @@ fun UpiSpendAnalyserWidgetContainer(content: @Composable () -> Unit, onCrossIcon
|
||||
.background(color = Color(0xFFF9F9FA), shape = CircleShape)
|
||||
.align(Alignment.TopEnd)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onCrossIconClick),
|
||||
.clickable(
|
||||
onClick = onCrossIconClick,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
),
|
||||
) {
|
||||
Illustration(
|
||||
illustrationType =
|
||||
|
||||
@@ -116,6 +116,8 @@ object DbCacheConstants {
|
||||
const val MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY =
|
||||
"MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY"
|
||||
const val MONEY_MANAGER_CONFIG_RESPONSE_KEY = "MONEY_MANAGER_CONFIG_RESPONSE_KEY"
|
||||
const val MONEY_MANAGER_WEEKLY_HISAAB_CONFIG_RESPONSE_KEY =
|
||||
"MONEY_MANAGER_WEEKLY_HISAAB_CONFIG_RESPONSE_KEY"
|
||||
}
|
||||
|
||||
object ScreenNameConstants {
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
package com.navi.moneymanager.common.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.icu.util.Calendar
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -51,6 +53,7 @@ import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import com.navi.base.deeplink.DeepLinkManager
|
||||
import com.navi.base.model.CtaData
|
||||
import com.navi.base.model.LineItem
|
||||
import com.navi.base.utils.BaseUtils.getDayMonthAndYearFromTimestamp
|
||||
@@ -77,6 +80,8 @@ import com.navi.moneymanager.common.illustration.model.IllustrationType
|
||||
import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL
|
||||
import com.navi.moneymanager.common.model.ChipData
|
||||
import com.navi.moneymanager.common.model.ChipIllustration
|
||||
import com.navi.moneymanager.common.model.DateInfo
|
||||
import com.navi.moneymanager.common.model.SelectedMonth
|
||||
import com.navi.moneymanager.common.model.SingleChipSelectionData
|
||||
import com.navi.moneymanager.common.model.SpendGoalState
|
||||
import com.navi.moneymanager.common.model.sectionHeader.MMJourneySource
|
||||
@@ -89,6 +94,8 @@ import com.navi.moneymanager.common.utils.Constants.PRODUCT_HELP_PAGE
|
||||
import com.navi.moneymanager.common.utils.Constants.PRODUCT_HELP_SCREEN_NAME
|
||||
import com.navi.moneymanager.common.utils.Constants.REDIRECTION_CTA
|
||||
import com.navi.moneymanager.common.utils.Constants.RUPEE_SYMBOL
|
||||
import com.navi.moneymanager.common.utils.Constants.SELECTED_MONTH
|
||||
import com.navi.moneymanager.common.utils.Constants.SOURCE
|
||||
import com.navi.moneymanager.common.utils.MonthConstants.monthNames
|
||||
import com.navi.moneymanager.preonboard.launcher.model.GenericErrorBottomSheetData
|
||||
import java.text.NumberFormat
|
||||
@@ -580,3 +587,72 @@ fun getNormalisedPhoneNumber(phoneNumber: String?): String {
|
||||
|
||||
return phoneNumber.filter { it.isAsciiDigit() }.takeLast(n = 10)
|
||||
}
|
||||
|
||||
fun getDateInfoFromTimestamp(timeStamp: Long): DateInfo {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = timeStamp
|
||||
|
||||
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||
val month = calendar.get(Calendar.MONTH) // 0-based
|
||||
val year = calendar.get(Calendar.YEAR)
|
||||
|
||||
val maxDaysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
|
||||
val remainingDays = maxDaysInMonth - day + 1
|
||||
|
||||
return DateInfo(day = day, month = month, year = year, remainingDaysInMonth = remainingDays)
|
||||
}
|
||||
|
||||
fun navigateToUpiSpendAnalysisScreen(
|
||||
activity: Activity,
|
||||
screenName: String,
|
||||
selectedMonth: SelectedMonth,
|
||||
bundle: Bundle?,
|
||||
) {
|
||||
val additionalData = bundle ?: Bundle()
|
||||
additionalData.putParcelable(SELECTED_MONTH, selectedMonth)
|
||||
DeepLinkManager.getDeepLinkListener()
|
||||
?.navigateTo(
|
||||
activity = activity,
|
||||
ctaData =
|
||||
CtaData(
|
||||
url = screenName,
|
||||
parameters =
|
||||
listOf(LineItem(key = SOURCE, value = MMJourneySource.NAVI_UPI.name)),
|
||||
),
|
||||
bundle = additionalData,
|
||||
finish = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun getPreviousMonthDayLimit(dateInfo: DateInfo): Int {
|
||||
val calendar = Calendar.getInstance()
|
||||
|
||||
val currentDay = dateInfo.day
|
||||
val currentMonth = dateInfo.month
|
||||
val currentYear = dateInfo.year
|
||||
|
||||
// Set calendar to current date
|
||||
calendar.set(currentYear, currentMonth, currentDay)
|
||||
|
||||
// Check if the current day is the last day of the current month
|
||||
val isLastDayOfCurrentMonth = currentDay == calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
|
||||
|
||||
// Calculate previous month and year
|
||||
val previousMonth = if (currentMonth == 0) 11 else currentMonth - 1
|
||||
val previousYear = if (currentMonth == 0) currentYear - 1 else currentYear
|
||||
|
||||
// Set calendar to the first day of the previous month to calculate its maximum days
|
||||
calendar.set(previousYear, previousMonth, 1)
|
||||
val maxDayInPreviousMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
|
||||
|
||||
// Return the appropriate day limit for the previous month:
|
||||
// - If the current day is the last day of the current month,
|
||||
// or if it exceeds the number of days in the previous month (e.g., March 30 vs. Feb 28),
|
||||
// use the last day of the previous month.
|
||||
// - Otherwise, use the same day as in the current month.
|
||||
return if (isLastDayOfCurrentMonth || currentDay > maxDayInPreviousMonth) {
|
||||
maxDayInPreviousMonth
|
||||
} else {
|
||||
currentDay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.moneymanager.entry.model.checkbalance
|
||||
|
||||
import com.navi.moneymanager.common.model.SelectedMonth
|
||||
import com.navi.moneymanager.common.model.SpendComparisonType
|
||||
import com.navi.moneymanager.common.model.SpendTrendColor
|
||||
|
||||
data class MMWeeklyHisaabWidgetData(
|
||||
val monthName: String? = null,
|
||||
val currentMonthSpend: SpendItemData? = null,
|
||||
val spendComparison: SpendComparisonViewState,
|
||||
val previousMonthSpend: SpendItemData? = null,
|
||||
)
|
||||
|
||||
data class SpendItemData(
|
||||
val monthName: String?,
|
||||
val currentDay: Int,
|
||||
val spendAmount: String?,
|
||||
val selectedMonth: SelectedMonth,
|
||||
)
|
||||
|
||||
data class SpendComparisonData(
|
||||
val percentage: Int,
|
||||
val changeType: SpendComparisonType,
|
||||
val currentMonthName: String,
|
||||
val daysLeft: Int,
|
||||
val iconUrl: String,
|
||||
val color: SpendTrendColor,
|
||||
val selectedMonth: SelectedMonth,
|
||||
)
|
||||
|
||||
sealed class SpendComparisonViewState {
|
||||
// Show the view with actual spend comparison data
|
||||
data class Show(val spendComparisonData: SpendComparisonData) : SpendComparisonViewState()
|
||||
|
||||
// Hide the view when:
|
||||
// 1. Either current or previous month spend is 0
|
||||
// 2. User was not loyal last month
|
||||
object Hide : SpendComparisonViewState()
|
||||
}
|
||||
@@ -10,7 +10,9 @@ package com.navi.moneymanager.postonboard.dashboard.repo
|
||||
import androidx.compose.runtime.MutableState
|
||||
import com.navi.base.utils.EMPTY
|
||||
import com.navi.base.utils.orZero
|
||||
import com.navi.common.network.models.isSuccess
|
||||
import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper
|
||||
import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMWeeklyHisaabConfigResponseHelper
|
||||
import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.DashboardDataProvider
|
||||
import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider
|
||||
@@ -24,6 +26,7 @@ import com.navi.moneymanager.common.model.sectionHeader.MMJourneySource
|
||||
import com.navi.moneymanager.common.navigation.utils.MMScreen
|
||||
import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider
|
||||
import com.navi.moneymanager.common.network.model.MMConfigResponse
|
||||
import com.navi.moneymanager.common.network.model.MMWeeklHisaabResponse
|
||||
import com.navi.moneymanager.common.utils.Constants.USER_NAME
|
||||
import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState
|
||||
import com.navi.moneymanager.postonboard.dashboard.model.FeedbackSectionData
|
||||
@@ -46,6 +49,7 @@ constructor(
|
||||
private val database: Lazy<MMDatabase>,
|
||||
private val upiSpendDatabase: Lazy<UpiSpendDatabase>,
|
||||
private val mmConfigResponseHelper: MMConfigResponseHelper,
|
||||
private val mmWeeklyHisaabConfigResponseHelper: MMWeeklyHisaabConfigResponseHelper,
|
||||
@RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider,
|
||||
) {
|
||||
|
||||
@@ -210,4 +214,19 @@ constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchAndSaveWeeklyHisaabConfigResponse(
|
||||
screenName: String,
|
||||
onApiFailed: (Int?, String?) -> Unit,
|
||||
): MMWeeklHisaabResponse? {
|
||||
val networkResponse = remoteDataProvider.fetchMMWeeklyHisaabData(screenName = screenName)
|
||||
val data = networkResponse.data
|
||||
return if (networkResponse.isSuccess() && data != null) {
|
||||
mmWeeklyHisaabConfigResponseHelper.saveWeeklyHisaabConfigToDB(data)
|
||||
mmWeeklyHisaabConfigResponseHelper.getWeeklyHisaabConfig()
|
||||
} else {
|
||||
onApiFailed(networkResponse.statusCode, networkResponse.error?.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,4 +267,18 @@
|
||||
<string name="check_spend_analysis">Check spend analysis</string>
|
||||
<string name="your_spend_analysis_for">Your spend analysis for %1$s</string>
|
||||
<string name="add_contacts_to_sync_contact_names">Add contacts to sync contact names automatically</string>
|
||||
<string name="monthly_overview_title">%1$s overview</string>
|
||||
<string name="spends">Spends</string>
|
||||
<string name="spend_change_percent_label">%1$s %2$s</string>
|
||||
<string name="spend_change_month_comparison"> in %1$s vs last month</string>
|
||||
<string name="budget_remaining_info">\u20B9%1$s left for %2$s in %3$s</string>
|
||||
<string name="spend_date_single">(%1$s %2$d)</string>
|
||||
<string name="spend_date_range">(%1$s 1 - %2$d)</string>
|
||||
<string name="less">less</string>
|
||||
<string name="higher">higher</string>
|
||||
<string name="similar_spends">Similar spends</string>
|
||||
<plurals name="balance_day_left">
|
||||
<item quantity="one">%d day</item>
|
||||
<item quantity="other">%d days</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
@@ -78,11 +78,17 @@ import com.navi.design.font.FontWeightEnum
|
||||
import com.navi.design.font.getFontWeight
|
||||
import com.navi.design.font.naviFontFamily
|
||||
import com.navi.moneymanager.common.analytics.MMDiscoverabilityEventTrackerImpl
|
||||
import com.navi.moneymanager.common.analytics.MMWeeklyHisaabEventTrackerImpl
|
||||
import com.navi.moneymanager.common.model.SelectedMonth
|
||||
import com.navi.moneymanager.common.navigation.utils.MMScreen
|
||||
import com.navi.moneymanager.common.ui.composable.checkbalance.CheckBalanceMMEntryPointWidget
|
||||
import com.navi.moneymanager.common.ui.composable.checkbalance.MMWeeklyHisaabWidget
|
||||
import com.navi.moneymanager.common.utils.Constants.BANK_FIP_ID
|
||||
import com.navi.moneymanager.common.utils.Constants.MASKED_ACCOUNT_NUMBER
|
||||
import com.navi.moneymanager.common.utils.navigateToUpiSpendAnalysisScreen
|
||||
import com.navi.moneymanager.entry.model.checkbalance.CheckBalanceMMEntryPointData
|
||||
import com.navi.moneymanager.entry.model.checkbalance.CheckBalanceMMEntryPointState
|
||||
import com.navi.moneymanager.entry.model.checkbalance.MMWeeklyHisaabWidgetData
|
||||
import com.navi.naviwidgets.extensions.NaviText
|
||||
import com.navi.pay.R
|
||||
import com.navi.pay.analytics.NaviPayAnalytics
|
||||
@@ -172,12 +178,22 @@ fun LinkedAccountBalanceScreen(
|
||||
val showMMDiscoverabilityEntryPoint by
|
||||
linkedAccountBalanceViewModel.showMMDiscoverabilityEntryPoint.collectAsStateWithLifecycle()
|
||||
|
||||
DisposableEffect(lifecycleOwner, showMMDiscoverabilityEntryPoint) {
|
||||
val showMMWeeklyHisaabWidget by
|
||||
linkedAccountBalanceViewModel.showMMWeeklyHisaabWidget.collectAsStateWithLifecycle()
|
||||
|
||||
DisposableEffect(lifecycleOwner, showMMDiscoverabilityEntryPoint, showMMWeeklyHisaabWidget) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME && showMMDiscoverabilityEntryPoint) {
|
||||
linkedAccountBalanceViewModel.mmDiscoverabilityEventTracker
|
||||
.onMMDiscoverabilityEntryPointLoadTriggered()
|
||||
linkedAccountBalanceViewModel.loadMMEntryPoint(linkedAccountEntity)
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
when {
|
||||
showMMDiscoverabilityEntryPoint -> {
|
||||
linkedAccountBalanceViewModel.mmDiscoverabilityEventTracker
|
||||
.onMMDiscoverabilityEntryPointLoadTriggered()
|
||||
linkedAccountBalanceViewModel.loadMMEntryPoint(linkedAccountEntity)
|
||||
}
|
||||
showMMWeeklyHisaabWidget -> {
|
||||
linkedAccountBalanceViewModel.loadMMWeeklyHisaabWidget()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
@@ -202,6 +218,9 @@ fun LinkedAccountBalanceScreen(
|
||||
val mmEntryPointData by
|
||||
linkedAccountBalanceViewModel.mmEntryPointData.collectAsStateWithLifecycle()
|
||||
|
||||
val weeklyHisaabWidgetData by
|
||||
linkedAccountBalanceViewModel.mmWeeklyHisaabWidgetData.collectAsStateWithLifecycle()
|
||||
|
||||
val bottomSheetState =
|
||||
rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
@@ -321,6 +340,8 @@ fun LinkedAccountBalanceScreen(
|
||||
bankAccountId = linkedAccountEntity.accountId,
|
||||
)
|
||||
},
|
||||
weeklyHisaabWidgetData = weeklyHisaabWidgetData,
|
||||
mmWeeklyHisaabEventTrackerImpl = linkedAccountBalanceViewModel.mmWeeklyHisaabEventTracker,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -341,6 +362,8 @@ private fun RenderLinkedAccountBalanceScreen(
|
||||
mmEntryPointData: CheckBalanceMMEntryPointData,
|
||||
mmDiscoverabilityEventTracker: MMDiscoverabilityEventTrackerImpl,
|
||||
redirectOnClick: (CtaData) -> Unit,
|
||||
weeklyHisaabWidgetData: MMWeeklyHisaabWidgetData?,
|
||||
mmWeeklyHisaabEventTrackerImpl: MMWeeklyHisaabEventTrackerImpl,
|
||||
) {
|
||||
|
||||
val context = LocalContext.current as Activity
|
||||
@@ -377,6 +400,36 @@ private fun RenderLinkedAccountBalanceScreen(
|
||||
mmDiscoverabilityEventTracker = mmDiscoverabilityEventTracker,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = weeklyHisaabWidgetData != null,
|
||||
enter =
|
||||
expandVertically(
|
||||
expandFrom = Alignment.Top,
|
||||
animationSpec =
|
||||
tween(500, easing = CubicBezierEasing(0.83f, 0.17f, 0.23f, 0.89f)),
|
||||
),
|
||||
exit =
|
||||
shrinkVertically(shrinkTowards = Alignment.Top, animationSpec = tween(100)),
|
||||
) {
|
||||
weeklyHisaabWidgetData?.let {
|
||||
MMWeeklyHisaabWidget(
|
||||
currentBalance = currentBalance,
|
||||
data = it,
|
||||
hideBalance = hideBalance,
|
||||
isBalanceFetchLoading = showShimmer,
|
||||
getEventTracker = { mmWeeklyHisaabEventTrackerImpl },
|
||||
onSpendAmountClick = { selectedMonth ->
|
||||
navigateToUpiSpendAnalysisScreen(
|
||||
activity = activity,
|
||||
screenName = MMScreen.DASHBOARD.screen,
|
||||
selectedMonth = selectedMonth ?: SelectedMonth(0, 0),
|
||||
bundle = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TransactionEntryPointCard(
|
||||
transactionEntrypointData = transactionEntrypointData,
|
||||
accountType = linkedAccountEntity.accountType,
|
||||
|
||||
@@ -17,16 +17,22 @@ import com.navi.base.utils.orFalse
|
||||
import com.navi.common.di.CoroutineDispatcherProvider
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.MM_DISCOVERABILITY_ENABLED
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.MM_UPI_SYNC_AND_CONFIG_API_TIMEOUT_MS
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.MM_WEEKLY_HISAAB_ENABLED
|
||||
import com.navi.common.network.ApiConstants.API_LIMIT_EXCEEDED
|
||||
import com.navi.common.network.models.RepoResult
|
||||
import com.navi.common.utils.SPACE
|
||||
import com.navi.moneymanager.common.analytics.MMDiscoverabilityEventTrackerImpl
|
||||
import com.navi.moneymanager.common.analytics.MMWeeklyHisaabEventTrackerImpl
|
||||
import com.navi.moneymanager.common.datasync.MMDiscoverabilitySyncPreviousDataUseCase
|
||||
import com.navi.moneymanager.common.datasync.UpiSpendDBSyncExecutor
|
||||
import com.navi.moneymanager.common.datasync.model.DataSyncState
|
||||
import com.navi.moneymanager.common.helper.CheckBalanceMMWidgetHelper
|
||||
import com.navi.moneymanager.common.model.sectionHeader.MMJourneySource
|
||||
import com.navi.moneymanager.common.utils.Constants.JOURNEY_SOURCE
|
||||
import com.navi.moneymanager.entry.model.checkbalance.CheckBalanceMMEntryPointData
|
||||
import com.navi.moneymanager.entry.model.checkbalance.CheckBalanceMMEntryPointState
|
||||
import com.navi.moneymanager.entry.model.checkbalance.MMWeeklyHisaabWidgetData
|
||||
import com.navi.moneymanager.postonboard.dashboard.repo.DashboardScreenRepository
|
||||
import com.navi.pay.R
|
||||
import com.navi.pay.analytics.NaviPayAnalytics
|
||||
@@ -62,16 +68,20 @@ import com.ramcosta.composedestinations.spec.Direction
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
@HiltViewModel
|
||||
class LinkedAccountBalanceViewModel
|
||||
@@ -89,6 +99,7 @@ constructor(
|
||||
private val checkBalanceMMWidgetHelper: CheckBalanceMMWidgetHelper,
|
||||
private val syncPreviousDataUseCase: MMDiscoverabilitySyncPreviousDataUseCase,
|
||||
private val dashboardRepository: DashboardScreenRepository,
|
||||
private val upiSpendDBSyncExecutor: UpiSpendDBSyncExecutor,
|
||||
) : NaviPayBaseVM() {
|
||||
|
||||
private val linkedAccountEntity: LinkedAccountEntity = savedStateHandle["linkedAccountEntity"]!!
|
||||
@@ -120,6 +131,9 @@ constructor(
|
||||
)
|
||||
val mmEntryPointData = _mmEntryPointData.asStateFlow()
|
||||
|
||||
private val _mmWeeklyHisaabWidgetData = MutableStateFlow<MMWeeklyHisaabWidgetData?>(null)
|
||||
val mmWeeklyHisaabWidgetData = _mmWeeklyHisaabWidgetData.asStateFlow()
|
||||
|
||||
private val source: String = savedStateHandle["source"] ?: ""
|
||||
|
||||
private val linkedAccountBalanceScreenSource: LinkedAccountBalanceScreenSource =
|
||||
@@ -178,6 +192,9 @@ constructor(
|
||||
private val _showMMDiscoverabilityEntryPoint = MutableStateFlow(false)
|
||||
val showMMDiscoverabilityEntryPoint = _showMMDiscoverabilityEntryPoint.asStateFlow()
|
||||
|
||||
private val _showMMWeeklyHisaabWidget = MutableStateFlow(false)
|
||||
val showMMWeeklyHisaabWidget = _showMMWeeklyHisaabWidget.asStateFlow()
|
||||
|
||||
val helpCta = getHelpCtaData(screenName = screenName)
|
||||
|
||||
private var timerJob: Job? = null
|
||||
@@ -188,6 +205,12 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
val mmWeeklyHisaabEventTracker by lazy {
|
||||
MMWeeklyHisaabEventTrackerImpl(
|
||||
parameters = mapOf(JOURNEY_SOURCE to MMJourneySource.NAVI_UPI.name)
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
naviPayAnalytics.onCheckBalanceSuccess(source = source)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
@@ -205,42 +228,99 @@ constructor(
|
||||
}
|
||||
fetchTransactionEntrypointConfig(screenName = screenName)
|
||||
runCountDownTimer()
|
||||
checkAndShowMMDiscoverabilityEntryPoint()
|
||||
checkAndShowMMEntryPoint()
|
||||
}
|
||||
|
||||
private fun checkAndShowMMDiscoverabilityEntryPoint() {
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) {
|
||||
val accountType = AccountType.getAccountType(linkedAccountEntity.accountType)
|
||||
val experimentEnabled = isMMDiscoverabilityExperimentEnabled()
|
||||
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityCheckStarted(
|
||||
accountType = accountType.name,
|
||||
experimentEnabled = experimentEnabled,
|
||||
bankCode = linkedAccountEntity.bankCode,
|
||||
)
|
||||
|
||||
if (accountType == AccountType.SAVINGS && experimentEnabled) {
|
||||
val mmConfig = syncPreviousDataUseCase.fetchAndSaveConfigResponse(screenName)
|
||||
if (
|
||||
!checkBalanceMMWidgetHelper.isBankEligible(
|
||||
linkedAccountEntity.bankCode,
|
||||
mmConfig,
|
||||
)
|
||||
) {
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityEntryPointNotEligible(
|
||||
"bank_not_eligible"
|
||||
)
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun checkAndShowMMEntryPoint() {
|
||||
if (upiTxnsyncJob?.isActive == true) {
|
||||
// Skip if a global sync job is already running
|
||||
mmWeeklyHisaabEventTracker.onUpiSyncSkippedAlreadyRunning()
|
||||
return
|
||||
}
|
||||
upiTxnsyncJob =
|
||||
GlobalScope.safeLaunch(Dispatchers.IO) {
|
||||
val isWeeklyHisaabEnabledViaRemoteConfig =
|
||||
FirebaseRemoteConfigHelper.getBoolean(MM_WEEKLY_HISAAB_ENABLED)
|
||||
if (!isWeeklyHisaabEnabledViaRemoteConfig) {
|
||||
mmWeeklyHisaabEventTracker.onMMWeeklyHisaabDisabledViaRemoteConfig()
|
||||
checkMMDiscoverability()
|
||||
return@safeLaunch
|
||||
}
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityEntryPointEligible()
|
||||
_showMMDiscoverabilityEntryPoint.value = true
|
||||
} else {
|
||||
val reason =
|
||||
if (accountType != AccountType.SAVINGS) "non_savings_account"
|
||||
else "experiment_disabled"
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityEntryPointNotEligible(reason)
|
||||
return@safeLaunch
|
||||
val accountType = AccountType.getAccountType(linkedAccountEntity.accountType)
|
||||
withTimeoutOrNull(
|
||||
FirebaseRemoteConfigHelper.getLong(
|
||||
MM_UPI_SYNC_AND_CONFIG_API_TIMEOUT_MS,
|
||||
10000L,
|
||||
)
|
||||
) {
|
||||
val configResponse =
|
||||
dashboardRepository.fetchAndSaveWeeklyHisaabConfigResponse(
|
||||
screenName = screenName,
|
||||
onApiFailed = { statusCode, errorMessage ->
|
||||
mmWeeklyHisaabEventTracker.onMMWeeklyHisaabConfigApiFailed(
|
||||
statusCode = statusCode.toString(),
|
||||
errorMessage = errorMessage.orEmpty(),
|
||||
)
|
||||
},
|
||||
)
|
||||
configResponse?.let { response ->
|
||||
val isHisaabEnabled = response.isHisaabEnabled == true
|
||||
val isPrimaryAccount = linkedAccountEntity.isAccountPrimary
|
||||
val isSavingsAccount = accountType == AccountType.SAVINGS
|
||||
if (isHisaabEnabled && isPrimaryAccount && isSavingsAccount) {
|
||||
mmWeeklyHisaabEventTracker.onMMWeeklyHisaablitmusEnabled()
|
||||
upiSpendDBSyncExecutor.execute(screenName).collectLatest { status ->
|
||||
if (status == DataSyncState.Completed) {
|
||||
_showMMWeeklyHisaabWidget.value = true
|
||||
}
|
||||
}
|
||||
return@withTimeoutOrNull true
|
||||
} else {
|
||||
val reason =
|
||||
when {
|
||||
!isHisaabEnabled -> "litmus_disabled"
|
||||
!isPrimaryAccount -> "not_primary_account"
|
||||
else -> "not_savings_account"
|
||||
}
|
||||
mmWeeklyHisaabEventTracker.onMMWeeklyHisaabLitmusDisabled(reason)
|
||||
checkMMDiscoverability()
|
||||
return@withTimeoutOrNull false
|
||||
}
|
||||
}
|
||||
return@withTimeoutOrNull false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkMMDiscoverability() {
|
||||
val experimentEnabled = isMMDiscoverabilityExperimentEnabled()
|
||||
val accountType = AccountType.getAccountType(linkedAccountEntity.accountType)
|
||||
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityCheckStarted(
|
||||
accountType = accountType.name,
|
||||
experimentEnabled = experimentEnabled,
|
||||
bankCode = linkedAccountEntity.bankCode,
|
||||
)
|
||||
|
||||
if (accountType == AccountType.SAVINGS && experimentEnabled) {
|
||||
val mmConfig = syncPreviousDataUseCase.fetchAndSaveConfigResponse(screenName)
|
||||
if (
|
||||
!checkBalanceMMWidgetHelper.isBankEligible(linkedAccountEntity.bankCode, mmConfig)
|
||||
) {
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityEntryPointNotEligible(
|
||||
"bank_not_eligible"
|
||||
)
|
||||
return
|
||||
}
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityEntryPointEligible()
|
||||
_showMMDiscoverabilityEntryPoint.value = true
|
||||
} else {
|
||||
val reason =
|
||||
if (accountType != AccountType.SAVINGS) "non_savings_account"
|
||||
else "experiment_disabled"
|
||||
mmDiscoverabilityEventTracker.onMMDiscoverabilityEntryPointNotEligible(reason)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +340,15 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMMWeeklyHisaabWidget() {
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) {
|
||||
checkBalanceMMWidgetHelper
|
||||
.getMMWeeklyHisaabWidgetData()
|
||||
.distinctUntilChanged() // emits only when the data actually changes
|
||||
.collect { data -> _mmWeeklyHisaabWidgetData.update { data } }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun isMMDiscoverabilityExperimentEnabled() =
|
||||
FirebaseRemoteConfigHelper.getBoolean(MM_DISCOVERABILITY_ENABLED) &&
|
||||
checkBalanceMMWidgetHelper.isMMDiscoverabilityExperimentEnabled() &&
|
||||
@@ -537,6 +626,10 @@ constructor(
|
||||
|
||||
override val screenName: String
|
||||
get() = NAVI_PAY_CHECK_BALANCE_SCREEN
|
||||
|
||||
companion object {
|
||||
private var upiTxnsyncJob: Job? = null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LinkedAccountBalanceScreenBottomSheetUIState {
|
||||
|
||||
@@ -90,6 +90,7 @@ import com.navi.base.utils.EMPTY
|
||||
import com.navi.common.R as CommonR
|
||||
import com.navi.common.utils.clickableDebounce
|
||||
import com.navi.common.utils.navigateUp
|
||||
import com.navi.common.utils.onClickWithDebounce
|
||||
import com.navi.design.font.FontWeightEnum
|
||||
import com.navi.design.font.getFontWeight
|
||||
import com.navi.design.font.naviFontFamily
|
||||
@@ -1034,7 +1035,10 @@ fun NewMonthView(
|
||||
color = NaviPayColor.textPrimary,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.wrapContentWidth().clickableDebounce { onInfoIconClick?.invoke() },
|
||||
modifier =
|
||||
Modifier.wrapContentWidth().onClickWithDebounce(indication = null) {
|
||||
onInfoIconClick?.invoke()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NaviText(
|
||||
|
||||
@@ -525,10 +525,10 @@ constructor(
|
||||
if (upiSpendAnalysisWidgetHelper.isTotalSyncCompleted()) {
|
||||
upiSpendAnalysisFirstSyncCompleted.update { true }
|
||||
}
|
||||
|
||||
val dataSyncState = upiDBSyncExecutor.execute(screenName)
|
||||
if (dataSyncState is DataSyncState.Completed) {
|
||||
isUpiSpendAnalysisSyncCompleted.update { true }
|
||||
upiDBSyncExecutor.execute(screenName).collect { state ->
|
||||
if (state == DataSyncState.Completed) {
|
||||
isUpiSpendAnalysisSyncCompleted.update { true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user