From 61f2e1b62cc7ccb115a90ee7a7ab3cb5bb63493b Mon Sep 17 00:00:00 2001 From: Shaurya Rehan Date: Fri, 2 May 2025 16:51:13 +0530 Subject: [PATCH] NTP-60715 | auto open contact permission pop-up (#16011) --- .../viewmodel/PayToContactsViewModelTest.kt | 7 +- .../common/model/view/NaviPermissionResult.kt | 2 + .../scanpay/ui/QrScannerScreen.kt | 2 + .../paytocontacts/ui/PayToContactsScreen.kt | 39 +++++++-- .../viewmodel/PayToContactsViewModel.kt | 80 +++++++++++++++---- .../binding/ui/NaviPayOnboardingScreen.kt | 2 + .../com/navi/pay/utils/NaviPayConstants.kt | 4 +- .../nativepayment/viewmodel/ScanCardVM.kt | 1 + 8 files changed, 110 insertions(+), 27 deletions(-) diff --git a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModelTest.kt b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModelTest.kt index 46c75cab69..89a25a251f 100644 --- a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModelTest.kt +++ b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModelTest.kt @@ -11,6 +11,7 @@ import androidx.test.core.app.ApplicationProvider import app.cash.turbine.test import com.navi.base.sharedpref.PreferenceManager import com.navi.base.utils.ResourceProvider +import com.navi.common.usecase.LitmusExperimentsUseCase import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.analytics.NaviPayToContacts import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity @@ -18,7 +19,6 @@ import com.navi.pay.common.model.config.NaviPayDefaultConfig import com.navi.pay.common.model.view.NaviPaySessionHelper import com.navi.pay.common.usecase.NaviPayConfigUseCase import com.navi.pay.common.usecase.ValidateVpaUseCase -import com.navi.pay.common.utils.DeviceInfoProvider import com.navi.pay.common.utils.FrequentOrdersHelper import com.navi.pay.entry.NaviPayActivityDataProvider import com.navi.pay.management.paytocontacts.PhoneContactManager @@ -39,7 +39,6 @@ import org.junit.Test @HiltAndroidTest class PayToContactsViewModelTest : NaviPayAndroidTest() { - private var deviceInfoProvider: DeviceInfoProvider = mockk() private var contactManager: PhoneContactManager = mockk() private var naviPayNetworkConnectivity: NaviPayNetworkConnectivity = mockk() private var naviPayConfigUseCase: NaviPayConfigUseCase = mockk() @@ -48,6 +47,7 @@ class PayToContactsViewModelTest : NaviPayAndroidTest() { private var resourceProvider: ResourceProvider = mockk() private var validateVpaUseCase: ValidateVpaUseCase = mockk() private var naviPayActivityDataProvider: NaviPayActivityDataProvider = mockk() + private var litmusExperimentsUseCase: LitmusExperimentsUseCase = mockk() private val naviPayAnalytics: NaviPayToContacts = mockk(relaxed = true) private lateinit var viewModel: PayToContactsViewModel @@ -68,10 +68,10 @@ class PayToContactsViewModelTest : NaviPayAndroidTest() { coEvery { resourceProvider.getString(any()) } returns "" coEvery { frequentOrdersHelper.getFrequentOrderList(any()) } returns emptyList() coEvery { naviPaySessionHelper.getNaviPaySessionAttributes() } returns emptyMap() + coEvery { litmusExperimentsUseCase.execute(experimentName = any()) } returns null viewModel = PayToContactsViewModel( - deviceInfoProvider = deviceInfoProvider, contactManager = contactManager, naviPayNetworkConnectivity = naviPayNetworkConnectivity, naviPayConfigUseCase = naviPayConfigUseCase, @@ -81,6 +81,7 @@ class PayToContactsViewModelTest : NaviPayAndroidTest() { validateVpaUseCase = validateVpaUseCase, naviPayActivityDataProvider = naviPayActivityDataProvider, naviPayAnalytics = naviPayAnalytics, + litmusExperimentsUseCase = litmusExperimentsUseCase, ) } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPermissionResult.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPermissionResult.kt index 6bb9996d4d..dd656e7f4e 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPermissionResult.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/NaviPermissionResult.kt @@ -23,6 +23,8 @@ sealed interface NaviPermissionResult { // Here user has denied permission so app can show a UI before asking for permission again. data object ShowRationale : NaviPermissionResult + + data object None : NaviPermissionResult } fun handlePermissionResult( diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt index 90e2339612..d9bc6f1128 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/moneytransfer/scanpay/ui/QrScannerScreen.kt @@ -329,6 +329,8 @@ fun QrScannerScreenContent( naviPaySessionAttributes = qrScannerViewModel.getNaviPaySessionAttributes(), ) } + + else -> {} } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt index e89ad7c9a5..d7646a9346 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/ui/PayToContactsScreen.kt @@ -145,6 +145,11 @@ fun PayToContactsScreen( payToContactsViewModel.frequentOrdersHeading.collectAsStateWithLifecycle() val shouldShowYourContactsTitle by payToContactsViewModel.shouldShowYourContactsTitle.collectAsStateWithLifecycle() + val isAutoOpenContactPermissionExperimentEnabled by + payToContactsViewModel.isAutoOpenContactPermissionExperimentEnabled + .collectAsStateWithLifecycle() + val showPermissionWidget by + payToContactsViewModel.showPermissionWidget.collectAsStateWithLifecycle() val onBackClick = { payToContactsViewModel.cancelPaymentRequest() @@ -226,6 +231,7 @@ fun PayToContactsScreen( naviPaySessionAttributes = payToContactsViewModel.getNaviPaySessionAttributes() ) + payToContactsViewModel.updatePermissionResult(permissionResult = it) } NaviPermissionResult.HardDenied -> { naviPayAnalytics.onPayToContactsPermissionDenied( @@ -233,11 +239,14 @@ fun PayToContactsScreen( naviPaySessionAttributes = payToContactsViewModel.getNaviPaySessionAttributes(), ) - naviPayActivity.launchPermissionSettingsScreen() - naviPayAnalytics.onAppSettingsScreenLaunched( - naviPaySessionAttributes = - payToContactsViewModel.getNaviPaySessionAttributes() - ) + payToContactsViewModel.updatePermissionResult(permissionResult = it) + if (payToContactsViewModel.isPermissionLaunchedFromAllowClick) { + naviPayActivity.launchPermissionSettingsScreen() + naviPayAnalytics.onAppSettingsScreenLaunched( + naviPaySessionAttributes = + payToContactsViewModel.getNaviPaySessionAttributes() + ) + } } NaviPermissionResult.ShowRationale -> { naviPayAnalytics.onPayToContactsPermissionDenied( @@ -245,11 +254,14 @@ fun PayToContactsScreen( naviPaySessionAttributes = payToContactsViewModel.getNaviPaySessionAttributes(), ) + payToContactsViewModel.updatePermissionResult(permissionResult = it) } + else -> {} } } val onAllowPermissionButtonClicked = { + payToContactsViewModel.isPermissionLaunchedFromAllowClick = true readContactsPermissionsState.launchMultiplePermissionRequest() } @@ -294,7 +306,7 @@ fun PayToContactsScreen( LaunchedEffect(readContactsPermissionsState.allPermissionsGranted) { if (readContactsPermissionsState.allPermissionsGranted) { - payToContactsViewModel.updateContactPermissionStatus(true) + payToContactsViewModel.updateContactPermissionStatus(isContactPermissionGranted = true) payToContactsViewModel.fetchContacts() } else { payToContactsViewModel.updateContactPermissionStatus(false) @@ -304,6 +316,15 @@ fun PayToContactsScreen( isContactListEmpty = contactList.isEmpty(), isFrequentOrderListEmpty = filteredFrequentOrdersList.isEmpty(), ) + + if ( + isAutoOpenContactPermissionExperimentEnabled && + !payToContactsViewModel.isPermissionPopupSeenOnLanded + ) { + payToContactsViewModel.onPermissionPopupSeenOnLanded() + payToContactsViewModel.isPermissionLaunchedFromAllowClick = false + readContactsPermissionsState.launchMultiplePermissionRequest() + } } } @@ -354,6 +375,7 @@ fun PayToContactsScreen( focusManager.clearFocus() payToContactsViewModel.onHelpCtaClicked() }, + showPermissionWidget = showPermissionWidget, ) } } @@ -383,6 +405,7 @@ fun RenderPayToContactsScreen( invalidState: WarningErrorInfoState, isEmptyState: Boolean, onHelpCtaClicked: () -> Unit, + showPermissionWidget: Boolean, ) { val scope = rememberCoroutineScope() val context = LocalContext.current @@ -506,6 +529,7 @@ fun RenderPayToContactsScreen( newContact = newContact, shouldShowYourContactsTitle = shouldShowYourContactsTitle, scrollState = scrollState, + showPermissionWidget = showPermissionWidget, ) } }, @@ -552,6 +576,7 @@ private fun PayToContactScreenScaffoldContent( newContact: PhoneContactEntity?, shouldShowYourContactsTitle: Boolean, scrollState: LazyListState, + showPermissionWidget: Boolean, ) { LazyColumn( modifier = modifier.fillMaxHeight(), @@ -627,7 +652,7 @@ private fun PayToContactScreenScaffoldContent( ) } } - } else { + } else if (showPermissionWidget) { item { PayToContactsPermissionView(onPrimaryButtonClicked = onPrimaryButtonClicked) } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt index 9ae5a9b756..b2dce05578 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/management/paytocontacts/viewmodel/PayToContactsViewModel.kt @@ -13,10 +13,10 @@ import com.navi.base.model.CtaData import com.navi.base.utils.ResourceProvider import com.navi.base.utils.isNotNullAndNotEmpty import com.navi.common.constants.EMPTY -import com.navi.common.extensions.or import com.navi.common.extensions.removeSpaces import com.navi.common.network.models.RepoResult import com.navi.common.network.models.isSuccessWithData +import com.navi.common.usecase.LitmusExperimentsUseCase import com.navi.pay.R import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_SEND_MONEY_TO_CONTACTS_SCREEN_V2 import com.navi.pay.analytics.NaviPayToContacts @@ -25,9 +25,9 @@ import com.navi.pay.common.model.config.NaviPayDefaultConfig import com.navi.pay.common.model.network.ValidateVpaRequest import com.navi.pay.common.model.network.ValidateVpaResponse import com.navi.pay.common.model.view.NaviPaySessionHelper +import com.navi.pay.common.model.view.NaviPermissionResult import com.navi.pay.common.usecase.NaviPayConfigUseCase import com.navi.pay.common.usecase.ValidateVpaUseCase -import com.navi.pay.common.utils.DeviceInfoProvider import com.navi.pay.common.utils.FrequentOrdersHelper import com.navi.pay.common.utils.NaviPayCommonUtils.getHelpCtaData import com.navi.pay.common.utils.NaviPayCommonUtils.getNormalisedPhoneNumber @@ -47,9 +47,11 @@ import com.navi.pay.tstore.list.model.view.FrequentOrderEntity import com.navi.pay.utils.DEFAULT_CONFIG import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS import com.navi.pay.utils.INVALID_VPA +import com.navi.pay.utils.LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION import com.navi.pay.utils.NAVI_PAY_SEARCH_QUERY_API_DELAY import com.navi.pay.utils.NAVI_PAY_WIDGET_CLICKED_KEY import com.navi.pay.utils.NOT_LINKED_TO_UPI +import com.navi.pay.utils.NaviPayExperimentVariantType import com.navi.pay.utils.PHONE_NUMBER_LENGTH import com.navi.pay.utils.REMOVE_WHITESPACE_REGEX import com.navi.pay.utils.isValidPhoneNumberLength @@ -78,7 +80,6 @@ import kotlinx.coroutines.launch class PayToContactsViewModel @Inject constructor( - private val deviceInfoProvider: DeviceInfoProvider, private val contactManager: PhoneContactManager, private val naviPayNetworkConnectivity: NaviPayNetworkConnectivity, private val naviPayConfigUseCase: NaviPayConfigUseCase, @@ -88,6 +89,7 @@ constructor( private val validateVpaUseCase: ValidateVpaUseCase, private val naviPayActivityDataProvider: NaviPayActivityDataProvider, private val naviPayAnalytics: NaviPayToContacts, + private val litmusExperimentsUseCase: LitmusExperimentsUseCase, ) : NaviPayBaseVM() { private var naviPayDefaultConfig = NaviPayDefaultConfig() @@ -134,6 +136,28 @@ constructor( private val _isNewContactVisible = MutableStateFlow(false) val isNewContactVisible = _isNewContactVisible.asStateFlow() + private val _permissionResult = + MutableStateFlow(NaviPermissionResult.None) + val permissionResult = _permissionResult.asStateFlow() + + private val _isAutoOpenContactPermissionExperimentEnabled = MutableStateFlow(false) + val isAutoOpenContactPermissionExperimentEnabled = + _isAutoOpenContactPermissionExperimentEnabled.asStateFlow() + + val showPermissionWidget = + combine(permissionResult, isAutoOpenContactPermissionExperimentEnabled) { + permissionResult, + isAutoOpenContactPermissionExperimentEnabled -> + permissionResult != NaviPermissionResult.None || + !isAutoOpenContactPermissionExperimentEnabled + } + .flowOn(Dispatchers.IO) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + private val _navigateToNextScreenFromHelpCta = MutableSharedFlow() val navigateToNextScreenFromHelpCta = _navigateToNextScreenFromHelpCta.asSharedFlow() @@ -165,6 +189,10 @@ constructor( private var lastSearchQuery = "" + var isPermissionPopupSeenOnLanded = false + + var isPermissionLaunchedFromAllowClick = false + private var isContactPermissionGranted = false private val helpCtaData = getHelpCtaData(screenName = screenName) @@ -243,31 +271,43 @@ constructor( init { viewModelScope.launch(context = Dispatchers.IO) { - updateNaviPaySessionId() - triggerEventForShortcutWidget() - updateNaviPayDefaultConfig() + initializeSynchronously() initializeAsynchronously() } } + private suspend fun initializeSynchronously() { + updateNaviPaySessionId() + updateNaviPayDefaultConfig() + setLitmusExperimentValues() + } + private suspend fun initializeAsynchronously() { coroutineScope { + launch { triggerEventForShortcutWidget() } launch { fetchFrequentOrders() } launch { observeAndHandleSearchResults() } } } + private suspend fun setLitmusExperimentValues() { + _isAutoOpenContactPermissionExperimentEnabled.update { + val autoOpenContactPermissionExperimentVariant = + litmusExperimentsUseCase + .execute(experimentName = LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION) + ?.variant + autoOpenContactPermissionExperimentVariant?.name == + NaviPayExperimentVariantType.TEST.value && + autoOpenContactPermissionExperimentVariant.enabled + } + } + private fun triggerEventForShortcutWidget() { - viewModelScope.launch(Dispatchers.IO) { - naviPayActivityDataProvider - .getIntentData() - ?.getString(NAVI_PAY_WIDGET_CLICKED_KEY) - ?.let { - naviPayAnalytics.onSendToContactShortcutClicked( - eventName = it, - naviPaySessionAttributes = getNaviPaySessionAttributes(), - ) - } + naviPayActivityDataProvider.getIntentData()?.getString(NAVI_PAY_WIDGET_CLICKED_KEY)?.let { + naviPayAnalytics.onSendToContactShortcutClicked( + eventName = it, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) } } @@ -366,6 +406,14 @@ constructor( updateShimmerState(showShimmer = false) } + fun onPermissionPopupSeenOnLanded() { + isPermissionPopupSeenOnLanded = true + } + + fun updatePermissionResult(permissionResult: NaviPermissionResult) { + _permissionResult.update { permissionResult } + } + private fun updateIsWarningOrErrorState( isError: Boolean? = null, isWarning: Boolean? = null, 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 5542085cd0..979a173622 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 @@ -312,6 +312,7 @@ fun NaviPayOnboardingScreen( ) ) } + else -> {} } } @@ -389,6 +390,7 @@ fun NaviPayOnboardingScreen( ) ) } + else -> {} } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt index 3bbf7805ed..0e02dc8ae8 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/NaviPayConstants.kt @@ -172,7 +172,6 @@ const val NAVI_PAY_SYNC_TABLE_CUSTOMER_ONBOARDING_DATA_KEY = "customerOnboarding // Litmus experiments const val LITMUS_EXPERIMENT_NAVIPAY_LITE_DEFAULT_ENTERED_AMOUNT = "NaviPay-lite-default-entered-amount" -// Transaction ledger Experiment const val LITMUS_EXPERIMENT_NAVIPAY_TRANSACTION_LEDGER = "NaviPay-exp-txn-ledger" const val LITMUS_EXPERIMENT_NAVIPAY_ORDER_TAG_SUMMARY = "NaviPay-order-tag-summary" const val LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING = "NaviPay-exp-smv-binding" @@ -184,6 +183,8 @@ const val LITMUS_EXPERIMENT_NAVIPAY_UPI_LITE_USP = "NaviPay-exp-lite-usp-exp" const val LITMUS_EXPERIMENT_NAVIPAY_HIGHLIGHT_NEW_PAYEE = "NaviPay-exp-highlight-new-payee" const val LITMUS_EXPERIMENT_ORDER_HISTORY_ERROR_WIDGET = "NaviTStore-exp-tds-error-widget" const val LITMUS_EXPERIMENT_NAVIPAY_PAYMENT_RETRY_EXPERIENCE = "NaviPay-payment-retry-experience" +const val LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION = "NaviPay-auto-open-contact-permission" + val NAVI_PAY_LITMUS_EXPERIMENTS = listOf( LITMUS_EXPERIMENT_NAVIPAY_TRANSACTION_LEDGER, @@ -200,6 +201,7 @@ val NAVI_PAY_LITMUS_EXPERIMENTS = LITMUS_EXPERIMENT_NAVI_IPL_POWERPLAY, LITMUS_EXPERIMENT_NAVIPAY_PAYMENT_RETRY_EXPERIENCE, LITMUS_EXPERIMENT_ORDER_HISTORY_ERROR_WIDGET, + LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION, ) // Generic diff --git a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/viewmodel/ScanCardVM.kt b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/viewmodel/ScanCardVM.kt index 1ae4aeb9e3..f490a63352 100644 --- a/android/navi-payment/src/main/java/com/navi/payment/nativepayment/viewmodel/ScanCardVM.kt +++ b/android/navi-payment/src/main/java/com/navi/payment/nativepayment/viewmodel/ScanCardVM.kt @@ -307,6 +307,7 @@ constructor( is NaviPermissionResult.ShowRationale -> { scanCardAnalytics.onCameraPermissionDenied(showRationale = true) } + else -> {} } }