NTP-59414 | Force update app for navi pay flow (#16055)

This commit is contained in:
Aditya Narayan Malik
2025-05-09 00:20:30 +05:30
committed by GitHub
parent 048933dced
commit b2dc18faf0
6 changed files with 169 additions and 18 deletions

View File

@@ -85,6 +85,8 @@ object FirebaseRemoteConfigHelper {
// NAVI_PAY
const val NAVI_PAY_MINIMUM_APP_VERSION_ALLOWED_FOR_ONBOARDING =
"NAVI_PAY_MINIMUM_APP_VERSION_ALLOWED_FOR_ONBOARDING"
const val NAVI_PAY_MINIMUM_APP_VERSION_ALLOWED_FOR_ACCESS =
"NAVI_PAY_MINIMUM_APP_VERSION_ALLOWED_FOR_ACCESS"
const val ENABLE_NAVI_PAY_KILL_SWITCH_MECHANISM = "ENABLE_NAVI_PAY_KILL_SWITCH_MECHANISM"
const val NAVI_PAY_ENABLE_CRED_BLOCK_LOGS = "NAVI_PAY_ENABLE_CRED_BLOCK_LOGS"
const val NAVI_PAY_ENABLE_VALIDATE_VPA_CACHING = "NAVI_PAY_ENABLE_VALIDATE_VPA_CACHING"

View File

@@ -127,6 +127,7 @@ class NaviPayActivity : BaseActivity() {
isErrorSheetCancellable = isCancelable
},
customerStatusRoute = customerStatusRoute,
naviPayViewModel = viewModel,
)
} else {
if (naviPayAccessEligibility.finish) {

View File

@@ -14,24 +14,31 @@ import androidx.lifecycle.viewModelScope
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.QuerySnapshot
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.base.utils.BaseUtils.getPhoneNumberWithNinetyOneCode
import com.navi.base.utils.FirestoreDataProvider
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.model.AppUpgradeResponse
import com.navi.common.payments.arc.usecase.ArcNudgeSyncUseCase
import com.navi.common.upi.WITHOUT_ONBOARDING_FLOW
import com.navi.common.usecase.SyncLitmusExperimentUseCase
import com.navi.common.utils.EMPTY
import com.navi.common.utils.NaviApiPoller
import com.navi.pay.BuildConfig
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_ACTIVITY
import com.navi.pay.common.batman.DarkKnightScheduler
import com.navi.pay.common.model.config.NaviPayDefaultConfig
import com.navi.pay.common.model.view.NaviPayScreenType
import com.navi.pay.common.model.view.PspType
import com.navi.pay.common.setup.NaviPayCustomerStatusHandler
import com.navi.pay.common.setup.NaviPayManager
import com.navi.pay.common.setup.NaviPayRouter
import com.navi.pay.common.usecase.BankUptimePollerUseCase
import com.navi.pay.common.usecase.LiteAccountSyncUseCase
import com.navi.pay.common.usecase.NaviPayConfigUseCase
import com.navi.pay.common.usecase.RefreshBankListUseCase
import com.navi.pay.common.usecase.RefreshConfigUseCase
import com.navi.pay.common.usecase.RefreshLinkedAccountsUseCase
@@ -45,6 +52,7 @@ import com.navi.pay.management.chat.util.SyncMessagesUseCase
import com.navi.pay.management.upinumber.list.model.view.toUpiNumberEntity
import com.navi.pay.management.upinumber.list.repository.UpiNumberRepository
import com.navi.pay.network.di.NaviPayGsonBuilder
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.FIRESTORE_CUSTOMER_DATA_COLLECTION_PATH
import com.navi.pay.utils.FIRESTORE_PSP_ROUTING_BUCKETS_COLLECTION_PATH
import com.navi.pay.utils.NAVI_PAY_LITMUS_EXPERIMENTS
@@ -54,6 +62,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@HiltViewModel
@@ -77,6 +94,7 @@ constructor(
private val darkKnightScheduler: DarkKnightScheduler,
private val syncConversationsUseCase: SyncConversationsUseCase,
private val syncMessagesUseCase: SyncMessagesUseCase,
private val naviPayConfigUseCase: NaviPayConfigUseCase,
) : NaviPayBaseVM() {
private val naviPayAnalytics: NaviPayAnalytics.NaviPayViewModel =
@@ -90,10 +108,51 @@ constructor(
NaviApiPoller(repeatInterval = 5.seconds, numberOfIterations = 1)
}
private val naviPayDefaultConfig =
MutableStateFlow<NaviPayDefaultConfig>(NaviPayDefaultConfig())
val appUpgradeData =
naviPayDefaultConfig
.map { naviPayDefaultConfig ->
AppUpgradeResponse(
softUpgrade = true,
hardUpgrade = false,
expectedAppVersionCode = 0,
downloadableUrl = EMPTY,
verificationHash = EMPTY,
updateType = EMPTY,
title = naviPayDefaultConfig.config.appUpgradeTitle,
description = naviPayDefaultConfig.config.appUpgradeDescription,
)
}
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue =
AppUpgradeResponse(
softUpgrade = true,
hardUpgrade = false,
expectedAppVersionCode = 0,
downloadableUrl = EMPTY,
verificationHash = EMPTY,
updateType = EMPTY,
title = naviPayDefaultConfig.value.config.appUpgradeTitle,
description = naviPayDefaultConfig.value.config.appUpgradeDescription,
),
)
private val _isHardUpdateRequired = MutableSharedFlow<Boolean>(replay = 1)
val isHardUpdateRequired = _isHardUpdateRequired.asSharedFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
naviPayAnalytics.onNaviPayViewModelInit()
updateNaviPayDefaultConfig()
launch { performMinimumAllowedVersionForNaviPayAccessCheck() }
launch { refreshBankListUseCase.execute(screenName = screenName) }
launch { refreshConfigUseCase.execute(screenName = screenName) }
@@ -139,6 +198,22 @@ constructor(
}
}
private suspend fun updateNaviPayDefaultConfig() {
naviPayDefaultConfig.update {
naviPayConfigUseCase.execute<NaviPayDefaultConfig>(
configKey = DEFAULT_CONFIG,
type = object : TypeToken<NaviPayDefaultConfig>() {}.type,
screenName = screenName,
) ?: NaviPayDefaultConfig()
}
}
private suspend fun performMinimumAllowedVersionForNaviPayAccessCheck() {
if (isCurrentAppVersionLowerThanMinimumAllowedVersionForUpiUsage()) {
_isHardUpdateRequired.emit(value = true)
}
}
private fun initUpiLiteSync() {
viewModelScope.launch(Dispatchers.IO) {
// Onboarding check is not required here
@@ -146,6 +221,21 @@ constructor(
}
}
private fun isCurrentAppVersionLowerThanMinimumAllowedVersionForUpiUsage(): Boolean {
val currentAppVersion = NaviPayManager.appVersionCode.toIntOrNull() ?: Int.MAX_VALUE
val minAppVersionAllowedForOnboarding =
FirebaseRemoteConfigHelper.getLong(
key = FirebaseRemoteConfigHelper.NAVI_PAY_MINIMUM_APP_VERSION_ALLOWED_FOR_ACCESS,
defaultValue = 0L,
)
return currentAppVersion < minAppVersionAllowedForOnboarding
}
@OptIn(ExperimentalCoroutinesApi::class)
fun clearReplayCacheForHardUpdateParam() {
_isHardUpdateRequired.resetReplayCache()
}
/**
* This method is used to get the route from the url This is needed when clicked from options in
* SA home page, then get this route & override the default route of NavGraph to have faster

View File

@@ -7,6 +7,7 @@
package com.navi.pay.entry.ui
import android.os.Bundle
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
@@ -17,13 +18,18 @@ import androidx.compose.material.navigation.ModalBottomSheetLayout
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.rememberNavController
import androidx.navigation.plusAssign
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.deeplink.util.DeeplinkConstants
import com.navi.base.model.CtaData
import com.navi.base.utils.orFalse
import com.navi.common.constants.APP_UPGRADE_DATA
import com.navi.pay.NavGraphs
import com.navi.pay.common.model.view.ErrorVisibilityEvent
import com.navi.pay.common.model.view.NaviPayErrorButtonConfig
@@ -32,6 +38,7 @@ import com.navi.pay.common.ui.NaviPayModalBottomSheet
import com.navi.pay.common.ui.rememberBottomSheetNavigator
import com.navi.pay.common.utils.ErrorEventHandler
import com.navi.pay.entry.NaviPayActivity
import com.navi.pay.entry.NaviPayViewModel
import com.navi.pay.utils.ANIMATION_SPEC_DURATION_IN_MILLIS
import com.navi.pay.utils.GenericErrorCtaHandler
import com.ramcosta.composedestinations.DestinationsNavHost
@@ -55,6 +62,7 @@ fun NaviPayMainScreen(
genericErrorCtaHandler: GenericErrorCtaHandler,
onErrorVisibilityChange: (Boolean, Boolean) -> Unit,
customerStatusRoute: Route?,
naviPayViewModel: NaviPayViewModel,
) {
val errorEvent by ErrorEventHandler.errorEvent.collectAsStateWithLifecycle(initialValue = null)
val errorVisibilityEvent by
@@ -69,6 +77,23 @@ fun NaviPayMainScreen(
)
val scope = rememberCoroutineScope()
val appUpgradeData by naviPayViewModel.appUpgradeData.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
naviPayViewModel.isHardUpdateRequired.collect {
naviPayViewModel.clearReplayCacheForHardUpdateParam()
if (it) {
DeepLinkManager.getDeepLinkListener()
?.navigateTo(
activity = naviPayActivity,
ctaData = CtaData(url = DeeplinkConstants.APP_UPDATE),
bundle = Bundle().apply { putParcelable(APP_UPGRADE_DATA, appUpgradeData) },
finish = true,
)
}
}
}
val dismissErrorSheet: () -> Unit = {
onErrorVisibilityChange(false, confirmStateChange)
scope

View File

@@ -99,6 +99,7 @@ fun NaviPayOnboardingScreen(
}
val onboardingSource by
naviPayOnboardingViewModel.onboardingSource.collectAsStateWithLifecycle()
val appUpgradeData by naviPayOnboardingViewModel.appUpgradeData.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
naviPayAnalytics.onNaviPaySetupLanded(
@@ -119,13 +120,7 @@ fun NaviPayOnboardingScreen(
?.navigateTo(
activity = naviPayOnboardingActivity,
ctaData = CtaData(url = DeeplinkConstants.APP_UPDATE),
bundle =
Bundle().apply {
putParcelable(
APP_UPGRADE_DATA,
naviPayOnboardingViewModel.appUpgradeData,
)
},
bundle = Bundle().apply { putParcelable(APP_UPGRADE_DATA, appUpgradeData) },
finish = true,
)
}

View File

@@ -107,6 +107,7 @@ import com.navi.pay.permission.utils.PermissionKeys.NON_FIRST_TIME_SCREEN_PERMIS
import com.navi.pay.permission.utils.PermissionUtils
import com.navi.pay.tstore.list.usecase.SyncOrderHistoryUseCase
import com.navi.pay.utils.ALLOW
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.DENY
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITHOUT_PLUS
import com.navi.pay.utils.KEY_IS_FIRST_TRANSACTION_SUCCESSFUL
@@ -144,10 +145,14 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
@@ -294,19 +299,39 @@ constructor(
totalPollingDurationInMillis = MAX_POLLING_DURATION_IN_MILLIS,
)
}
private val naviPayDefaultConfig = NaviPayDefaultConfig()
private val naviPayDefaultConfig =
MutableStateFlow<NaviPayDefaultConfig>(NaviPayDefaultConfig())
val appUpgradeData =
AppUpgradeResponse(
softUpgrade = true,
hardUpgrade = false,
expectedAppVersionCode = 0,
downloadableUrl = EMPTY,
verificationHash = EMPTY,
updateType = EMPTY,
title = naviPayDefaultConfig.config.appUpgradeTitle,
description = naviPayDefaultConfig.config.appUpgradeDescription,
)
naviPayDefaultConfig
.map { naviPayDefaultConfig ->
AppUpgradeResponse(
softUpgrade = true,
hardUpgrade = false,
expectedAppVersionCode = 0,
downloadableUrl = EMPTY,
verificationHash = EMPTY,
updateType = EMPTY,
title = naviPayDefaultConfig.config.appUpgradeTitle,
description = naviPayDefaultConfig.config.appUpgradeDescription,
)
}
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue =
AppUpgradeResponse(
softUpgrade = true,
hardUpgrade = false,
expectedAppVersionCode = 0,
downloadableUrl = EMPTY,
verificationHash = EMPTY,
updateType = EMPTY,
title = naviPayDefaultConfig.value.config.appUpgradeTitle,
description = naviPayDefaultConfig.value.config.appUpgradeDescription,
),
)
private companion object {
private val POLLING_INTERVAL = 2.5.seconds
@@ -327,11 +352,24 @@ constructor(
private val locationPermissionRequestResult = Channel<Unit>()
init {
updateNaviPayDefaultConfig()
updateOnboardingIntentData()
updateNaviPayOnboardingConfig()
triggerNaviPaySetup()
}
private fun updateNaviPayDefaultConfig() {
viewModelScope.launch(Dispatchers.IO) {
naviPayDefaultConfig.update {
naviPayConfigUseCase.execute<NaviPayDefaultConfig>(
configKey = DEFAULT_CONFIG,
type = object : TypeToken<NaviPayDefaultConfig>() {}.type,
screenName = screenName,
) ?: NaviPayDefaultConfig()
}
}
}
private fun triggerNaviPaySetup() {
viewModelScope.safeLaunch(coroutineDispatcherProvider.io) {
val phoneStatePermission =