diff --git a/android/.gitignore b/android/.gitignore index 06a6984382..bc1b597abf 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -21,4 +21,4 @@ vcs.xml visit-sdk/build # Local build cache build-cache -api-credentials.json +api-credentials.json \ No newline at end of file diff --git a/android/app/src/main/java/com/naviapp/registration/RegistrationActivity.kt b/android/app/src/main/java/com/naviapp/registration/RegistrationActivity.kt index f102f90565..62c2d53524 100644 --- a/android/app/src/main/java/com/naviapp/registration/RegistrationActivity.kt +++ b/android/app/src/main/java/com/naviapp/registration/RegistrationActivity.kt @@ -226,7 +226,7 @@ class RegistrationActivity : handleConsentIntent: (Intent) -> Unit = {} ): OtpReceiveListener { return object : OtpReceiveListener { - override fun onOtpReceive(otp: String) { + override fun onOtpReceive(otp: String, smsOriginatingAddress: String?) { registrationSharedVM.setOtpAutoParsed(otp) } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index dd6590cb38..cf0f355302 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -4,7 +4,7 @@ accompanist-permissions = "0.37.0" accompanist-systemuicontroller = "0.17.0" android-flexbox = "3.0.0" android-gms-playServicesAds = "23.6.0" -android-gms-playServicesAuthApiPhone = "18.1.0" +android-gms-playServicesAuthApiPhone = "18.2.0" android-gms-playServicesLocation = "21.3.0" android-gms-playServicesMaps = "17.0.0" android-gms-playServicesMlkitTextRecognition = "19.0.1" @@ -14,6 +14,7 @@ android-material = "1.9.0" android-places = "4.1.0" android-play-appUpdateKtx = "2.1.0" android-play-featureDeliveryKtx = "2.1.0" +android-play-integrity = "1.4.0" android-play-reviewKtx = "2.0.2" android-r8 = "8.9.35" androidGradlePlugin = "8.8.0" @@ -145,6 +146,7 @@ android-places = { module = "com.google.android.libraries.places:places", versio android-play-appUpdateKtx = { module = "com.google.android.play:app-update-ktx", version.ref = "android-play-appUpdateKtx" } android-play-featureDeliveryKtx = { module = "com.google.android.play:feature-delivery-ktx", version.ref = "android-play-featureDeliveryKtx" } +android-play-integrity = { module = "com.google.android.play:integrity", version.ref = "android-play-integrity" } android-play-reviewKtx = { module = "com.google.android.play:review-ktx", version.ref = "android-play-reviewKtx" } android-r8 = { module = "com.android.tools:r8", version.ref = "android-r8" } diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt index 129fbfd98a..89f92226ea 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/common/fragment/OtpFragment.kt @@ -199,7 +199,7 @@ class OtpFragment : AmcBaseFragment(), View.OnClickListener { activity?.let { SmsRetriever.getClient(it).startSmsRetriever() } otpReceiver.setListener( object : OtpReceiveListener { - override fun onOtpReceive(otp: String) { + override fun onOtpReceive(otp: String, smsOriginatingAddress: String?) { if (otp.length == SUPPORTED_OTP_SIZE) { binding.otpLayout.setOtp(otp) if (autoReadOtpDisabled().not()) { diff --git a/android/navi-common/build.gradle b/android/navi-common/build.gradle index ba5a027552..4db7e53ea0 100644 --- a/android/navi-common/build.gradle +++ b/android/navi-common/build.gradle @@ -80,6 +80,7 @@ dependencies { api libs.kotlinx.coroutines.core implementation libs.android.material + implementation libs.android.gms.playServicesAuthApiPhone implementation libs.androidx.appcompat implementation libs.androidx.constraintlayout implementation libs.androidx.core.ktx diff --git a/android/navi-common/src/main/java/com/navi/common/receiver/OtpReceiveListener.kt b/android/navi-common/src/main/java/com/navi/common/receiver/OtpReceiveListener.kt index e6218c74ae..4155703e9b 100644 --- a/android/navi-common/src/main/java/com/navi/common/receiver/OtpReceiveListener.kt +++ b/android/navi-common/src/main/java/com/navi/common/receiver/OtpReceiveListener.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2020-2024 by Navi Technologies Limited + * * Copyright © 2020-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -11,7 +11,7 @@ import android.content.Intent interface OtpReceiveListener { - fun onOtpReceive(otp: String) + fun onOtpReceive(otp: String, smsOriginatingAddress: String?) {} fun onConsentIntentRetrieved(intent: Intent) {} } diff --git a/android/navi-common/src/main/java/com/navi/common/receiver/SmsAutoReadReceiver.kt b/android/navi-common/src/main/java/com/navi/common/receiver/SmsAutoReadReceiver.kt index dbb679809b..6cf031e860 100644 --- a/android/navi-common/src/main/java/com/navi/common/receiver/SmsAutoReadReceiver.kt +++ b/android/navi-common/src/main/java/com/navi/common/receiver/SmsAutoReadReceiver.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2020 by Navi Technologies Limited + * * Copyright © 2020-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -18,7 +18,7 @@ import com.google.android.gms.common.api.Status import com.navi.common.utils.log import java.util.regex.Pattern -class SmsAutoReadReceiver : BroadcastReceiver() { +class SmsAutoReadReceiver(private val otpLength: Int = 4) : BroadcastReceiver() { private var otpReceiveListener: OtpReceiveListener? = null @@ -34,8 +34,13 @@ class SmsAutoReadReceiver : BroadcastReceiver() { status?.let { statusCode -> if (statusCode == CommonStatusCodes.SUCCESS) { val message = extra.get(SmsRetriever.EXTRA_SMS_MESSAGE) as String? + val smsOriginatingAddress = + extras.getString(SmsRetriever.EXTRA_SMS_ORIGINATING_ADDRESS) parseCode(message)?.let { otp -> - otpReceiveListener?.onOtpReceive(otp) + otpReceiveListener?.onOtpReceive( + otp, + smsOriginatingAddress = smsOriginatingAddress, + ) return@onReceive } } @@ -46,7 +51,7 @@ class SmsAutoReadReceiver : BroadcastReceiver() { val smsMessage = SmsMessage.createFromPdu(pdu as? ByteArray) val messageBody = smsMessage?.messageBody parseCode(messageBody)?.let { otp -> - otpReceiveListener?.onOtpReceive(otp) + otpReceiveListener?.onOtpReceive(otp, "") return@onReceive } } @@ -60,7 +65,7 @@ class SmsAutoReadReceiver : BroadcastReceiver() { if (message == null) return null var code: String? = null try { - val p = Pattern.compile("\\b\\d{4}\\b") + val p = Pattern.compile("\\b\\d{$otpLength}\\b") val m = p.matcher(message) while (m.find()) { code = m.group(0) diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt b/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt index 34929a8746..9e21f96b00 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt @@ -452,6 +452,7 @@ fun registerReceiverWithVersionCheck( broadcastReceiver: BroadcastReceiver?, intentFilter: IntentFilter, listenToBroadcastsFromOtherApps: Boolean = true, + broadcastPermission: String? = null, ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val receiverFlags = @@ -460,7 +461,13 @@ fun registerReceiverWithVersionCheck( } else { RECEIVER_NOT_EXPORTED } - context.registerReceiver(broadcastReceiver, intentFilter, receiverFlags) + context.registerReceiver( + broadcastReceiver, + intentFilter, + broadcastPermission, + null, + receiverFlags, + ) } else { context.registerReceiver(broadcastReceiver, intentFilter) } diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/kyc/activity/OtpCaptchaActivity.kt b/android/navi-insurance/src/main/java/com/navi/insurance/kyc/activity/OtpCaptchaActivity.kt index f55a9348e6..a7365336a2 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/kyc/activity/OtpCaptchaActivity.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/kyc/activity/OtpCaptchaActivity.kt @@ -70,7 +70,7 @@ class OtpCaptchaActivity : GiBaseActivity(), WidgetCallback { } otpReceiver.setListener( object : OtpReceiveListener { - override fun onOtpReceive(otp: String) { + override fun onOtpReceive(otp: String, smsOriginatingAddress: String?) { if (otp.length == CKYC_OTP_MAX_CHAR) { viewModel.updateOtpValue(otp) } diff --git a/android/navi-pay/build.gradle b/android/navi-pay/build.gradle index 2e1b90fce8..8bf376abe7 100644 --- a/android/navi-pay/build.gradle +++ b/android/navi-pay/build.gradle @@ -55,6 +55,7 @@ android { buildConfigField 'String', 'NAVIPAY_SMV_CLIENT_ID', formatString('093f0fc3-bbe5-4944-a6c3-1bf410ae4237') buildConfigField "String", "JUSPAY_AXIS_HANDLE", formatString('@axisbiz') buildConfigField 'String', 'NAVIPAY_CONVERSATION_ID_GENERATOR_SALT', formatString('c9b5d45f-fc8c-44e9-8db7-04f09e7bf7db') + buildConfigField 'String', 'FIREBASE_CLOUD_PROJECT_NUMBER', formatString('679085041776') } prod { dimension "app" @@ -67,6 +68,7 @@ android { buildConfigField 'String', 'NAVIPAY_SMV_CLIENT_ID', formatString("$NAVIPAY_SMV_CLIENT_ID") buildConfigField "String", "JUSPAY_AXIS_HANDLE", formatString('@naviaxis') buildConfigField 'String', 'NAVIPAY_CONVERSATION_ID_GENERATOR_SALT', formatString("$NAVIPAY_CONVERSATION_ID_GENERATOR_SALT") + buildConfigField 'String', 'FIREBASE_CLOUD_PROJECT_NUMBER', formatString('1074736393094') } } } @@ -89,6 +91,7 @@ dependencies { implementation project(":navi-money-manager") implementation libs.accompanist.systemuicontroller implementation libs.android.material + implementation libs.android.play.integrity implementation libs.androidx.appcompat implementation libs.androidx.camera.mlkit.vision implementation libs.androidx.compose.animation diff --git a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModelTest.kt b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModelTest.kt index dfc489992c..d22a87db84 100644 --- a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModelTest.kt +++ b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/management/common/upiid/viewmodel/UPIIdInputViewModelTest.kt @@ -54,15 +54,15 @@ class UpiIdViewModelUnitTest : NaviPayAndroidTest() { } returns null upiIdInputViewModel = UPIIdInputViewModel( - coroutineDispatcherProvider, - DeviceInfoImplTest(), - linkedAccountsUseCase, - naviPayNetworkConnectivity, - naviPayConfigUseCase, - naviPaySessionHelper, - validateVpaUseCase, - resourceProvider, - naviPayActivityDataProvider, + coroutineDispatcherProvider = coroutineDispatcherProvider, + deviceInfoProvider = DeviceInfoImplTest(), + linkedAccountsUseCase = linkedAccountsUseCase, + naviPayNetworkConnectivity = naviPayNetworkConnectivity, + naviPayConfigUseCase = naviPayConfigUseCase, + naviPaySessionHelper = naviPaySessionHelper, + validateVpaUseCase = validateVpaUseCase, + resourceProvider = resourceProvider, + naviPayActivityDataProvider = naviPayActivityDataProvider, ) } diff --git a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/network/di/NaviPayNetworkModuleTest.kt b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/network/di/NaviPayNetworkModuleTest.kt index 606a604484..2f6f4442b1 100644 --- a/android/navi-pay/src/androidTest/kotlin/com/navi/pay/network/di/NaviPayNetworkModuleTest.kt +++ b/android/navi-pay/src/androidTest/kotlin/com/navi/pay/network/di/NaviPayNetworkModuleTest.kt @@ -50,6 +50,8 @@ import com.navi.pay.network.retrofit.NaviPayExternalRetrofitService import com.navi.pay.network.retrofit.NaviPayRetrofitService import com.navi.pay.npcicl.NpciSessionHandler import com.navi.pay.npcicl.NpciSessionHandlerImpl +import com.navi.pay.onboarding.binding.util.IntegrityManager +import com.navi.pay.onboarding.binding.util.StandardIntegrityManager import com.navi.pay.onboarding.binding.viewmodel.SmsManager import com.navi.pay.onboarding.binding.viewmodel.SmsManagerImpl import com.navi.pay.utils.NAVIPAY_NETWORK_INFO_TIMEOUT @@ -283,6 +285,12 @@ abstract class NaviPayDeviceModuleTest { abstract fun bindDarkKnightScheduler( darkKnightSchedulerImpl: DarkKnightSchedulerImpl ): DarkKnightScheduler + + @Singleton + @Binds + abstract fun bindIntegrityManager( + standardIntegrityManager: StandardIntegrityManager + ): IntegrityManager } @Module diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt index 3663ee84f0..1dc8d306ae 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/analytics/NaviPayAnalytics.kt @@ -50,6 +50,7 @@ import com.navi.pay.onboarding.account.common.model.view.VpaEntity import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity import com.navi.pay.onboarding.account.linked.model.view.LinkedAccountsScreenSource import com.navi.pay.onboarding.binding.model.network.BindingType +import com.navi.pay.onboarding.binding.model.network.BindingType.Companion.name import com.navi.pay.onboarding.binding.model.network.CustomerResponse import com.navi.pay.onboarding.binding.model.network.DeviceAttributes import com.navi.pay.onboarding.binding.model.network.ServiceProvider @@ -304,6 +305,27 @@ class NaviPayAnalytics private constructor() { mapOf("duplicateIds" to duplicateIds), ) } + + fun onIntegrityTokenProviderGeneration(timeTaken: String) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_IntegrityTokenProvider_Generation", + mapOf("timeTaken" to timeTaken), + ) + } + + fun onIntegrityTokenProviderGenerationFailure(exception: String) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_IntegrityTokenProvider_Generation_Failure", + mapOf("exception" to exception), + ) + } + + fun onIntegrityTokenGeneration(timeTaken: String) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_IntegrityTokenRequest_Generation", + mapOf("timeTaken" to timeTaken), + ) + } } inner class SetupUseCaseEvents { @@ -462,13 +484,13 @@ class NaviPayAnalytics private constructor() { bindingType: BindingType, ) { NaviTrackEvent.trackEventOnClickStream( - eventName = "NaviPay_Dev_onStartBindStatusPolling", + eventName = "NaviPay_onStartBindStatusPolling", eventValues = mapOf( "naviPayOnboardingSource" to onboardingSource.orEmpty(), "naviPayOnboardingSessionId" to naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), - "bindingType" to bindingType.name, + "bindingType" to bindingType.name(), ), ) } @@ -523,9 +545,10 @@ class NaviPayAnalytics private constructor() { deviceAttributes: DeviceAttributes, onboardingSource: String? = null, naviPaySessionAttributes: Map? = null, + bindingType: String, ) { NaviTrackEvent.trackEventOnClickStream( - "NaviPay_Dev_OnBindDeviceCallForBinding", + "NaviPay_OnBindDeviceCallForBinding", mapOf( Pair("deviceAttributes", deviceAttributes.toString()), Pair("naviPayOnboardingSource", onboardingSource.orEmpty()), @@ -533,6 +556,7 @@ class NaviPayAnalytics private constructor() { "naviPayOnboardingSessionId", naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), ), + "bindingType" to bindingType, ), ) } @@ -605,6 +629,7 @@ class NaviPayAnalytics private constructor() { fun simBindingSuccess( onboardingSource: String? = null, naviPaySessionAttributes: Map? = null, + bindingType: BindingType, ) { NaviTrackEvent.trackEventOnClickStream( "NaviPay_Setup_SimBinding_Success", @@ -612,6 +637,7 @@ class NaviPayAnalytics private constructor() { "naviPayOnboardingSource" to onboardingSource.orEmpty(), "naviPayOnboardingSessionId" to naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + "bindingType" to bindingType.name(), ), ) } @@ -891,6 +917,7 @@ class NaviPayAnalytics private constructor() { provider: NetworkProvider, naviPaySessionAttributes: Map? = null, otpVerificationState: String, + bindingType: BindingType, ) { NaviTrackEvent.trackEventOnClickStream( "NaviPay_Setup_OTPVerification_ResendOTP_Clicked", @@ -902,6 +929,7 @@ class NaviPayAnalytics private constructor() { naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), ), Pair("otpVerificationState", otpVerificationState), + "bindingType" to bindingType.name(), ), ) } @@ -988,6 +1016,25 @@ class NaviPayAnalytics private constructor() { ), ) } + + fun onRsmsTriggeredPreviouslyAndFailed() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_RsmsTriggeredPreviouslyAndFailed") + } + + fun onRsmsLitmusEligibility(isEligible: Boolean) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_RsmsLitmusEligibility", + mapOf("isEligible" to isEligible.toString()), + ) + } + + fun onRsmsOtpGeneratedSuccessfully() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_RsmsOtpGeneratedSuccessfully") + } + + fun onRsmsAutoReadOtpSuccess() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_RsmsAutoReadOtpSuccess") + } } inner class NaviPayPermission { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt index 9ab6ea0594..d9fd3977c2 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt @@ -63,6 +63,8 @@ import com.navi.pay.network.retrofit.NaviPayExternalRetrofitService import com.navi.pay.network.retrofit.NaviPayRetrofitService import com.navi.pay.npcicl.NpciSessionHandler import com.navi.pay.npcicl.NpciSessionHandlerImpl +import com.navi.pay.onboarding.binding.util.IntegrityManager +import com.navi.pay.onboarding.binding.util.StandardIntegrityManager import com.navi.pay.onboarding.binding.viewmodel.SmsManager import com.navi.pay.onboarding.binding.viewmodel.SmsManagerImpl import com.navi.pay.utils.NAVIPAY_NETWORK_INFO_TIMEOUT @@ -309,6 +311,12 @@ abstract class NaviPayDeviceModule { abstract fun bindDarkKnightScheduler( darkKnightSchedulerImpl: DarkKnightSchedulerImpl ): DarkKnightScheduler + + @Singleton + @Binds + abstract fun bindIntegrityManager( + standardIntegrityManager: StandardIntegrityManager + ): IntegrityManager } @Module diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt index 07f19387f9..58f250b564 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceRequest.kt @@ -19,5 +19,15 @@ data class BindDeviceRequest( data class BindDeviceRequestPspDetails( @SerializedName("merchantCustomerId") val merchantCustomerId: String?, - @SerializedName("bindingType") val bindingType: BindingType, + @SerializedName("bindingType") val bindingType: String, + @SerializedName("rsmsBindingRequestData") + val rsmsBindingRequestData: RsmsBindingRequestData? = null, +) + +data class RsmsBindingRequestData( + @SerializedName("integrityToken") val integrityToken: String, + @SerializedName("attemptIdentifier") + val attemptIdentifier: String?, // null for 1st time, value from response when resend clicked + @SerializedName("internalDeviceId") + val internalDeviceId: String?, // null for 1st time, value for resend clicked ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt index 0132690e6f..4d9c3a6cd0 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt @@ -19,6 +19,7 @@ data class BindDeviceResponsePspDetails( @SerializedName("internalDeviceId") val internalDeviceId: String, @SerializedName("smsBindingData") val smsBindingData: SmsBindingData?, @SerializedName("smvBindingData") val smvBindingData: SmvBindingData?, + @SerializedName("rsmsBindingData") val rsmsBindingData: RsmsBindingData?, ) data class SmvBindingData( @@ -32,6 +33,8 @@ data class SmsBindingData( @SerializedName("expiryTimestamp") val expiryTimestamp: String, ) +data class RsmsBindingData(@SerializedName("attemptIdentifier") val attemptIdentifier: String) + data class ServiceProvider( @SerializedName("name") val name: String, @SerializedName("number") val number: String, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt index aa02356694..3086f6d68b 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt @@ -19,5 +19,13 @@ data class BindDeviceStatusRequest( data class BindDeviceStatusRequestPspDetails( @SerializedName("internalDeviceId") val internalDeviceId: String, @SerializedName("merchantCustomerId") val merchantCustomerId: String, - @SerializedName("bindingType") val bindingType: BindingType, + @SerializedName("bindingType") val bindingType: String, + @SerializedName("rsmsBindingStatusData") + val rsmsBindingStatusData: RsmsBindingStatusData? = null, +) + +data class RsmsBindingStatusData( + @SerializedName("otp") val otp: String, + @SerializedName("senderId") val senderId: String, + @SerializedName("integrityToken") val integrityToken: String, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt index 5bb96afd21..79bf9c9889 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt @@ -7,7 +7,20 @@ package com.navi.pay.onboarding.binding.model.network -enum class BindingType { - SMV, - SMS, +sealed class BindingType { + data object SMV : BindingType() + + data object SMS : BindingType() + + data class RSMS(val otp: String = "", val senderAddress: String = "") : BindingType() + + companion object { + fun BindingType.name(): String { + return when (this) { + is SMV -> "SMV" + is SMS -> "SMS" + is RSMS -> "RSMS" + } + } + } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt index b63605115f..bcf9ce7596 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt @@ -24,7 +24,7 @@ sealed class DeviceBindingState { companion object { fun DeviceBindingState.isVerificationOngoing(): Boolean { - return this == Binding || this == Verifying + return this in listOf(Initiated, Binding, Verifying) } } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/NaviPayOnboardingBottomSheetType.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/NaviPayOnboardingBottomSheetType.kt index 903b1d9670..2607cdd236 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/NaviPayOnboardingBottomSheetType.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/NaviPayOnboardingBottomSheetType.kt @@ -10,6 +10,7 @@ package com.navi.pay.onboarding.binding.model.view import com.navi.pay.common.model.view.NaviPayButtonAction import com.navi.pay.common.model.view.NaviPayErrorConfig import com.navi.pay.common.model.view.SimInfo +import com.navi.pay.onboarding.binding.model.network.BindingType sealed class NaviPayOnboardingBottomSheetType { @@ -63,13 +64,14 @@ sealed class NaviPayOnboardingBottomSheetType { val actionType: NaviPayButtonAction?, ) : NaviPayOnboardingBottomSheetType() - data object AutoReadOtp : NaviPayOnboardingBottomSheetType() + data class AutoReadOtp(val bindingType: BindingType) : NaviPayOnboardingBottomSheetType() data class OtpNotGeneratedError( val title: String, val description: String, val primaryButtonText: String, val secondaryButtonText: String, + val bindingType: BindingType, ) : NaviPayOnboardingBottomSheetType() data object AutoReadOtpVerificationLimitReached : NaviPayOnboardingBottomSheetType() diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingBottomSheetContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingBottomSheetContent.kt index 0e70afde8e..062d28df86 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingBottomSheetContent.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/NaviPayOnboardingBottomSheetContent.kt @@ -230,8 +230,16 @@ fun NaviPayOnboardingBottomSheetContent( BOTTOM_SHEET_HEIGHT_PERCENTAGE_80 ), timeOutInSeconds = otpTimeOut, - onResendOtpClicked = naviPayOnboardingViewModel::onReVerifyOrReSendButtonClicked, - onReVerifyOtpClicked = naviPayOnboardingViewModel::onReVerifyOrReSendButtonClicked, + onResendOtpClicked = { + naviPayOnboardingViewModel.onReVerifyOrReSendButtonClicked( + bindingType = bottomSheetType.bindingType + ) + }, + onReVerifyOtpClicked = { + naviPayOnboardingViewModel.onReVerifyOrReSendButtonClicked( + bindingType = bottomSheetType.bindingType + ) + }, generatedOtp = generatedOtp, autoReadOtpVerificationState = autoReadOtpVerificationState, showButtonLoader = showButtonLoader, @@ -295,8 +303,11 @@ fun NaviPayOnboardingBottomSheetContent( description = bottomSheetType.description, primaryButton = bottomSheetType.primaryButtonText, secondaryButton = bottomSheetType.secondaryButtonText, - onPrimaryButtonClicked = - naviPayOnboardingViewModel::onReVerifyOrReSendButtonClicked, + onPrimaryButtonClicked = { + naviPayOnboardingViewModel.onReVerifyOrReSendButtonClicked( + bindingType = bottomSheetType.bindingType + ) + }, onSecondaryButtonClicked = { naviPayAnalytics.onDeclineDeviceCancelClick() naviPayOnboardingViewModel.updateOnboardingAction( 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 a6f55e58dd..61e4084b54 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 @@ -10,6 +10,7 @@ package com.navi.pay.onboarding.binding.ui import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context +import android.content.Intent import android.content.IntentFilter import android.os.Bundle import androidx.activity.compose.BackHandler @@ -44,6 +45,8 @@ import com.navi.base.deeplink.DeepLinkManager import com.navi.base.deeplink.util.DeeplinkConstants import com.navi.base.model.CtaData import com.navi.common.constants.APP_UPGRADE_DATA +import com.navi.common.receiver.OtpReceiveListener +import com.navi.common.receiver.SmsAutoReadReceiver import com.navi.common.receiver.SmsAutoReadWithConsentReceiver import com.navi.common.upi.UPI_RESULT_CODE import com.navi.common.utils.generateSenderNames @@ -73,7 +76,7 @@ import com.navi.pay.permission.utils.PermissionKeys.FIRST_TIME_SCREEN_PERMISSION import com.navi.pay.permission.utils.PermissionKeys.LOCATION_PERMISSION_KEY import com.navi.pay.permission.utils.PermissionKeys.NON_FIRST_TIME_SCREEN_PERMISSION_KEY import com.navi.pay.permission.utils.PermissionUtils -import com.navi.pay.utils.getOtpReceiveListener +import com.navi.pay.utils.NAVI_PAY_RSMS_OTP_LENGTH import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult @@ -132,6 +135,17 @@ fun NaviPayOnboardingScreen( contract = ActivityResultContracts.StartActivityForResult() ) {} + LaunchedEffect(Unit) { + naviPayOnboardingViewModel.triggerSmsRetrieverInstance.collect { + naviPayOnboardingViewModel.resetTriggerSmsRetrieverInstance() + val smsRetrieverClient = SmsRetriever.getClient(naviPayOnboardingActivity) + smsRetrieverClient + .startSmsRetriever() + .addOnSuccessListener { naviPayOnboardingViewModel.onSmsRetrieverSetupComplete() } + .addOnFailureListener {} + } + } + LaunchedEffect(Unit) { naviPayOnboardingViewModel.redirectToSetPinAndFinishActivity.collect { selectedAccountIdForPinSet -> @@ -155,6 +169,10 @@ fun NaviPayOnboardingScreen( onAutoReadOtpConsentAllowClicked = onAutoReadOtpConsentAllowClicked, ) + SmsAutoReadHandler( + onRsmsAutoReadOtpReceived = naviPayOnboardingViewModel::onRsmsAutoReadOtpReceived + ) + val coroutineScope = rememberCoroutineScope() val onboardingDataFromIntent by naviPayOnboardingViewModel.onboardingDataFromIntent.collectAsStateWithLifecycle() @@ -606,8 +624,9 @@ fun SmsConsentHandler( SmsAutoReadWithConsentReceiver().apply { setListener( otpReceiveListener = - getOtpReceiveListener( - handleConsentIntent = { intent -> + object : OtpReceiveListener { + + override fun onConsentIntentRetrieved(intent: Intent) { try { onSmsConsentBottomSheetVisible() smsConsentIntentLauncher.launch(intent) @@ -615,7 +634,7 @@ fun SmsConsentHandler( e.log() } } - ) + } ) } val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION) @@ -642,6 +661,48 @@ fun SmsConsentHandler( } } +@Composable +private fun SmsAutoReadHandler(onRsmsAutoReadOtpReceived: (String, String?) -> Unit) { + + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner) { + val smsReceiver = + SmsAutoReadReceiver(otpLength = NAVI_PAY_RSMS_OTP_LENGTH).apply { + setListener( + otpReceiveListener = + object : OtpReceiveListener { + override fun onOtpReceive(otp: String, smsOriginatingAddress: String?) { + onRsmsAutoReadOtpReceived(otp, smsOriginatingAddress) + } + } + ) + } + + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + registerReceiverWithVersionCheck( + context = context, + broadcastReceiver = smsReceiver, + intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION), + listenToBroadcastsFromOtherApps = true, + broadcastPermission = SmsRetriever.SEND_PERMISSION, + ) + } + Lifecycle.Event.ON_PAUSE -> { + context.unregisterReceiver(smsReceiver) + } + else -> {} + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } +} + /* Todo: Replace this with NaviPayModalBottomSheet */ diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/util/IntegrityManager.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/util/IntegrityManager.kt new file mode 100644 index 0000000000..7f2f7a704f --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/util/IntegrityManager.kt @@ -0,0 +1,94 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.onboarding.binding.util + +import android.content.Context +import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest +import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider +import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest +import com.navi.common.utils.log +import com.navi.pay.BuildConfig +import com.navi.pay.analytics.NaviPayAnalytics +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.measureTimedValue +import kotlinx.coroutines.tasks.await + +interface IntegrityManager { + suspend fun prepareIntegrityTokenProvider() + + suspend fun requestIntegrityToken(requestHash: String): String? +} + +@Singleton +class StandardIntegrityManager @Inject constructor(@ApplicationContext val context: Context) : + IntegrityManager { + + private val standardIntegrityManager = IntegrityManagerFactory.createStandard(context) + + private var integrityTokenProvider: StandardIntegrityTokenProvider? = null + + private val naviPayAnalytics: NaviPayAnalytics.UtilityEvents = + NaviPayAnalytics.INSTANCE.UtilityEvents() + + override suspend fun prepareIntegrityTokenProvider() { + + if (integrityTokenProvider != null) { + return + } + + val prepareIntegrityTokenRequest = + PrepareIntegrityTokenRequest.builder() + .setCloudProjectNumber(BuildConfig.FIREBASE_CLOUD_PROJECT_NUMBER.toLong()) + .build() + + try { + val integrityTokenProviderRequestHolder = measureTimedValue { + standardIntegrityManager.prepareIntegrityToken(prepareIntegrityTokenRequest).await() + } + integrityTokenProvider = integrityTokenProviderRequestHolder.value + naviPayAnalytics.onIntegrityTokenProviderGeneration( + timeTaken = + integrityTokenProviderRequestHolder.duration.inWholeMilliseconds.toString() + ) + } catch (e: Exception) { + e.log() + naviPayAnalytics.onIntegrityTokenProviderGenerationFailure( + exception = e.message.toString() + ) + } + } + + override suspend fun requestIntegrityToken(requestHash: String): String? { + + if (integrityTokenProvider == null) { + prepareIntegrityTokenProvider() + } + + val integrityTokenRequest = + StandardIntegrityTokenRequest.builder() + .apply { + if (!requestHash.isBlank()) { + setRequestHash(requestHash) + } + } + .build() + + val integrityTokenRequestHolder = measureTimedValue { + integrityTokenProvider?.request(integrityTokenRequest)?.await()?.token() + } + + naviPayAnalytics.onIntegrityTokenGeneration( + timeTaken = integrityTokenRequestHolder.duration.inWholeMilliseconds.toString() + ) + + return integrityTokenRequestHolder.value + } +} 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 2e86d0a7b7..69d65c2692 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 @@ -86,11 +86,14 @@ import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusRequest import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusRequestPspDetails import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusResponse import com.navi.pay.onboarding.binding.model.network.BindingType +import com.navi.pay.onboarding.binding.model.network.BindingType.Companion.name import com.navi.pay.onboarding.binding.model.network.DeclineBindDeviceRequestPspDetails import com.navi.pay.onboarding.binding.model.network.DeclineDeviceRequest import com.navi.pay.onboarding.binding.model.network.DeviceAttributes import com.navi.pay.onboarding.binding.model.network.DeviceBasicAttributes import com.navi.pay.onboarding.binding.model.network.NaviPayOnboardingConfig +import com.navi.pay.onboarding.binding.model.network.RsmsBindingRequestData +import com.navi.pay.onboarding.binding.model.network.RsmsBindingStatusData import com.navi.pay.onboarding.binding.model.view.DeviceBindingState import com.navi.pay.onboarding.binding.model.view.DeviceBindingState.Companion.isVerificationOngoing import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetStateHolder @@ -98,6 +101,7 @@ import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetTy import com.navi.pay.onboarding.binding.model.view.OnboardingDeviceData import com.navi.pay.onboarding.binding.model.view.SmvEligibilityStatus import com.navi.pay.onboarding.binding.repository.NaviPayOnboardingRepository +import com.navi.pay.onboarding.binding.util.IntegrityManager import com.navi.pay.onboarding.common.NaviPayOnBoardingActions import com.navi.pay.onboarding.common.NaviPayOnboardingActionsType import com.navi.pay.onboarding.common.utils.OnboardingIntentDataProvider @@ -111,8 +115,10 @@ 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 +import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_REVERSE_SMS_BINDING import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING import com.navi.pay.utils.NAVI_PAY_API_STATUS_SUCCESS +import com.navi.pay.utils.NAVI_PAY_DEVICE_BINDING_IS_RSMS_TRIGGERED_AND_FAILED import com.navi.pay.utils.NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED import com.navi.pay.utils.NAVI_PAY_GREEN_TICK_LOTTIE import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE @@ -141,7 +147,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -189,6 +194,7 @@ constructor( private val naviPayCustomerStatusHandler: NaviPayCustomerStatusHandler, private val naviCommonRepository: NaviCommonRepository, private val sharedPreferenceRepository: SharedPreferenceRepository, + private val integrityManager: IntegrityManager, savedStateHandle: SavedStateHandle, ) : NaviPayBaseVM() { @@ -302,6 +308,11 @@ constructor( private val naviPayDefaultConfig = MutableStateFlow(NaviPayDefaultConfig()) + private val _triggerSmsRetrieverInstance = MutableSharedFlow(replay = 1) + val triggerSmsRetrieverInstance = _triggerSmsRetrieverInstance.asSharedFlow() + + private val smsRetrieverSetupResult = Channel(capacity = 1) + val appUpgradeData = naviPayDefaultConfig .map { naviPayDefaultConfig -> @@ -342,14 +353,18 @@ constructor( private val _showButtonLoader = MutableStateFlow(false) val showButtonLoader = _showButtonLoader.asStateFlow() - private val _otpTimeOut = MutableStateFlow(30) + private val OTP_TIMEOUT = 30 + + private val _otpTimeOut = MutableStateFlow(OTP_TIMEOUT) val otpTimeOut = _otpTimeOut.asStateFlow() private var generatedOtpToken: String = EMPTY private var isBindingEligibleForSmv = false - private val locationPermissionRequestResult = Channel() + private var isBindingEligibleForRsms = false + + private val locationPermissionRequestResult = Channel(capacity = 1) init { updateNaviPayDefaultConfig() @@ -405,6 +420,11 @@ constructor( _launchVpaActivationFlow.resetReplayCache() } + @OptIn(ExperimentalCoroutinesApi::class) + fun resetTriggerSmsRetrieverInstance() { + _triggerSmsRetrieverInstance.resetReplayCache() + } + private fun updateOnboardingIntentData() { _onboardingDataFromIntent.update { onboardingDataFromIntentProvider.getOnboardingIntentData() @@ -573,7 +593,7 @@ constructor( } selectedSimInfo = selectedSimInfo ?: simInfoList[0] - selectedSimInfo?.phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber" + selectedSimInfo.phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber" _selectedSimInfo.update { selectedSimInfo } } @@ -684,6 +704,14 @@ constructor( fun confirmSimSelection() { viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { + updateShowButtonLoader(true) + isBindingEligibleForRsms = getRsmsEligibilityStatus() + + if (isBindingEligibleForRsms) { + startSimBinding() + return@safeLaunch + } + val smvEligibilityStatus = getSmvEligibilityStatus(provider = getNetworkProvider()) if (!smvEligibilityStatus.isEligible) { @@ -832,7 +860,11 @@ constructor( naviPaySessionHelper.getNaviPaySessionAttributes() fun onLocationPermissionRequestResult() { - viewModelScope.launch { locationPermissionRequestResult.send(Unit) } + viewModelScope.launch(Dispatchers.IO) { locationPermissionRequestResult.send(Unit) } + } + + fun onSmsRetrieverSetupComplete() { + viewModelScope.launch(Dispatchers.IO) { smsRetrieverSetupResult.send(Unit) } } private suspend fun requestLocationPermission() { @@ -894,44 +926,9 @@ constructor( ) } - val deviceBasicAttributes = getDeviceBasicAttributes() - val deviceAttributes = - DeviceAttributes( - deviceBasicAttributes = deviceBasicAttributes, - manufacturer = Build.MANUFACTURER, - model = Build.MODEL, - version = Build.VERSION.SDK_INT.toString(), - ) - naviPayAnalytics.onBindDeviceCallForBinding( - deviceAttributes = deviceAttributes, - onboardingSource = onboardingSource.value, - naviPaySessionAttributes = getNaviPaySessionAttributes(), - ) - - val isConsentGivenForOtpAutoRead = - sharedPreferenceRepository.getBooleanValueSecurely(key = AUTO_READ_OTP_CONSENT_KEY) val bindDeviceAPIResponse = naviPayOnboardingRepository.bindDevice( - bindDeviceRequest = - BindDeviceRequest( - deviceAttributes = deviceAttributes, - phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber", - pspDetails = - mapOf( - onboardingPsp to - BindDeviceRequestPspDetails( - merchantCustomerId = - naviPayCustomerStatusHandler - .getCustomerOnboardingEntity(onboardingPsp) - ?.merchantCustomerId - .takeIf { it?.isNotBlank() == true }, - bindingType = - if (isBindingEligibleForSmv) BindingType.SMV - else BindingType.SMS, - ) - ), - isAutoOtpPermitted = isConsentGivenForOtpAutoRead, - ), + bindDeviceRequest = generateBindDeviceRequest(), metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), ) updateShowButtonLoader(false) @@ -943,6 +940,11 @@ constructor( } else { onBindingError(bindDeviceAPIResponse) } + + if (isBindingEligibleForRsms) { + markRsmsTriggeredAndFailed() + } + return } @@ -969,13 +971,71 @@ constructor( merchantCustomerId = merchantCustomerId, ) - if (isBindingEligibleForSmv) { + if (isBindingEligibleForRsms) { + processBindDeviceResponseForRsms() + } else if (isBindingEligibleForSmv) { processBindDeviceResponseForSmv() } else { processBindDeviceResponseForSms(provider = provider) } } + private suspend fun generateBindDeviceRequest(): BindDeviceRequest { + + val isConsentGivenForOtpAutoRead = + sharedPreferenceRepository.getBooleanValueSecurely(key = AUTO_READ_OTP_CONSENT_KEY) + + val deviceBasicAttributes = getDeviceBasicAttributes() + val deviceAttributes = + DeviceAttributes( + deviceBasicAttributes = deviceBasicAttributes, + manufacturer = Build.MANUFACTURER, + model = Build.MODEL, + version = Build.VERSION.SDK_INT.toString(), + ) + + val bindingType = + if (isBindingEligibleForRsms) BindingType.RSMS().name() + else if (isBindingEligibleForSmv) BindingType.SMV.name() else BindingType.SMS.name() + + naviPayAnalytics.onBindDeviceCallForBinding( + deviceAttributes = deviceAttributes, + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + bindingType = bindingType, + ) + + return BindDeviceRequest( + deviceAttributes = deviceAttributes, + phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber", + pspDetails = + mapOf( + onboardingPsp to + BindDeviceRequestPspDetails( + merchantCustomerId = + naviPayCustomerStatusHandler + .getCustomerOnboardingEntity(onboardingPsp) + ?.merchantCustomerId + .takeIf { it?.isNotBlank() == true }, + bindingType = bindingType, + rsmsBindingRequestData = + if (bindingType != BindingType.RSMS().name()) null + else + RsmsBindingRequestData( + integrityToken = + integrityManager.requestIntegrityToken( + requestHash = deviceInfoProvider.getDeviceId() + ) ?: "", + attemptIdentifier = null, + internalDeviceId = null, + ), + ) + ), + isAutoOtpPermitted = + if (bindingType == BindingType.RSMS().name()) true else isConsentGivenForOtpAutoRead, + ) + } + private fun updateShowButtonLoader(showButtonLoader: Boolean) { _showButtonLoader.update { showButtonLoader } } @@ -986,14 +1046,19 @@ constructor( _autoReadOtpVerificationState.update { autoReadOtpVerificationState } } - fun onReVerifyOrReSendButtonClicked() { + fun onReVerifyOrReSendButtonClicked(bindingType: BindingType) { viewModelScope.launch(Dispatchers.IO) { naviPayAnalytics.onResendOtpClicked( naviPaySessionAttributes = getNaviPaySessionAttributes(), provider = getNetworkProvider(), otpVerificationState = autoReadOtpVerificationState.value.name, + bindingType = bindingType, ) - handleCaseForNonVerifiedSenderLoginOrResendOtp() + if (bindingType is BindingType.RSMS) { + handleCaseForRsmsResendOtp() + } else { + handleCaseForNonVerifiedSenderLoginOrResendOtp() + } } } @@ -1068,6 +1133,27 @@ constructor( } } + private suspend fun processBindDeviceResponseForRsms() { + naviPayAnalytics.onRsmsOtpGeneratedSuccessfully() + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.DEFAULT) + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.AutoReadOtp(bindingType = BindingType.RSMS()), + ) + + startOtpTimer() + + otpTimerJob?.join() + + if (otpTimeOut.value == 0) { + if (autoReadOtpVerificationState.value == AutoReadOtpVerificationState.DEFAULT) { + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.TIMEOUT) + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) + } + } + } + private fun checkForSMSSentAndStartPolling() { viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { naviPayAnalytics.checkForSMSSentInDevice( @@ -1167,7 +1253,19 @@ constructor( bindDeviceResponsePspDetails?.merchantCustomerId.orEmpty(), internalDeviceId = bindDeviceResponsePspDetails?.internalDeviceId.orEmpty(), - bindingType = bindingType, + bindingType = bindingType.name(), + rsmsBindingStatusData = + if (bindingType !is BindingType.RSMS) null + else + RsmsBindingStatusData( + otp = bindingType.otp, + senderId = bindingType.senderAddress, + integrityToken = + integrityManager.requestIntegrityToken( + requestHash = + deviceInfoProvider.getDeviceId() + ) ?: "", + ), ) ) naviPayOnboardingRepository.getBindDeviceStatus( @@ -1224,8 +1322,15 @@ constructor( updateDeviceBindingState(DeviceBindingState.Failure) naviApiPoller.stopPolling() onBindingError(bindDeviceStatusAPIResponse) - if (bindingType == BindingType.SMV) { - markSmvTriggeredAndFailed() + when (bindingType) { + is BindingType.RSMS -> { + markRsmsTriggeredAndFailed() + } + + BindingType.SMV -> { + markSmvTriggeredAndFailed() + } + else -> Unit } } } @@ -1253,6 +1358,7 @@ constructor( customerStatus = bindDeviceStatusResponse.pspDetails[onboardingPsp]?.customerStatus ?: NaviPayCustomerStatus.NOT_SET, + bindingType = bindingType, ) } } @@ -1260,6 +1366,7 @@ constructor( private suspend fun saveDeviceDataInSharedPreferenceAndUpdateUiState( deviceFingerPrint: String, customerStatus: NaviPayCustomerStatus, + bindingType: BindingType, ) { if (!naviPayCustomerStatusHandler.isUserOnboardedForAnyPsp()) { deviceInfoProvider.saveDeviceData(deviceData = deviceData!!) @@ -1271,16 +1378,12 @@ constructor( customerStatus = customerStatus, ) - handleDeviceBindingSuccess(customerStatus = customerStatus) - } - - private fun handleDeviceBindingSuccess(customerStatus: NaviPayCustomerStatus) { - updateDeviceBindingState(DeviceBindingState.Success) naviPayAnalytics.simBindingSuccess( onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), + bindingType = bindingType, ) handleNavigationOnCustomerStatus(customerStatus = customerStatus) @@ -1297,6 +1400,17 @@ constructor( ) } + private suspend fun markRsmsTriggeredAndFailed() { + naviCacheRepository.save( + naviCacheEntity = + NaviCacheEntity( + key = NAVI_PAY_DEVICE_BINDING_IS_RSMS_TRIGGERED_AND_FAILED, + value = "true", + version = 1, + ) + ) + } + fun declineDeviceBinding() { viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { delay( @@ -1714,7 +1828,7 @@ constructor( fun autoReadOtpConsentDenyClicked() { viewModelScope.launch(Dispatchers.IO) { otpTimerJob?.cancel() - updateOtpTimeOut(otpTimeOut = 30) + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) updateGeneratedOtp("") naviPayAnalytics.onOtpAutoReadCtaClicked( @@ -1753,7 +1867,7 @@ constructor( ) updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.VERIFYING) otpTimerJob?.cancel() - updateOtpTimeOut(otpTimeOut = 30) + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) val verifyOtpResponse = naviCommonRepository.verifyOtp( @@ -1826,7 +1940,8 @@ constructor( updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.DEFAULT) updateBottomSheetUIState( showBottomSheet = true, - bottomSheetUIState = NaviPayOnboardingBottomSheetType.AutoReadOtp, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.AutoReadOtp(bindingType = BindingType.SMS), ) startOtpTimer() @@ -1870,6 +1985,7 @@ constructor( primaryButtonText = resourceProvider.getString(resId = R.string.retry), secondaryButtonText = resourceProvider.getString(resId = R.string.cancel), + bindingType = BindingType.SMS, ), ) } @@ -1879,7 +1995,7 @@ constructor( isOtpGenerationAttemptExhausted = isOtpGenerationAttemptExhausted, ) otpTimerJob?.cancel() - updateOtpTimeOut(otpTimeOut = 30) + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) updateGeneratedOtp("") return } @@ -1890,7 +2006,7 @@ constructor( if (otpTimeOut.value == 0) { updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.TIMEOUT) - updateOtpTimeOut(otpTimeOut = 30) + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) naviPayAnalytics.onOtpAutoReadTimeout( provider = getNetworkProvider(), naviPaySessionAttributes = getNaviPaySessionAttributes(), @@ -1987,7 +2103,7 @@ constructor( naviCacheRepository .get(key = NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED) ?.value - ?.toBoolean() ?: false + ?.toBoolean() == true if (isSmvTriggeredPreviously) { naviPayAnalytics.onSmvPreviouslyTriggered() @@ -2041,6 +2157,116 @@ constructor( } } + private suspend fun getRsmsEligibilityStatus(): Boolean { + // TODO: To be removed for go live release + return false + updateShowButtonLoader(true) + val isRsmsTriggeredPreviouslyAndFailed = + naviCacheRepository + .get(key = NAVI_PAY_DEVICE_BINDING_IS_RSMS_TRIGGERED_AND_FAILED) + ?.value + ?.toBoolean() == true + + if (isRsmsTriggeredPreviouslyAndFailed) { + updateShowButtonLoader(false) + naviPayAnalytics.onRsmsTriggeredPreviouslyAndFailed() + return false + } + + val rsmsLitmusVariant = + litmusExperimentsUseCase + .execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_REVERSE_SMS_BINDING) + ?.variant + + val isRsmsExperimentEnabled = + rsmsLitmusVariant?.name == "enabled" && rsmsLitmusVariant.enabled == true + naviPayAnalytics.onRsmsLitmusEligibility(isEligible = isRsmsExperimentEnabled) + + if (isRsmsExperimentEnabled) { + _triggerSmsRetrieverInstance.emit(true) + smsRetrieverSetupResult.receive() + } + updateShowButtonLoader(false) + + return isRsmsExperimentEnabled + } + + fun onRsmsAutoReadOtpReceived(otp: String, senderAddress: String?) { + viewModelScope.launch(Dispatchers.IO) { + updateGeneratedOtp(otp = otp) + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.VERIFYING) + otpTimerJob?.cancel() + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) + + naviPayAnalytics.onRsmsAutoReadOtpSuccess() + + startStatusPolling( + bindingType = BindingType.RSMS(otp = otp, senderAddress = senderAddress ?: "") + ) + } + } + + private suspend fun handleCaseForRsmsResendOtp() { + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.DEFAULT) + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.AutoReadOtp(bindingType = BindingType.RSMS()), + ) + + startOtpTimer() + + val existingBindDeviceRequest = generateBindDeviceRequest() + val bindDeviceRequestForRsmsResendOtp = + existingBindDeviceRequest.copy( + pspDetails = + existingBindDeviceRequest.pspDetails.mapValues { (_, value) -> + value.copy( + rsmsBindingRequestData = + RsmsBindingRequestData( + integrityToken = + "", // integrity token is not required for resend OTP + attemptIdentifier = + bindDeviceResponse.pspDetails[onboardingPsp] + ?.rsmsBindingData + ?.attemptIdentifier, + internalDeviceId = + bindDeviceResponse.pspDetails[onboardingPsp] + ?.internalDeviceId + .orEmpty(), + ) + ) + } + ) + val bindDeviceAPIResponse = + naviPayOnboardingRepository.bindDevice( + bindDeviceRequest = bindDeviceRequestForRsmsResendOtp, + metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), + ) + + if (!bindDeviceAPIResponse.isSuccessWithData()) { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.ErrorBottomSheet( + isCustomerSetupError = false, + errorConfig = getError(response = bindDeviceAPIResponse, cancelable = false), + ), + ) + otpTimerJob?.cancel() + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) + updateGeneratedOtp("") + } + + otpTimerJob?.join() + + if (otpTimeOut.value == 0) { + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.TIMEOUT) + updateOtpTimeOut(otpTimeOut = OTP_TIMEOUT) + return + } + } + override val screenName: String get() = NAVI_PAY_SETUP } 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 112d0c9949..99a3d8eddf 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 @@ -42,6 +42,7 @@ const val AMOUNT_MAX_LENGTH = AMOUNT_MAX_LENGTH_BEFORE_DECIMAL + AMOUNT_MAX_LENG const val AMOUNT_MAX_LENGTH_BEFORE_DECIMAL_RCC = 10 const val AMOUNT_MAX_LENGTH_RCC = AMOUNT_MAX_LENGTH_BEFORE_DECIMAL_RCC + AMOUNT_MAX_LENGTH_AFTER_DECIMAL + 1 +const val NAVI_PAY_RSMS_OTP_LENGTH = 6 // File names const val NAVI_PAY_SECONDARY_SPLASH_LOTTIE = "navi-pay-secondary-splash.lottie" @@ -143,6 +144,8 @@ const val NAVI_PAY_GENERIC_OFFERS_INFO = "naviPayGenericOffersInfo" const val KEY_UPI_LITE_LAST_NOTIFICATION_TIMESTAMP = "upiLiteLastNotificationTimestamp" const val NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED = "naviPayDeviceBindingIsSmvTriggeredAndFailed" +const val NAVI_PAY_DEVICE_BINDING_IS_RSMS_TRIGGERED_AND_FAILED = + "naviPayDeviceBindingIsRsmsTriggeredAndFailed" const val NAVI_PAY_PSP_ROUTING_BUCKETS_KEY = "naviPayPspRoutingBucketsKey" const val KEY_UPI_LITE_LAST_MANDATE_EXECUTION_TIMESTAMP = "upiLiteLastMandateExecutionTimestamp" const val KEY_UPI_LITE_LAST_MANDATE_EXECUTION_ATTEMPTS = "upiLiteLastMandateExecutionAttempts" @@ -189,6 +192,7 @@ const val LITMUS_EXPERIMENT_ORDER_HISTORY_ERROR_WIDGET = "NaviTStore-exp-tds-err 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" +const val LITMUS_EXPERIMENT_NAVIPAY_REVERSE_SMS_BINDING = "NaviPay-exp-reverse-sms-binding" val NAVI_PAY_LITMUS_EXPERIMENTS = listOf( LITMUS_EXPERIMENT_NAVIPAY_TRANSACTION_LEDGER, @@ -206,6 +210,7 @@ val NAVI_PAY_LITMUS_EXPERIMENTS = LITMUS_EXPERIMENT_NAVIPAY_PAYMENT_RETRY_EXPERIENCE, LITMUS_EXPERIMENT_ORDER_HISTORY_ERROR_WIDGET, LITMUS_EXPERIMENT_AUTO_OPEN_CONTACT_PERMISSION, + LITMUS_EXPERIMENT_NAVIPAY_REVERSE_SMS_BINDING, ) // Generic diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/OtpReceiveListener.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/OtpReceiveListener.kt deleted file mode 100644 index f98f85df63..0000000000 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/OtpReceiveListener.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * - * * Copyright © 2025 by Navi Technologies Limited - * * All rights reserved. Strictly confidential - * - */ - -package com.navi.pay.utils - -import android.content.Intent -import com.navi.common.receiver.OtpReceiveListener - -fun getOtpReceiveListener(handleConsentIntent: (Intent) -> Unit = {}): OtpReceiveListener { - return object : OtpReceiveListener { - override fun onOtpReceive(otp: String) {} - - override fun onConsentIntentRetrieved(intent: Intent) { - handleConsentIntent(intent) - } - } -}