diff --git a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt index 8ccfb65dbf..492c3e5f94 100644 --- a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt +++ b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt @@ -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" diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayActivity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayActivity.kt index e0d2d9a3f5..423ed88579 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayActivity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayActivity.kt @@ -127,6 +127,7 @@ class NaviPayActivity : BaseActivity() { isErrorSheetCancellable = isCancelable }, customerStatusRoute = customerStatusRoute, + naviPayViewModel = viewModel, ) } else { if (naviPayAccessEligibility.finish) { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayViewModel.kt index 907fcfe721..0816cd202c 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/NaviPayViewModel.kt @@ -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()) + + 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(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( + configKey = DEFAULT_CONFIG, + type = object : TypeToken() {}.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 diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt index 7b9b175b80..73c90eb5c2 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/entry/ui/NaviPayMainScreen.kt @@ -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 diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt index 979a173622..a6f55e58dd 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingScreen.kt @@ -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, ) } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt index c4e2850eeb..2e86d0a7b7 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/NaviPayOnboardingViewModel.kt @@ -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()) 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() init { + updateNaviPayDefaultConfig() updateOnboardingIntentData() updateNaviPayOnboardingConfig() triggerNaviPaySetup() } + private fun updateNaviPayDefaultConfig() { + viewModelScope.launch(Dispatchers.IO) { + naviPayDefaultConfig.update { + naviPayConfigUseCase.execute( + configKey = DEFAULT_CONFIG, + type = object : TypeToken() {}.type, + screenName = screenName, + ) ?: NaviPayDefaultConfig() + } + } + } + private fun triggerNaviPaySetup() { viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { val phoneStatePermission =