diff --git a/android/app/src/main/java/com/naviapp/analytics/utils/NaviAnalytics.kt b/android/app/src/main/java/com/naviapp/analytics/utils/NaviAnalytics.kt index 654e3d76c5..491efa40d9 100644 --- a/android/app/src/main/java/com/naviapp/analytics/utils/NaviAnalytics.kt +++ b/android/app/src/main/java/com/naviapp/analytics/utils/NaviAnalytics.kt @@ -22,6 +22,7 @@ import com.navi.base.utils.BaseUtils import com.navi.base.utils.isNull import com.navi.base.utils.orFalse import com.navi.common.model.ModuleNameV2 +import com.navi.common.network.models.LoginType import com.navi.common.utils.CommonNaviAnalytics import com.navi.common.utils.deviceId import com.navi.common.utils.fetchInstallerName @@ -30,7 +31,6 @@ import com.navi.insurance.util.UNDERSCORE import com.navi.payment.utils.PaymentAnalytics import com.naviapp.BuildConfig import com.naviapp.app.NaviApplication -import com.naviapp.models.request.LoginType import com.naviapp.utils.Constants import com.naviapp.utils.EMPTY import com.naviapp.utils.bundleToJson diff --git a/android/app/src/main/java/com/naviapp/models/request/UserLoginRequest.kt b/android/app/src/main/java/com/naviapp/models/request/UserLoginRequest.kt index cb937e7cea..457e593180 100644 --- a/android/app/src/main/java/com/naviapp/models/request/UserLoginRequest.kt +++ b/android/app/src/main/java/com/naviapp/models/request/UserLoginRequest.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2019-2024 by Navi Technologies Limited + * * Copyright © 2019-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -9,9 +9,9 @@ package com.naviapp.models.request import com.google.gson.annotations.SerializedName import com.navi.common.model.ClonedDetails +import com.navi.common.utils.SenderVerificationStatus import com.naviapp.BuildConfig import com.naviapp.models.TruecallerAuthData -import com.naviapp.registration.helper.SenderVerificationStatus data class UserLoginRequest( @SerializedName("type") val loginType: String? = null, diff --git a/android/app/src/main/java/com/naviapp/network/retrofit/RetrofitService.kt b/android/app/src/main/java/com/naviapp/network/retrofit/RetrofitService.kt index ea331de622..e6fc45a432 100644 --- a/android/app/src/main/java/com/naviapp/network/retrofit/RetrofitService.kt +++ b/android/app/src/main/java/com/naviapp/network/retrofit/RetrofitService.kt @@ -75,22 +75,18 @@ import com.naviapp.models.request.CustomPaymentRequest import com.naviapp.models.request.EmailRequest import com.naviapp.models.request.FeatureCompletionRequest import com.naviapp.models.request.ForecloseLoanRequest -import com.naviapp.models.request.GenerateOtpRequest import com.naviapp.models.request.NetPromoterScoreRequest import com.naviapp.models.request.NewPaymentApiRequest import com.naviapp.models.request.OnboardingRequest import com.naviapp.models.request.PreSignedUrlRequest import com.naviapp.models.request.SaphyraRequestData import com.naviapp.models.request.UserLoginRequest -import com.naviapp.models.request.VerifyOtpRequest import com.naviapp.models.response.BranchSDKResponse import com.naviapp.models.response.CreateSessionResponse import com.naviapp.models.response.EmailIssueResponse import com.naviapp.models.response.EmailUsResponse import com.naviapp.models.response.FirebaseRefreshAuthTokenResponse -import com.naviapp.models.response.GenerateOtpResponse import com.naviapp.models.response.HomeFeatureResponse -import com.naviapp.models.response.LoginOtpVerifyResponse import com.naviapp.models.response.LoginResponse import com.naviapp.models.response.NotificationSettingsContent import com.naviapp.models.response.OnboardingResponse @@ -127,18 +123,6 @@ import retrofit2.http.Url interface RetrofitService { - @POST("/otp/v1/generate") - suspend fun submitPhoneNumber( - @Body generateOtpRequest: GenerateOtpRequest, - @Header(HEADER_X_TENANT_ID) tenantId: String, - ): Response> - - @POST("/otp/v1/verify") - suspend fun submitOTP( - @Body verifyOtpRequest: VerifyOtpRequest, - @Header(HEADER_X_TENANT_ID) tenantId: String, - ): Response> - @RetryPolicy @POST("/login-service/v1/login") suspend fun userLogin( diff --git a/android/app/src/main/java/com/naviapp/registration/OtpFragment.kt b/android/app/src/main/java/com/naviapp/registration/OtpFragment.kt index 0db4c91ee4..56f1274606 100644 --- a/android/app/src/main/java/com/naviapp/registration/OtpFragment.kt +++ b/android/app/src/main/java/com/naviapp/registration/OtpFragment.kt @@ -22,13 +22,18 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.google.android.gms.auth.api.phone.SmsRetriever import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.sharedpref.PreferenceManager import com.navi.base.utils.BaseUtils import com.navi.base.utils.isNotNullAndNotEmpty import com.navi.base.utils.orFalse import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.ENABLE_LOGIN_OTP_SENDER_VERIFICATION +import com.navi.common.network.models.LoginType +import com.navi.common.network.models.VerifyOtpRequest import com.navi.common.ui.fragment.BaseFragment +import com.navi.common.utils.Constants.AUTO_READ_OTP_CONSENT_KEY import com.navi.common.utils.Constants.LOGIN_SOURCE +import com.navi.common.utils.generateSenderNames import com.navi.common.utils.hideKeyboard import com.navi.common.utils.observeNonNull import com.navi.design.R as DesignR @@ -41,12 +46,9 @@ import com.naviapp.common.helper.ReferralHelper import com.naviapp.dashboard.listeners.FragmentInteractionListener import com.naviapp.databinding.OtpFragmentBinding import com.naviapp.models.request.AuthDetails -import com.naviapp.models.request.LoginType import com.naviapp.models.request.UserLoginRequest -import com.naviapp.models.request.VerifyOtpRequest import com.naviapp.network.ApiErrorTagType import com.naviapp.registration.RegistrationActivity.Companion.LOGIN_SCREEN -import com.naviapp.registration.helper.generateSenderNames import com.naviapp.registration.listeners.LoginListener import com.naviapp.registration.viewmodel.RegistrationSharedVM import com.naviapp.registration.viewmodel.RegistrationVM @@ -360,6 +362,10 @@ class OtpFragment : BaseFragment(), View.OnClickListener { } private fun verifyOtp(isAutoFetchOtp: Boolean = false) { + PreferenceManager.setBooleanSecurely( + key = AUTO_READ_OTP_CONSENT_KEY, + value = isAutoFetchOtp, + ) otpAutofill = isAutoFetchOtp if ( System.currentTimeMillis() - apiCallLastTime <= 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 5337b42465..f102f90565 100644 --- a/android/app/src/main/java/com/naviapp/registration/RegistrationActivity.kt +++ b/android/app/src/main/java/com/naviapp/registration/RegistrationActivity.kt @@ -36,6 +36,7 @@ import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.ENABLE_LOGIN_OTP_SENDER_VERIFICATION import com.navi.common.model.AppUpgradeResponse import com.navi.common.model.ModuleNameV2 +import com.navi.common.network.models.LoginType import com.navi.common.receiver.OtpReceiveListener import com.navi.common.receiver.SmsAutoReadReceiver import com.navi.common.receiver.SmsAutoReadWithConsentReceiver @@ -43,12 +44,14 @@ import com.navi.common.ui.activity.BaseActivity import com.navi.common.utils.Constants.LOGIN_SOURCE import com.navi.common.utils.Constants.ScreenLockConstants.LOGIN_SESSION_ID import com.navi.common.utils.FirebaseAuthHelper +import com.navi.common.utils.SenderVerificationStatus import com.navi.common.utils.TemporaryStorageHelper import com.navi.common.utils.getScreenRefreshRate import com.navi.common.utils.getSessionId import com.navi.common.utils.log import com.navi.common.utils.observeNonNull import com.navi.common.utils.observeNullable +import com.navi.common.utils.parseCode import com.navi.common.utils.registerReceiverWithVersionCheck import com.naviapp.R import com.naviapp.analytics.utils.NaviAnalytics @@ -63,13 +66,10 @@ import com.naviapp.home.reducer.HpEffects import com.naviapp.home.viewmodel.HomeViewModel import com.naviapp.models.TruecallerAuthData import com.naviapp.models.request.AuthDetails -import com.naviapp.models.request.LoginType import com.naviapp.models.request.UserLoginRequest import com.naviapp.models.response.LoginResponse import com.naviapp.pushnotification.NotificationHandler -import com.naviapp.registration.helper.SenderVerificationStatus import com.naviapp.registration.helper.TrueCallerFacade -import com.naviapp.registration.helper.parseCode import com.naviapp.registration.listeners.LoginListener import com.naviapp.registration.usecase.LoginDeeplinkAndRedirectionHelper import com.naviapp.registration.viewmodel.ConfigVM diff --git a/android/app/src/main/java/com/naviapp/registration/repositories/RegisterRepository.kt b/android/app/src/main/java/com/naviapp/registration/repositories/RegisterRepository.kt index 114751dd29..cd855e7752 100644 --- a/android/app/src/main/java/com/naviapp/registration/repositories/RegisterRepository.kt +++ b/android/app/src/main/java/com/naviapp/registration/repositories/RegisterRepository.kt @@ -10,42 +10,17 @@ package com.naviapp.registration.repositories import com.navi.common.checkmate.model.MetricInfo import com.navi.common.model.CommunicationAppLaunchData import com.navi.common.model.DeviceDetail +import com.navi.common.network.models.TenantId import com.navi.common.network.models.isSuccessWithData -import com.naviapp.analytics.utils.NaviAnalytics -import com.naviapp.models.request.GenerateOtpRequest import com.naviapp.models.request.OnboardingRequest import com.naviapp.models.request.SaphyraRequestData -import com.naviapp.models.request.TenantId import com.naviapp.models.request.UserLoginRequest -import com.naviapp.models.request.VerifyOtpRequest import com.naviapp.network.retrofit.ResponseCallback import com.naviapp.utils.retrofitService import com.naviapp.utils.superAppRetrofitService class RegisterRepository : ResponseCallback() { - suspend fun submitPhoneNumber(generateOtpRequest: GenerateOtpRequest) = - apiResponseCallback( - superAppRetrofitService() - .submitPhoneNumber(generateOtpRequest, TenantId.LOGIN_ANDROID.name), - metricInfo = - MetricInfo.AppMetric( - screen = NaviAnalytics.REGISTRATION, - isNae = { !it.isSuccessWithData() }, - ), - ) - - suspend fun verifyOtp(verifyOtpRequest: VerifyOtpRequest, screenName: String) = - apiResponseCallback( - superAppRetrofitService() - .submitOTP( - verifyOtpRequest = verifyOtpRequest, - tenantId = TenantId.LOGIN_ANDROID.name, - ), - metricInfo = - MetricInfo.AppMetric(screen = screenName, isNae = { !it.isSuccessWithData() }), - ) - suspend fun userLogin(userLoginRequest: UserLoginRequest, screenName: String) = apiResponseCallback( superAppRetrofitService() diff --git a/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationSharedVM.kt b/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationSharedVM.kt index 353d90edc8..e081900640 100644 --- a/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationSharedVM.kt +++ b/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationSharedVM.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2020-2024 by Navi Technologies Limited + * * Copyright © 2020-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -10,7 +10,7 @@ package com.naviapp.registration.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.naviapp.registration.helper.SenderVerificationStatus +import com.navi.common.utils.SenderVerificationStatus class RegistrationSharedVM : ViewModel() { diff --git a/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationVM.kt b/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationVM.kt index 7729e3aa58..4f75c64cb0 100644 --- a/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationVM.kt +++ b/android/app/src/main/java/com/naviapp/registration/viewmodel/RegistrationVM.kt @@ -16,18 +16,23 @@ import com.navi.base.model.CtaData import com.navi.base.utils.BaseUtils import com.navi.base.utils.FAILURE import com.navi.base.utils.orFalse +import com.navi.common.checkmate.model.MetricInfo import com.navi.common.network.authenticator.TokenAuthenticator +import com.navi.common.network.models.GenerateOtpRequest +import com.navi.common.network.models.GenerateOtpResponse +import com.navi.common.network.models.LoginOtpVerifyResponse +import com.navi.common.network.models.LoginType +import com.navi.common.network.models.TenantId +import com.navi.common.network.models.VerifyOtpRequest +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.repo.NaviCommonRepository import com.navi.common.utils.CommonNaviAnalytics import com.navi.common.utils.isValidResponse import com.navi.common.viewmodel.BaseVM -import com.naviapp.models.request.GenerateOtpRequest -import com.naviapp.models.request.LoginType +import com.naviapp.analytics.utils.NaviAnalytics import com.naviapp.models.request.OnboardingActionType import com.naviapp.models.request.OnboardingRequest import com.naviapp.models.request.UserLoginRequest -import com.naviapp.models.request.VerifyOtpRequest -import com.naviapp.models.response.GenerateOtpResponse -import com.naviapp.models.response.LoginOtpVerifyResponse import com.naviapp.models.response.LoginResponse import com.naviapp.models.response.OnboardingDeeplinkResponse import com.naviapp.network.ApiConstants.API_EXPIRED_OTP @@ -43,8 +48,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber -class RegistrationVM(private val registerRepository: RegisterRepository = RegisterRepository()) : - BaseVM() { +class RegistrationVM( + private val registerRepository: RegisterRepository = RegisterRepository(), + private val naviCommonRepository: NaviCommonRepository = NaviCommonRepository(), +) : BaseVM() { private val _otpTokenAndPhone = MutableLiveData() val otpTokenAndPhone: LiveData @@ -86,7 +93,16 @@ class RegistrationVM(private val registerRepository: RegisterRepository = Regist isRecipientEdited = isPhoneNumberEdited, ) viewModelScope.launch(Dispatchers.IO) { - val response = registerRepository.submitPhoneNumber(generateOtpRequest) + val response = + naviCommonRepository.submitPhoneNumber( + generateOtpRequest = generateOtpRequest, + tenantId = TenantId.LOGIN_ANDROID.name, + metricInfo = + MetricInfo.AppMetric( + screen = NaviAnalytics.REGISTRATION, + isNae = { !it.isSuccessWithData() }, + ), + ) Timber.i("login data $response") when { response.error == null -> { @@ -111,9 +127,14 @@ class RegistrationVM(private val registerRepository: RegisterRepository = Regist fun verifyOtp(verifyOtpRequest: VerifyOtpRequest, screenName: String) { viewModelScope.launch(Dispatchers.IO) { val response = - registerRepository.verifyOtp( + naviCommonRepository.verifyOtp( verifyOtpRequest = verifyOtpRequest, - screenName = screenName, + tenantId = TenantId.LOGIN_ANDROID.name, + metricInfo = + MetricInfo.AppMetric( + screen = screenName, + isNae = { !it.isSuccessWithData() }, + ), ) isOtpVerifyInProgress.set(false) Timber.i("otp data $response") diff --git a/android/app/src/main/java/com/naviapp/utils/Constants.kt b/android/app/src/main/java/com/naviapp/utils/Constants.kt index 0ec49aebae..e77788ec8b 100644 --- a/android/app/src/main/java/com/naviapp/utils/Constants.kt +++ b/android/app/src/main/java/com/naviapp/utils/Constants.kt @@ -226,7 +226,6 @@ object Constants { const val PAGER_SCROLL = "pager_scroll" const val STATUS = "status" const val HTTP_REGEX = "^https?://" - const val NAVIHQ = "NAVIHQ" const val LOAN_TAB_OFFER_NUDGE_APPEARED_COUNT = "LOAN_TAB_OFFER_NUDGE_APPEARED_COUNT" const val BLOCK_NOTIFICATION = "blockNotification" @@ -280,8 +279,4 @@ object Constants { enum class FlowType(val type: String) { PART_PRE_PAYMENT("PART_PRE_PAYMENT") } - - object RegistrationConstants { - const val SENDER_NAME_SUFFIX = "-NAVIHQ" - } } diff --git a/android/app/src/main/java/com/naviapp/utils/LoginUtils.kt b/android/app/src/main/java/com/naviapp/utils/LoginUtils.kt index 9ab1e2a21e..46336570ab 100644 --- a/android/app/src/main/java/com/naviapp/utils/LoginUtils.kt +++ b/android/app/src/main/java/com/naviapp/utils/LoginUtils.kt @@ -12,6 +12,7 @@ import com.navi.base.utils.NaviNetworkConnectivityImpl import com.navi.common.utils.CommonFraudUtil import com.navi.common.utils.CommonRootDeviceUtil import com.navi.common.utils.Constants +import com.navi.common.utils.SenderVerificationStatus import com.navi.common.utils.deviceId import com.navi.common.utils.fetchInstallerName import com.navi.common.utils.getLocalStorageLocation @@ -20,7 +21,6 @@ import com.navi.common.utils.isInstalledInProfile import com.navi.common.utils.isLastLocationMocked import com.navi.guarddog.datamanagement.securityManager.SecurityManager import com.naviapp.models.request.LoginDeviceDetails -import com.naviapp.registration.helper.SenderVerificationStatus fun getLoginDeviceUtils( context: Context, diff --git a/android/navi-base/src/main/java/com/navi/base/sharedpref/PreferenceManager.kt b/android/navi-base/src/main/java/com/navi/base/sharedpref/PreferenceManager.kt index 83b6ac28c9..14f942ed2c 100644 --- a/android/navi-base/src/main/java/com/navi/base/sharedpref/PreferenceManager.kt +++ b/android/navi-base/src/main/java/com/navi/base/sharedpref/PreferenceManager.kt @@ -291,6 +291,20 @@ object PreferenceManager { } else setIntPreference(key, value) } + fun setBooleanSecurely(key: String, value: Boolean) { + if (shouldUseEncryptedSharedPref()) { + val editor = secureSharedPreferencesForSession?.edit() + editor?.putBoolean(key, value) + editor?.apply() + } else setBooleanPreference(key, value) + } + + fun getBooleanSecurely(key: String, defValue: Boolean = false): Boolean { + return if (shouldUseEncryptedSharedPref()) { + secureSharedPreferencesForSession?.getBoolean(key, defValue) ?: defValue + } else getBooleanPreference(key, defValue) + } + fun getSecureString(key: String): String? { return if (shouldUseEncryptedSharedPref()) { secureSharedPreferencesForSession?.getString(key, null) 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 136c771a53..b1eae184d5 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 @@ -138,6 +138,7 @@ object FirebaseRemoteConfigHelper { "NAVI_PAY_PPS_UPI_LITE_BANNER_ENABLED_FLAG" const val NAVI_PAY_SEND_MONEY_PRE_VALIDATION_ENABLED = "NAVI_PAY_SEND_MONEY_PRE_VALIDATION_ENABLED" + const val NAVI_PAY_AUTO_READ_OTP_DISABLED = "NAVI_PAY_AUTO_READ_OTP_DISABLED" // COMMON const val LITMUS_EXPERIMENTS_CACHE_DURATION_IN_MILLIS = diff --git a/android/navi-common/src/main/java/com/navi/common/lottie/LottieRepository.kt b/android/navi-common/src/main/java/com/navi/common/lottie/LottieRepository.kt index d099c78975..ecba68bf19 100644 --- a/android/navi-common/src/main/java/com/navi/common/lottie/LottieRepository.kt +++ b/android/navi-common/src/main/java/com/navi/common/lottie/LottieRepository.kt @@ -10,6 +10,7 @@ package com.navi.common.lottie object LottieRepository { const val ARC_EXPLANATORY_LOTTIE_URL = "ARC_EXPLANATORY_LOTTIE_URL" const val LITE_MASTHEAD_LOTTIE_URL = "LITE_MASTHEAD_LOTTIE_URL" + const val AUTO_READ_OTP_LOTTIE_URL = "AUTO_READ_OTP_LOTTIE_URL" private val lottieUrlMap = mapOf( @@ -17,6 +18,8 @@ object LottieRepository { "https://public-assets.prod.navi-pay.in/lotties/arc_explanation.lottie", LITE_MASTHEAD_LOTTIE_URL to "https://public-assets.prod.navi-sa.in/navi-pay/lottie/navi-pay-lite-masthead.lottie", + AUTO_READ_OTP_LOTTIE_URL to + "https://public-assets.prod.navi-pay.in/lotties/navi-pay-auto-read.lottie", ) fun getLottieUrl(lottieName: String): String { diff --git a/android/app/src/main/java/com/naviapp/models/request/GenerateOtpRequest.kt b/android/navi-common/src/main/java/com/navi/common/network/models/GenerateOtpRequest.kt similarity index 82% rename from android/app/src/main/java/com/naviapp/models/request/GenerateOtpRequest.kt rename to android/navi-common/src/main/java/com/navi/common/network/models/GenerateOtpRequest.kt index cfdaefeece..1844dd21af 100644 --- a/android/app/src/main/java/com/naviapp/models/request/GenerateOtpRequest.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/models/GenerateOtpRequest.kt @@ -5,7 +5,7 @@ * */ -package com.naviapp.models.request +package com.navi.common.network.models data class GenerateOtpRequest( val recipient: String?, @@ -15,7 +15,8 @@ data class GenerateOtpRequest( ) enum class TenantId { - LOGIN_ANDROID + LOGIN_ANDROID, + UPI_AUTO_READ_OTP, } enum class LoginType { diff --git a/android/app/src/main/java/com/naviapp/models/response/GenerateOtpResponse.kt b/android/navi-common/src/main/java/com/navi/common/network/models/GenerateOtpResponse.kt similarity index 92% rename from android/app/src/main/java/com/naviapp/models/response/GenerateOtpResponse.kt rename to android/navi-common/src/main/java/com/navi/common/network/models/GenerateOtpResponse.kt index bc8461638d..831db58bc5 100644 --- a/android/app/src/main/java/com/naviapp/models/response/GenerateOtpResponse.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/models/GenerateOtpResponse.kt @@ -5,7 +5,7 @@ * */ -package com.naviapp.models.response +package com.navi.common.network.models import com.google.gson.annotations.SerializedName diff --git a/android/app/src/main/java/com/naviapp/models/response/LoginOtpVerifyResponse.kt b/android/navi-common/src/main/java/com/navi/common/network/models/LoginOtpVerifyResponse.kt similarity index 74% rename from android/app/src/main/java/com/naviapp/models/response/LoginOtpVerifyResponse.kt rename to android/navi-common/src/main/java/com/navi/common/network/models/LoginOtpVerifyResponse.kt index 26d95fdcd2..d3d66da69a 100644 --- a/android/app/src/main/java/com/naviapp/models/response/LoginOtpVerifyResponse.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/models/LoginOtpVerifyResponse.kt @@ -1,11 +1,11 @@ /* * - * * Copyright © 2019-2024 by Navi Technologies Limited + * * Copyright © 2019-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ -package com.naviapp.models.response +package com.navi.common.network.models import com.google.gson.annotations.SerializedName diff --git a/android/app/src/main/java/com/naviapp/models/request/VerifyOtpRequest.kt b/android/navi-common/src/main/java/com/navi/common/network/models/VerifyOtpRequest.kt similarity index 72% rename from android/app/src/main/java/com/naviapp/models/request/VerifyOtpRequest.kt rename to android/navi-common/src/main/java/com/navi/common/network/models/VerifyOtpRequest.kt index 43bbf24ed4..864c37dd29 100644 --- a/android/app/src/main/java/com/naviapp/models/request/VerifyOtpRequest.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/models/VerifyOtpRequest.kt @@ -1,11 +1,11 @@ /* * - * * Copyright © 2019-2024 by Navi Technologies Limited + * * Copyright © 2019-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ -package com.naviapp.models.request +package com.navi.common.network.models import com.google.gson.annotations.SerializedName diff --git a/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt b/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt index e369364cb1..1ba1f1801e 100644 --- a/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt @@ -30,12 +30,16 @@ import com.navi.common.model.SubmitPermissionResponse import com.navi.common.model.UploadDataAsyncResponse import com.navi.common.network.BaseHttpClient import com.navi.common.network.models.FirebaseAuthTokenResponse +import com.navi.common.network.models.GenerateOtpRequest +import com.navi.common.network.models.GenerateOtpResponse import com.navi.common.network.models.GenericResponse import com.navi.common.network.models.LitmusExperimentResponse import com.navi.common.network.models.LitmusExperimentsRequest +import com.navi.common.network.models.LoginOtpVerifyResponse import com.navi.common.network.models.RedirectionAuthTokenRequest import com.navi.common.network.models.RedirectionAuthTokenResponse import com.navi.common.network.models.SuccessResponse +import com.navi.common.network.models.VerifyOtpRequest import com.navi.common.network.retry.annotations.RetryPolicy import com.navi.common.payments.arc.model.network.ArcExactOfferRequest import com.navi.common.payments.arc.model.network.ArcExactOfferResponse @@ -44,6 +48,7 @@ import com.navi.common.useruploaddata.model.DataIngestionUploadResponse import com.navi.common.useruploaddata.model.IngestionStatusBody import com.navi.common.useruploaddata.model.PreSignedUrlListResponse import com.navi.common.utils.Constants.CDS +import com.navi.common.utils.Constants.HEADER_X_TENANT_ID import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Response @@ -212,4 +217,16 @@ interface RetrofitService { @Header(X_TARGET) xTarget: String = GRATIFICATION_SERVICE, @Body arcExactOfferRequest: ArcExactOfferRequest, ): Response> + + @POST("/otp/v1/generate") + suspend fun submitPhoneNumber( + @Body generateOtpRequest: GenerateOtpRequest, + @Header(HEADER_X_TENANT_ID) tenantId: String, + ): Response> + + @POST("/otp/v1/verify") + suspend fun submitOtp( + @Body verifyOtpRequest: VerifyOtpRequest, + @Header(HEADER_X_TENANT_ID) tenantId: String, + ): Response> } diff --git a/android/navi-common/src/main/java/com/navi/common/repo/NaviCommonRepository.kt b/android/navi-common/src/main/java/com/navi/common/repo/NaviCommonRepository.kt index 2ab3b2e8bd..5e3a3c5bf3 100644 --- a/android/navi-common/src/main/java/com/navi/common/repo/NaviCommonRepository.kt +++ b/android/navi-common/src/main/java/com/navi/common/repo/NaviCommonRepository.kt @@ -9,8 +9,12 @@ package com.navi.common.repo import com.navi.common.checkmate.model.MetricInfo import com.navi.common.model.ModuleName +import com.navi.common.network.models.GenerateOtpRequest +import com.navi.common.network.models.GenerateOtpResponse import com.navi.common.network.models.LitmusExperimentsRequest +import com.navi.common.network.models.LoginOtpVerifyResponse import com.navi.common.network.models.RepoResult +import com.navi.common.network.models.VerifyOtpRequest import com.navi.common.network.retrofit.ResponseCallback import com.navi.common.payments.arc.model.network.ArcNudgeResponse import com.navi.common.utils.retrofitService @@ -30,4 +34,33 @@ class NaviCommonRepository @Inject constructor() : ResponseCallback() { metricInfo = metricInfo, ) } + + suspend fun submitPhoneNumber( + generateOtpRequest: GenerateOtpRequest, + tenantId: String, + metricInfo: MetricInfo>, + ): RepoResult { + return apiResponseCallback( + response = + retrofitService() + .submitPhoneNumber( + generateOtpRequest = generateOtpRequest, + tenantId = tenantId, + ), + metricInfo = metricInfo, + ) + } + + suspend fun verifyOtp( + verifyOtpRequest: VerifyOtpRequest, + tenantId: String, + metricInfo: MetricInfo>, + ): RepoResult { + return apiResponseCallback( + response = + retrofitService() + .submitOtp(verifyOtpRequest = verifyOtpRequest, tenantId = tenantId), + metricInfo = metricInfo, + ) + } } diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt index abde0414c2..a5925dddba 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt @@ -285,6 +285,7 @@ object Constants { // Navi Common DB cache keys const val NAVI_BBPS_REWARDS_NUDGE_CACHE_KEY = "naviBbpsRewardsNudge" const val KEY_UPI_PRIMARY_ACCOUNT_VPA = "upiPrimaryAccountVpa" + const val AUTO_READ_OTP_CONSENT_KEY = "autoReadOtpConsent" // GENERIC const val AM_SMALL = "am" @@ -315,6 +316,7 @@ object Constants { const val RESOURCE_DOWNLOAD_BUCKET = "resource_download_bucket" const val BUCKET_SIZE = "bucket_size" const val LOGIN_SMS_CONSENT_BOTTOM_SHEET_VISIBLE = "login_sms_consent_bottom_sheet_visible" + const val NAVIHQ = "NAVIHQ" // source of geocoding const val SOURCE_ANDROID_GEOCODER_API = "ANDROID_GEOCODER_API" @@ -377,4 +379,8 @@ object Constants { const val FLOW_PERMISSION = "PERMISSION" const val FLOW_POST_PURCHASE = "POST_PURCHASE" + + object RegistrationConstants { + const val SENDER_NAME_SUFFIX = "-NAVIHQ" + } } diff --git a/android/app/src/main/java/com/naviapp/registration/helper/SmsAutoReadHelper.kt b/android/navi-common/src/main/java/com/navi/common/utils/SmsAutoReadHelper.kt similarity index 84% rename from android/app/src/main/java/com/naviapp/registration/helper/SmsAutoReadHelper.kt rename to android/navi-common/src/main/java/com/navi/common/utils/SmsAutoReadHelper.kt index c373b658ea..68db95bf0a 100644 --- a/android/app/src/main/java/com/naviapp/registration/helper/SmsAutoReadHelper.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/SmsAutoReadHelper.kt @@ -5,13 +5,12 @@ * */ -package com.naviapp.registration.helper +package com.navi.common.utils import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.LOGIN_OTP_SENDER_ID -import com.navi.common.utils.log -import com.naviapp.utils.Constants.NAVIHQ -import com.naviapp.utils.Constants.RegistrationConstants.SENDER_NAME_SUFFIX +import com.navi.common.utils.Constants.NAVIHQ +import com.navi.common.utils.Constants.RegistrationConstants.SENDER_NAME_SUFFIX import java.util.regex.Pattern fun parseCode(message: String?): String? { diff --git a/android/navi-pay/src/main/assets/navi-pay-auto-read.lottie b/android/navi-pay/src/main/assets/navi-pay-auto-read.lottie new file mode 100644 index 0000000000..71c707f0bc Binary files /dev/null and b/android/navi-pay/src/main/assets/navi-pay-auto-read.lottie differ 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 5b6540dd21..35aa6f0929 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 @@ -650,11 +650,8 @@ class NaviPayAnalytics private constructor() { ) } - fun onNonVerifiedSenderLogin(error: NaviPayErrorConfig) { - NaviTrackEvent.trackEventOnClickStream( - eventName = "NaviPay_Dev_NonVerifiedSenderLogin", - eventValues = mapOf("error" to error.toString()), - ) + fun onNonVerifiedSenderLogin() { + NaviTrackEvent.trackEventOnClickStream(eventName = "NaviPay_Dev_NonVerifiedSenderLogin") } fun onNonVerifiedSenderLoginBottomSheetClicked( @@ -715,6 +712,146 @@ class NaviPayAnalytics private constructor() { fun onSmvInitiated() { NaviTrackEvent.trackEventOnClickStream("NaviPay_SmvInitiated") } + + fun onOtpAutoReadCtaClicked( + provider: NetworkProvider, + naviPaySessionAttributes: Map? = null, + ctaAction: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_Setup_VerifyNumber_Screen_OTP_AutoRead_Screen_CTA_Clicked", + mapOf( + Pair("simProvider", provider.name), + Pair("simId", provider.ssid), + Pair( + "naviPayOnboardingSessionId", + naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + ), + Pair("isFromAutoReadScreen", "true"), + Pair("Action", ctaAction), + ), + ) + } + + fun onAutoReadOtpConsentDeniedBottomSheetLanded( + provider: NetworkProvider, + naviPaySessionAttributes: Map? = null, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_Setup_OTPVerification_AutoReadOTP_ConsentDenied_BottomSheet_Landed", + mapOf( + Pair("simProvider", provider.name), + Pair("simId", provider.ssid), + Pair( + "naviPayOnboardingSessionId", + naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + ), + Pair("isFromAutoReadScreen", "true"), + ), + ) + } + + fun onResendOtpClicked( + provider: NetworkProvider, + naviPaySessionAttributes: Map? = null, + otpVerificationState: String, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_Setup_OTPVerification_ResendOTP_Clicked", + mapOf( + Pair("simProvider", provider.name), + Pair("simId", provider.ssid), + Pair( + "naviPayOnboardingSessionId", + naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + ), + Pair("otpVerificationState", otpVerificationState), + ), + ) + } + + fun onOtpAutoReadTimeout( + provider: NetworkProvider, + naviPaySessionAttributes: Map? = null, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_Setup_OTPVerification_TimedOut_BottomSheet_Landed", + mapOf( + Pair("simProvider", provider.name), + Pair("simId", provider.ssid), + Pair( + "naviPayOnboardingSessionId", + naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + ), + Pair("isFromAutoReadScreen", "true"), + ), + ) + } + + fun onSmsConsentBottomSheetVisible( + onboardingSource: String? = null, + provider: NetworkProvider, + naviPaySessionAttributes: Map? = null, + ) { + NaviTrackEvent.trackEvent( + eventName = "NaviPay_Setup_AutoReadOTP_SmsConsent_BottomSheet_Visible", + eventValues = + mapOf( + "naviPayOnboardingSource" to onboardingSource.orEmpty(), + "simProvider" to provider.name, + "simId" to provider.ssid, + "isFromAutoReadScreen" to "true", + "naviPayOnboardingSessionId" to + naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + ), + ) + } + + fun onOtpAutoReadVerificationFailed( + provider: NetworkProvider, + naviPaySessionAttributes: Map? = null, + isOtpValidationAttemptExhausted: Boolean, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_Setup_OTPVerification_Failed_BottomSheet_Landed", + mapOf( + Pair("simProvider", provider.name), + Pair("simId", provider.ssid), + Pair( + "naviPayOnboardingSessionId", + naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + ), + Pair("isFromAutoReadScreen", "true"), + Pair( + "isOtpValidationAttemptExhausted", + isOtpValidationAttemptExhausted.toString(), + ), + ), + ) + } + + fun onOtpAutoReadGenerationFailed( + provider: NetworkProvider, + naviPaySessionAttributes: Map? = null, + isOtpGenerationAttemptExhausted: Boolean, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_Setup_OTPGeneration_Failed_BottomSheet_Landed", + mapOf( + Pair("simProvider", provider.name), + Pair("simId", provider.ssid), + Pair( + "naviPayOnboardingSessionId", + naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + ), + Pair("isFromAutoReadScreen", "true"), + Pair( + "isOtpGenerationAttemptExhausted", + isOtpGenerationAttemptExhausted.toString(), + ), + ), + ) + } } inner class NaviPayPermission { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/AutoReadOtpUiState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/AutoReadOtpUiState.kt new file mode 100644 index 0000000000..09c45b4fcf --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/model/view/AutoReadOtpUiState.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.common.model.view + +enum class AutoReadOtpVerificationState { + DEFAULT, + VERIFYING, + FAILED, + TIMEOUT, + DENIED, +} + +data class AutoReadOtpBottomSheetProperties( + val iconResId: Int, + val titleResId: Int, + val descriptionResId: Int, +) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/repository/SharedPreferenceRepository.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/repository/SharedPreferenceRepository.kt index a1444c6bea..caf28460b7 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/repository/SharedPreferenceRepository.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/repository/SharedPreferenceRepository.kt @@ -41,6 +41,16 @@ class SharedPreferenceRepository { PreferenceManager.getBooleanPreference(key = key, defValue = defValue) } + suspend fun setBooleanValueSecurely(key: String, value: Boolean) = + withContext(Dispatchers.IO) { + PreferenceManager.setBooleanSecurely(key = key, value = value) + } + + suspend fun getBooleanValueSecurely(key: String, defValue: Boolean = false) = + withContext(Dispatchers.IO) { + PreferenceManager.getBooleanSecurely(key = key, defValue = defValue) + } + suspend fun clearKeyBasedSessionPreferenceData( encryptedDataKeys: List, nonEncryptedDataKeys: List, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/ui/NaviPayAutoReadOtpBottomSheet.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/ui/NaviPayAutoReadOtpBottomSheet.kt new file mode 100644 index 0000000000..84b26d1800 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/ui/NaviPayAutoReadOtpBottomSheet.kt @@ -0,0 +1,393 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.common.ui + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.R as CommonR +import com.navi.common.lottie.LottieRepository +import com.navi.common.model.NaviLottieCompositionSpecType +import com.navi.common.utils.SPACE +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.naviwidgets.R as WidgetsR +import com.navi.naviwidgets.extensions.NaviText +import com.navi.pay.R +import com.navi.pay.common.model.view.AutoReadOtpBottomSheetProperties +import com.navi.pay.common.model.view.AutoReadOtpVerificationState +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.utils.HYPHEN +import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE +import com.navi.pay.utils.noRippleClickableWithDebounce + +@Composable +fun NaviPayAutoReadOtpBottomSheet( + modifier: Modifier = Modifier, + timeOutInSeconds: Int, + generatedOtp: String, + showButtonLoader: Boolean, + onResendOtpClicked: () -> Unit, + onReVerifyOtpClicked: () -> Unit, + autoReadOtpVerificationState: AutoReadOtpVerificationState, +) { + val stateProperties = + remember(autoReadOtpVerificationState) { + getAutoReadOtpVerificationStateProperties( + autoReadOtpVerificationState = autoReadOtpVerificationState + ) + } + + Column(modifier = modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp)) { + IconHeader(iconResId = stateProperties.iconResId) + + Spacer(modifier = Modifier.size(8.dp)) + + TitleText(titleResId = stateProperties.titleResId) + + Spacer(modifier = Modifier.size(8.dp)) + + DescriptionText(descriptionResId = stateProperties.descriptionResId) + + Spacer(modifier = Modifier.size(16.dp)) + + if (autoReadOtpVerificationState != AutoReadOtpVerificationState.DENIED) { + RenderOtpBoxes( + modifier = Modifier.fillMaxWidth(), + generatedOtp = generatedOtp, + autoReadOtpVerificationState = autoReadOtpVerificationState, + ) + } + + Spacer(modifier = Modifier.size(16.dp)) + + RenderAutoReadOtpStatesContent( + autoReadOtpVerificationState = autoReadOtpVerificationState, + timeOutInSeconds = timeOutInSeconds, + onResendOtpClicked = onResendOtpClicked, + onReVerifyOtpClicked = onReVerifyOtpClicked, + showButtonLoader = showButtonLoader, + ) + } +} + +@Composable +private fun RenderAutoReadOtpStatesContent( + autoReadOtpVerificationState: AutoReadOtpVerificationState, + timeOutInSeconds: Int, + showButtonLoader: Boolean, + onResendOtpClicked: () -> Unit, + onReVerifyOtpClicked: () -> Unit, +) { + when (autoReadOtpVerificationState) { + AutoReadOtpVerificationState.DEFAULT -> + RenderDefaultState(timeOutInSeconds = timeOutInSeconds) + + AutoReadOtpVerificationState.VERIFYING -> RenderVerifyingState() + + AutoReadOtpVerificationState.TIMEOUT, + AutoReadOtpVerificationState.FAILED -> + RenderTimeOutOrFailedState(onResendOtpClicked = onResendOtpClicked) + + AutoReadOtpVerificationState.DENIED -> { + LoaderRoundedButton( + text = stringResource(id = R.string.np_restart_otp_verification_cta), + modifier = Modifier.fillMaxWidth(), + onClick = onReVerifyOtpClicked, + enabled = !showButtonLoader, + showLoader = showButtonLoader, + lottieFileName = NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE, + ) + } + } +} + +@Composable +private fun RenderTimeOutOrFailedState(onResendOtpClicked: () -> Unit) { + RenderResendOtpStrip( + modifier = Modifier.wrapContentWidth(), + onResendOtpClicked = onResendOtpClicked, + ) + + Spacer(modifier = Modifier.size(32.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier.size(250.dp), + painter = painterResource(id = R.drawable.ic_auto_read_still_frame), + contentDescription = null, + ) + } +} + +@Composable +private fun RenderVerifyingState() { + NaviPayLottieAnimation( + modifier = Modifier.size(24.dp), + lottieFileName = NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE, + showLottieInfiniteTimes = true, + ) + + Spacer(modifier = Modifier.size(25.dp)) + + RenderAutoReadOtpLottie(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) +} + +@Composable +private fun RenderDefaultState(timeOutInSeconds: Int) { + NaviText( + text = + buildAnnotatedString { + withStyle( + style = + SpanStyle( + fontSize = 12.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.textPrimary, + ) + ) { + append(text = stringResource(id = R.string.np_resend_otp_in) + SPACE) + } + withStyle( + style = + SpanStyle( + fontSize = 12.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + color = NaviPayColor.textPrimary, + ) + ) { + append("00 : ${timeOutInSeconds.toString().padStart(2, '0')}") + } + } + ) + + Spacer(modifier = Modifier.size(32.dp)) + + RenderAutoReadOtpLottie(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) +} + +@Composable +private fun RenderAutoReadOtpLottie(modifier: Modifier = Modifier) { + var isLottieLoaded by remember { mutableStateOf(false) } + + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Image( + painter = painterResource(R.drawable.ic_auto_read_still_frame), + contentDescription = null, + modifier = Modifier.alpha(if (isLottieLoaded) 0f else 1f), + contentScale = ContentScale.FillWidth, + ) + + NaviPayLottieAnimation( + modifier = Modifier.size(250.dp), + lottieFileName = + LottieRepository.getLottieUrl(LottieRepository.AUTO_READ_OTP_LOTTIE_URL), + lottieCompositionSpecType = NaviLottieCompositionSpecType.Url, + showLottieInfiniteTimes = true, + onAnimationStart = { isLottieLoaded = true }, + contentScale = ContentScale.FillWidth, + ) + } +} + +@Composable +private fun IconHeader(@DrawableRes iconResId: Int) { + Image( + painter = painterResource(iconResId), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) +} + +@Composable +private fun TitleText(@StringRes titleResId: Int) { + NaviText( + text = stringResource(id = titleResId), + fontSize = 16.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviPayColor.textPrimary, + lineHeight = 24.sp, + ) +} + +@Composable +private fun DescriptionText(@StringRes descriptionResId: Int) { + NaviText( + text = stringResource(id = descriptionResId), + fontSize = 14.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + color = NaviPayColor.textTertiary, + lineHeight = 22.sp, + ) +} + +@Composable +fun RenderResendOtpStrip(modifier: Modifier = Modifier, onResendOtpClicked: () -> Unit) { + Row( + modifier = modifier.noRippleClickableWithDebounce { onResendOtpClicked() }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Image( + painter = painterResource(id = WidgetsR.drawable.ic_resend_new_theme), + contentDescription = null, + modifier = Modifier.size(16.dp).rotate(degrees = 180f), + colorFilter = ColorFilter.tint(color = NaviPayColor.ctaPrimary), + ) + Spacer(modifier = Modifier.size(4.dp)) + NaviText( + text = stringResource(id = R.string.np_resend_otp), + fontSize = 12.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + color = NaviPayColor.ctaPrimary, + lineHeight = 16.sp, + textDecoration = TextDecoration.Underline, + ) + } +} + +@Composable +fun RenderOtpBoxes( + modifier: Modifier = Modifier, + generatedOtp: String, + autoReadOtpVerificationState: AutoReadOtpVerificationState, +) { + val boxBgColorAdjusted by + remember(autoReadOtpVerificationState) { + derivedStateOf { + if (autoReadOtpVerificationState == AutoReadOtpVerificationState.VERIFYING) { + NaviPayColor.bgNonEditable + } else { + NaviPayColor.bgDefault + } + } + } + + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) { + repeat(6) { index -> + Box( + modifier = + Modifier.weight(1f) + .aspectRatio(1f) + .border( + width = 1.dp, + shape = RoundedCornerShape(4.dp), + color = NaviPayColor.borderAlt, + ) + .background(color = boxBgColorAdjusted, shape = RoundedCornerShape(4.dp)), + contentAlignment = Alignment.BottomCenter, + ) { + NaviText( + text = generatedOtp.getOrNull(index)?.toString() ?: HYPHEN, + fontSize = 24.sp, + fontFamily = naviFontFamily, + fontWeight = + if (generatedOtp.isBlank()) { + getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR) + } else { + getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD) + }, + color = NaviPayColor.textTertiary, + lineHeight = 34.sp, + textAlign = TextAlign.Center, + modifier = + if (generatedOtp.isBlank()) { + Modifier.padding(vertical = 8.dp) + } else { + Modifier.padding(top = 4.dp, bottom = 10.dp) + }, + ) + } + } + } +} + +fun getAutoReadOtpVerificationStateProperties( + autoReadOtpVerificationState: AutoReadOtpVerificationState +): AutoReadOtpBottomSheetProperties { + return when (autoReadOtpVerificationState) { + AutoReadOtpVerificationState.DEFAULT -> + AutoReadOtpBottomSheetProperties( + iconResId = R.drawable.ic_np_read_otp, + titleResId = R.string.np_auto_read_otp_verification, + descriptionResId = R.string.np_auto_read_otp_verification_desc, + ) + + AutoReadOtpVerificationState.VERIFYING -> + AutoReadOtpBottomSheetProperties( + iconResId = R.drawable.ic_np_read_otp, + titleResId = R.string.np_verifying_otp, + descriptionResId = R.string.np_verifying_otp_desc, + ) + + AutoReadOtpVerificationState.TIMEOUT -> + AutoReadOtpBottomSheetProperties( + iconResId = CommonR.drawable.ic_exclamation_red_border, + titleResId = R.string.np_otp_verification_timeout, + descriptionResId = R.string.np_otp_verification_timeout_desc, + ) + + AutoReadOtpVerificationState.FAILED -> + AutoReadOtpBottomSheetProperties( + iconResId = CommonR.drawable.ic_exclamation_red_border, + titleResId = R.string.np_otp_verification_failed, + descriptionResId = R.string.np_otp_verification_failed_desc, + ) + + AutoReadOtpVerificationState.DENIED -> + AutoReadOtpBottomSheetProperties( + iconResId = R.drawable.ic_np_read_otp, + titleResId = R.string.np_restart_otp_verification_title, + descriptionResId = R.string.np_restart_otp_verification_desc, + ) + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/ui/SelectBankScreen.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/ui/SelectBankScreen.kt index aa286bd6ba..13f2e97e8b 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/ui/SelectBankScreen.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/account/add/ui/SelectBankScreen.kt @@ -164,7 +164,7 @@ fun SelectBankScreen( .padding(top = 16.dp, bottom = 8.dp) ) { Image( - painter = painterResource(id = R.drawable.ic_np_rupee_card), + painter = painterResource(id = R.drawable.ic_bank_default), contentDescription = "", modifier = Modifier.padding(horizontal = 16.dp).size(24.dp), ) 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 e13a2e3488..a685ae16fe 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 @@ -17,4 +17,5 @@ data class BindDeviceRequest( @SerializedName("merchantCustomerId") val merchantCustomerId: String, @SerializedName("deviceData") val deviceData: DeviceData, @SerializedName("bindingType") val bindingType: BindingType, + @SerializedName("isAutoOtpPermitted") val isAutoOtpPermitted: Boolean, ) 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 6bd69177d2..83d3f071f2 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 @@ -66,4 +66,15 @@ sealed class NaviPayOnboardingBottomSheetType { val secondaryButtonText: String, val actionType: NaviPayButtonAction?, ) : NaviPayOnboardingBottomSheetType() + + data object AutoReadOtp : NaviPayOnboardingBottomSheetType() + + data class OtpNotGeneratedError( + val title: String, + val description: String, + val primaryButtonText: String, + val secondaryButtonText: String, + ) : NaviPayOnboardingBottomSheetType() + + data object AutoReadOtpVerificationLimitReached : NaviPayOnboardingBottomSheetType() } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt index 6593883d3f..72a8c15e2e 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/repository/NaviPayOnboardingRepository.kt @@ -79,7 +79,7 @@ constructor( suspend fun getAllCustomerOnboardingData() = naviPayCustomerOnboardingDao.getAllCustomerOnboardingData() - suspend fun upsertCustomerOnboardingDataEntity( + private suspend fun upsertCustomerOnboardingDataEntity( naviPayCustomerOnboardingDataEntity: NaviPayCustomerOnboardingEntity ) = naviPayCustomerOnboardingDao.upsert( 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 5749d1d0f9..f9447a66c1 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 @@ -8,24 +8,39 @@ package com.navi.pay.onboarding.binding.ui import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.navi.base.deeplink.DeepLinkManager import com.navi.base.model.CtaData +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily import com.navi.pay.R import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.common.model.view.NaviPayButtonAction +import com.navi.pay.common.theme.color.NaviPayColor +import com.navi.pay.common.theme.color.NaviPayColor.textTertiary import com.navi.pay.common.ui.BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader import com.navi.pay.common.ui.BottomSheetLoadingScreen +import com.navi.pay.common.ui.NaviPayAutoReadOtpBottomSheet import com.navi.pay.common.ui.SuccessBottomSheetContent import com.navi.pay.common.ui.TitleDescriptionWithLinearProgressBar import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetType import com.navi.pay.onboarding.binding.viewmodel.NaviPayOnboardingViewModel import com.navi.pay.onboarding.common.NaviPayOnBoardingActions +import com.navi.pay.utils.BOTTOM_SHEET_HEIGHT_PERCENTAGE_80 @Composable fun NaviPayOnboardingBottomSheetContent( @@ -37,6 +52,12 @@ fun NaviPayOnboardingBottomSheetContent( ) { val bindingProgress by naviPayOnboardingViewModel.bindingProgress.collectAsStateWithLifecycle() + val autoReadOtpVerificationState by + naviPayOnboardingViewModel.autoReadOtpVerificationState.collectAsStateWithLifecycle() + val generatedOtp by naviPayOnboardingViewModel.generatedOtp.collectAsStateWithLifecycle() + val otpTimeOut by naviPayOnboardingViewModel.otpTimeOut.collectAsStateWithLifecycle() + val showButtonLoader by + naviPayOnboardingViewModel.showButtonLoader.collectAsStateWithLifecycle() when (bottomSheetType) { is NaviPayOnboardingBottomSheetType.Loading -> { @@ -45,6 +66,7 @@ fun NaviPayOnboardingBottomSheetContent( descriptionTextId = bottomSheetType.descriptionResId, ) } + NaviPayOnboardingBottomSheetType.NoSimCardBottomSheet -> { BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( headerTextId = R.string.sim_card_detection_failed, @@ -68,12 +90,14 @@ fun NaviPayOnboardingBottomSheetContent( }, ) } + is NaviPayOnboardingBottomSheetType.SingleSimConfirmationBottomSheet -> SingleSimConfirmationBottomSheetContent( naviPayAnalytics = naviPayAnalytics, singleSimConfirmationBottomSheet = bottomSheetType, naviPayOnboardingViewModel = naviPayOnboardingViewModel, ) + is NaviPayOnboardingBottomSheetType.SimSelectionBottomSheet -> SimSelectionBottomSheetContent( naviPayAnalytics = naviPayAnalytics, @@ -81,12 +105,14 @@ fun NaviPayOnboardingBottomSheetContent( naviPayOnboardingViewModel = naviPayOnboardingViewModel, isFirstTimeUserExperience = isFirstTimeUserExperience, ) + is NaviPayOnboardingBottomSheetType.BindingInProgressBottomSheet -> TitleDescriptionWithLinearProgressBar( rolodexTitleList = bottomSheetType.rolodexTitleList, description = bottomSheetType.descriptionText, indicatorProgress = bindingProgress, ) + is NaviPayOnboardingBottomSheetType.DeclineDeviceBottomSheet -> BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( header = bottomSheetType.title, @@ -106,6 +132,7 @@ fun NaviPayOnboardingBottomSheetContent( ) }, ) + is NaviPayOnboardingBottomSheetType.PhoneVerifiedBottomSheet -> BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( iconId = R.drawable.navi_pay_ic_checked_circle_green, @@ -117,6 +144,7 @@ fun NaviPayOnboardingBottomSheetContent( onSecondaryButtonClicked = {}, headerLottieFileName = bottomSheetType.headerLottieFileName, ) + is NaviPayOnboardingBottomSheetType.ErrorBottomSheet -> OnboardingErrorBottomSheetContent( errorConfig = bottomSheetType.errorConfig, @@ -137,6 +165,7 @@ fun NaviPayOnboardingBottomSheetContent( ) }, ) + is NaviPayOnboardingBottomSheetType.SuccessBottomSheet -> { SuccessBottomSheetContent( title = bottomSheetType.titleText, @@ -147,6 +176,7 @@ fun NaviPayOnboardingBottomSheetContent( headerLottieFileName = bottomSheetType.headerLottieFileName, ) } + is NaviPayOnboardingBottomSheetType.NonVerifiedSenderLoginError -> { BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( header = bottomSheetType.titleText, @@ -187,6 +217,93 @@ fun NaviPayOnboardingBottomSheetContent( }, ) } + + is NaviPayOnboardingBottomSheetType.AutoReadOtp -> { + NaviPayAutoReadOtpBottomSheet( + modifier = + Modifier.fillMaxWidth() + .heightIn( + max = + LocalConfiguration.current.screenHeightDp.dp * + BOTTOM_SHEET_HEIGHT_PERCENTAGE_80 + ), + timeOutInSeconds = otpTimeOut, + onResendOtpClicked = naviPayOnboardingViewModel::onReVerifyOrReSendButtonClicked, + onReVerifyOtpClicked = naviPayOnboardingViewModel::onReVerifyOrReSendButtonClicked, + generatedOtp = generatedOtp, + autoReadOtpVerificationState = autoReadOtpVerificationState, + showButtonLoader = showButtonLoader, + ) + } + + is NaviPayOnboardingBottomSheetType.AutoReadOtpVerificationLimitReached -> { + BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( + header = stringResource(id = R.string.np_verification_limit_reached), + annotatedDescription = + buildAnnotatedString { + val description = + stringResource(id = R.string.np_verification_limit_reached_desc) + val highlightedText = "24 hours." + val highlightedTextIndex = description.indexOf(highlightedText) + + withStyle( + style = + SpanStyle( + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontSize = 14.sp, + color = textTertiary, + ) + ) { + append( + description.substring( + startIndex = 0, + endIndex = highlightedTextIndex, + ) + ) + } + + withStyle( + style = + SpanStyle( + fontFamily = naviFontFamily, + fontWeight = + getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR), + fontSize = 14.sp, + color = NaviPayColor.textPrimary, + ) + ) { + append(highlightedText) + } + }, + onPrimaryButtonClicked = { + naviPayOnboardingViewModel.updateOnboardingAction( + onboardingAction = NaviPayOnBoardingActions.FinishScreen + ) + }, + onSecondaryButtonClicked = {}, + primaryButton = stringResource(id = R.string.np_okay_got_it), + secondaryButton = null, + ) + } + + is NaviPayOnboardingBottomSheetType.OtpNotGeneratedError -> { + BottomSheetContentWithIconHeaderPrimarySecondaryButtonCtaLoader( + header = bottomSheetType.title, + description = bottomSheetType.description, + primaryButton = bottomSheetType.primaryButtonText, + secondaryButton = bottomSheetType.secondaryButtonText, + onPrimaryButtonClicked = + naviPayOnboardingViewModel::onReVerifyOrReSendButtonClicked, + onSecondaryButtonClicked = { + naviPayAnalytics.onDeclineDeviceCancelClick() + naviPayOnboardingViewModel.updateOnboardingAction( + onboardingAction = NaviPayOnBoardingActions.FinishScreen + ) + }, + ) + } + is NaviPayOnboardingBottomSheetType.None -> Spacer(modifier = Modifier.height(1.dp)) } } 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 fccc7fcc83..f0f282ff42 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 @@ -7,7 +7,10 @@ package com.navi.pay.onboarding.binding.ui +import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Context +import android.content.IntentFilter import android.os.Bundle import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult @@ -27,6 +30,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.window.SecureFlagPolicy import androidx.hilt.navigation.compose.hiltViewModel @@ -35,11 +39,16 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.android.gms.auth.api.phone.SmsRetriever 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.SmsAutoReadWithConsentReceiver import com.navi.common.upi.UPI_RESULT_CODE +import com.navi.common.utils.generateSenderNames +import com.navi.common.utils.log +import com.navi.common.utils.registerReceiverWithVersionCheck import com.navi.pay.R import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.common.model.view.NaviPayScreenType @@ -62,6 +71,7 @@ import com.navi.pay.permission.model.view.PermissionState import com.navi.pay.permission.utils.PermissionKeys.FIRST_TIME_SCREEN_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.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult @@ -138,6 +148,16 @@ fun NaviPayOnboardingScreen( } } + val onAutoReadOtpConsentAllowClicked = { message: String? -> + naviPayOnboardingViewModel.autoReadOtpConsentAllowClicked(message) + } + + SmsConsentHandler( + onSmsConsentBottomSheetVisible = naviPayOnboardingViewModel::onSmsConsentBottomSheetVisible, + onAutoReadOtpConsentDenyClicked = naviPayOnboardingViewModel::autoReadOtpConsentDenyClicked, + onAutoReadOtpConsentAllowClicked = onAutoReadOtpConsentAllowClicked, + ) + val coroutineScope = rememberCoroutineScope() val bottomSheetStateHolder by naviPayOnboardingViewModel.bottomSheetStateHolder.collectAsStateWithLifecycle() @@ -510,6 +530,75 @@ fun NaviPayOnboardingScreen( } } +@Composable +fun SmsConsentHandler( + onSmsConsentBottomSheetVisible: () -> Unit, + onAutoReadOtpConsentDenyClicked: () -> Unit, + onAutoReadOtpConsentAllowClicked: (String?) -> Unit, +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val smsConsentIntentLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result + -> + if (result.resultCode == Activity.RESULT_OK && result.data != null) { + result.data?.let { intent -> + val message = intent.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE) + onAutoReadOtpConsentAllowClicked(message) + } ?: { onAutoReadOtpConsentDenyClicked() } + } else { + onAutoReadOtpConsentDenyClicked() + } + } + + LaunchedEffect(Unit) { + val client = SmsRetriever.getClient(context) + val senderNamesList = generateSenderNames() + senderNamesList.forEach { client.startSmsUserConsent(it) } + } + + DisposableEffect(lifecycleOwner) { + val smsReceiver = + SmsAutoReadWithConsentReceiver().apply { + setListener( + otpReceiveListener = + getOtpReceiveListener( + handleConsentIntent = { intent -> + try { + onSmsConsentBottomSheetVisible() + smsConsentIntentLauncher.launch(intent) + } catch (e: ActivityNotFoundException) { + e.log() + } + } + ) + ) + } + val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION) + + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + registerReceiverWithVersionCheck( + context = context, + broadcastReceiver = smsReceiver, + intentFilter = intentFilter, + listenToBroadcastsFromOtherApps = true, + ) + } + Lifecycle.Event.ON_PAUSE -> { + context.unregisterReceiver(smsReceiver) + } + else -> {} + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } +} + /* Todo: Replace this with NaviPayModalBottomSheet */ @@ -559,15 +648,19 @@ private fun InitLifecycleListener(naviPayOnboardingViewModel: NaviPayOnboardingV val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(key1 = lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START) { - naviPayOnboardingViewModel.updateIsAwayFromMainScreenState( - isAwayFromMainScreen = false - ) - } else if (event == Lifecycle.Event.ON_STOP) { - naviPayOnboardingViewModel.updateIsAwayFromMainScreenState( - isAwayFromMainScreen = true - ) - naviPayOnboardingViewModel.declineDeviceBinding() + when (event) { + Lifecycle.Event.ON_START -> { + naviPayOnboardingViewModel.updateIsAwayFromMainScreenState( + isAwayFromMainScreen = false + ) + } + Lifecycle.Event.ON_STOP -> { + naviPayOnboardingViewModel.updateIsAwayFromMainScreenState( + isAwayFromMainScreen = true + ) + naviPayOnboardingViewModel.declineDeviceBinding() + } + else -> {} } } lifecycleOwner.lifecycle.addObserver(observer) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt index be814ba2a7..ae91bfbeac 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SingleSimConfirmationBottomSheetContent.kt @@ -50,15 +50,10 @@ fun SingleSimConfirmationBottomSheetContent( "$INDIA_COUNTRY_CODE_WITH_PLUS ${naviPayOnboardingViewModel.userPhoneNumber}" } - val title = remember { - String.format( - naviPayOnboardingViewModel.naviPayOnboardingConfig.config.simSelectionSheetTitle, - userPhoneNumber, - ) - } - val isSmvEligibilityCheckOngoing by naviPayOnboardingViewModel.isSmvEligibilityCheckOngoing.collectAsStateWithLifecycle() + val showButtonLoader by + naviPayOnboardingViewModel.showButtonLoader.collectAsStateWithLifecycle() Column( modifier = @@ -72,7 +67,7 @@ fun SingleSimConfirmationBottomSheetContent( ) Spacer(modifier = Modifier.height(8.dp)) NaviText( - text = title, + text = stringResource(id = R.string.np_verify_your_number, userPhoneNumber), fontSize = 16.sp, fontFamily = naviFontFamily, fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), @@ -106,9 +101,9 @@ fun SingleSimConfirmationBottomSheetContent( LoaderRoundedButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.verify), - enabled = !isSmvEligibilityCheckOngoing, + enabled = !isSmvEligibilityCheckOngoing && !showButtonLoader, lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE, - showLoader = isSmvEligibilityCheckOngoing, + showLoader = isSmvEligibilityCheckOngoing || showButtonLoader, onClick = { val selectedSim = currentSimInfoList[0] naviPayAnalytics.onSingleSimConfirmationClick( 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 35e3397000..67ba3392ce 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 @@ -19,27 +19,37 @@ import com.google.gson.reflect.TypeToken import com.navi.base.AppServiceManager import com.navi.base.cache.model.NaviCacheEntity import com.navi.base.cache.repository.NaviCacheRepository +import com.navi.base.deeplink.util.DeeplinkConstants +import com.navi.base.utils.BaseUtils import com.navi.base.utils.ResourceProvider import com.navi.base.utils.orFalse import com.navi.base.utils.orTrue import com.navi.common.di.CoroutineDispatcherProvider import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_AUTO_READ_OTP_DISABLED import com.navi.common.model.AppUpgradeResponse import com.navi.common.model.PermissionVerticalType +import com.navi.common.network.models.GenerateOtpRequest import com.navi.common.network.models.RepoResult +import com.navi.common.network.models.TenantId +import com.navi.common.network.models.VerifyOtpRequest import com.navi.common.network.models.isError import com.navi.common.network.models.isSuccess import com.navi.common.network.models.isSuccessWithData +import com.navi.common.repo.NaviCommonRepository import com.navi.common.repo.PermissionSubmitRepository import com.navi.common.usecase.LitmusExperimentsUseCase +import com.navi.common.utils.Constants.AUTO_READ_OTP_CONSENT_KEY import com.navi.common.utils.EMPTY import com.navi.common.utils.NaviApiPoller +import com.navi.common.utils.parseCode import com.navi.pay.BuildConfig import com.navi.pay.R import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_SETUP import com.navi.pay.common.connectivity.NaviPayNetworkConnectivity import com.navi.pay.common.model.config.NaviPayDefaultConfig +import com.navi.pay.common.model.view.AutoReadOtpVerificationState import com.navi.pay.common.model.view.DeviceData import com.navi.pay.common.model.view.DeviceDetails import com.navi.pay.common.model.view.NaviPayButtonAction @@ -51,6 +61,7 @@ import com.navi.pay.common.model.view.NetworkProvider import com.navi.pay.common.model.view.SimInfo import com.navi.pay.common.model.view.isAnyEmpty import com.navi.pay.common.repository.CommonRepository +import com.navi.pay.common.repository.SharedPreferenceRepository import com.navi.pay.common.setup.NaviPayCustomerStatusHandler import com.navi.pay.common.setup.NaviPayManager import com.navi.pay.common.setup.NaviPaySetupUseCase @@ -95,6 +106,8 @@ import com.navi.pay.permission.utils.PermissionKeys.NON_FIRST_TIME_SCREEN_PERMIS import com.navi.pay.permission.utils.PermissionStateProvider 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.DENY import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITHOUT_PLUS import com.navi.pay.utils.LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING import com.navi.pay.utils.NAVI_PAY_API_STATUS_SUCCESS @@ -103,9 +116,12 @@ import com.navi.pay.utils.NAVI_PAY_GREEN_TICK_LOTTIE import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE import com.navi.pay.utils.NON_VERIFIED_SENDER_LOGIN import com.navi.pay.utils.ONBOARDING_CONFIG +import com.navi.pay.utils.OTP_AUTO_READ_TIMEOUT_IN_SECONDS import com.navi.pay.utils.PHONE_NUMBER_LENGTH import com.navi.pay.utils.SMS_SENT_CHECK_TIMEOUT import com.navi.pay.utils.SMS_VERIFICATION_PENDING +import com.navi.pay.utils.TOO_MANY_OTP_GENERATE_ATTEMPTS +import com.navi.pay.utils.TOO_MANY_OTP_VALIDATION_ATTEMPTS import dagger.hilt.android.lifecycle.HiltViewModel import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -115,6 +131,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -160,6 +177,8 @@ constructor( private val naviCacheRepository: NaviCacheRepository, private val clearOnboardingAndDeviceDataUseCase: ClearOnboardingAndDeviceDataUseCase, private val naviPayCustomerStatusHandler: NaviPayCustomerStatusHandler, + private val naviCommonRepository: NaviCommonRepository, + private val sharedPreferenceRepository: SharedPreferenceRepository, savedStateHandle: SavedStateHandle, ) : NaviPayBaseVM() { @@ -191,6 +210,15 @@ constructor( ) private val simInfoList = _simInfoList.asStateFlow() + private val _generatedOtp = MutableStateFlow(EMPTY) + val generatedOtp = _generatedOtp.asStateFlow() + + private val _autoReadOtpVerificationState = + MutableStateFlow(AutoReadOtpVerificationState.DEFAULT) + val autoReadOtpVerificationState = _autoReadOtpVerificationState.asStateFlow() + + private var otpTimerJob: Job? = null + private val _selectedSimInfo = MutableStateFlow(null) val selectedSimInfo = _selectedSimInfo.asStateFlow() @@ -280,6 +308,16 @@ constructor( private const val TAG_CUSTOMER_SETUP_ERROR = "CUSTOMER_SETUP_ERROR" } + private val _showButtonLoader = MutableStateFlow(false) + val showButtonLoader = _showButtonLoader.asStateFlow() + + private val _otpTimeOut = MutableStateFlow(30) + val otpTimeOut = _otpTimeOut.asStateFlow() + + private var generatedOtpToken: String = EMPTY + + private var isBindingEligibleForSmv = false + init { updateOnboardingIntentData() updateNaviPayOnboardingConfig() @@ -374,6 +412,10 @@ constructor( this.isFirstTimeUserExperience.update { isFirstTimeUserExperience } } + private fun updateGeneratedOtp(otp: String) { + _generatedOtp.update { otp } + } + private fun updateBindingProgress(progress: Float) { _bindingProgress.update { progress } } @@ -388,26 +430,30 @@ constructor( customerStatus = customerOnboardingEntity?.customerStatus?.name.orEmpty() ) } + NaviPayOnboardingActionsType.DEVICE_BINDING -> { updateOnboardingAction( onboardingAction = NaviPayOnBoardingActions.DeviceBinding ) } + NaviPayOnboardingActionsType.ACCOUNT_ADDITION -> { handleAction(action = NaviPayOnBoardingActions.AccountAddition) } + NaviPayOnboardingActionsType.SELF_TRANSFER, NaviPayOnboardingActionsType.UPI_LITE, NaviPayOnboardingActionsType.UPI_NUMBER, NaviPayOnboardingActionsType.UPI_INTERNATIONAL -> { handleAction(action = NaviPayOnBoardingActions.FlowSpecificOnboardingIntent) } + NaviPayOnboardingActionsType.ACCOUNT_ACTIVATION -> {} } } } - fun triggerFinishActivity() { + private fun triggerFinishActivity() { coroutineScope.safeLaunch(coroutineDispatcherProvider.io) { _finishActivity.emit(Unit) } } @@ -478,6 +524,7 @@ constructor( ), tag = TAG_CUSTOMER_SETUP_ERROR, ) + else -> { notifyError( errorConfig = @@ -486,6 +533,7 @@ constructor( } } } + is NaviPaySetupStatus.Success -> { val customerStatus = naviPaySetupStatus.naviPaySetupSuccessDetailsMap[onboardingPsp] @@ -558,10 +606,12 @@ constructor( syncLiteAccountInfo() handleOnboardingActionsFromIntent() } + NaviPayCustomerStatus.DEVICE_BOUNDED -> { updateEnabledAccountTypes(enabledAccountTypes = enabledAccountTypes.value) handleAction(action = NaviPayOnBoardingActions.AccountAddition) } + else -> { handleE2EOnboarding(customerStatus = customerStatus.name) } @@ -590,9 +640,11 @@ constructor( } emitCustomerStatusAfterOnboarding(customerStatus = NaviPayCustomerStatus.LINKED_VPA) } + NaviPayCustomerStatus.DEVICE_BOUNDED.name -> { handleAction(action = NaviPayOnBoardingActions.AccountAddition) } + else -> { handleAction(action = NaviPayOnBoardingActions.DeviceBinding) } @@ -681,20 +733,22 @@ constructor( val smvEligibilityStatus = getSmvEligibilityStatus(provider = getNetworkProvider()) if (!smvEligibilityStatus.isEligible) { - checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv = false) + checkForBindingPermissionAndStartBinding() return@safeLaunch } + isBindingEligibleForSmv = true + // SMV is eligible cases if (!smvEligibilityStatus.isSmsPermissionEnabled) { - startSimBinding(isBindingEligibleForSmv = true) + startSimBinding() } else { - checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv = true) + checkForBindingPermissionAndStartBinding() } } } - private suspend fun checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv: Boolean) { + private suspend fun checkForBindingPermissionAndStartBinding() { val multipleStatePermission = PermissionUtils.permissionDataMap.getOrElse( FIRST_TIME_SCREEN_PERMISSION_KEY, @@ -706,7 +760,7 @@ constructor( permissionList = multipleStatePermission.flatMap { it.qualifierList } ) ) { - startSimBinding(isBindingEligibleForSmv = isBindingEligibleForSmv) + startSimBinding() } else { _requestPermission.emit(FIRST_TIME_SCREEN_PERMISSION_KEY) } @@ -718,6 +772,7 @@ constructor( is NaviPayOnBoardingActions.E2EOnboarding -> { updateOnboardingAction(onboardingAction = action) } + is NaviPayOnBoardingActions.DeviceBinding -> { if ( checkIfPermissionGranted( @@ -735,16 +790,20 @@ constructor( _requestPermission.emit(NON_FIRST_TIME_SCREEN_PERMISSION_KEY) } } + is NaviPayOnBoardingActions.AccountAddition -> { _launchAccountAdditionFlow.emit(true) } + is NaviPayOnBoardingActions.RetrySimBinding, NaviPayOnBoardingActions.RetrySimCheck -> { triggerNaviPaySetup() } + is NaviPayOnBoardingActions.FlowSpecificOnboardingIntent -> { handleFlowSpecificOnboardingIntent() } + is NaviPayOnBoardingActions.FinishScreen -> { triggerFinishActivity() } @@ -794,7 +853,7 @@ constructor( fun getNaviPaySessionAttributes(): Map = naviPaySessionHelper.getNaviPaySessionAttributes() - private suspend fun startSimBinding(isBindingEligibleForSmv: Boolean) { + private suspend fun startSimBinding() { naviPayAnalytics.onPermissionGranted( onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), @@ -811,19 +870,7 @@ constructor( ) return } - _bindingProgress.update { 0f } - updateBottomSheetUIState( - showBottomSheet = true, - bottomSheetUIState = - NaviPayOnboardingBottomSheetType.BindingInProgressBottomSheet( - rolodexTitleList = - naviPayOnboardingConfig.config.deviceBindingProgressTitleList, - descriptionText = - resourceProvider.getString( - resId = R.string.np_device_binding_progress_description - ), - ), - ) + updateShowButtonLoader(true) updateDeviceBindingState(DeviceBindingState.Initiated) // Step 1: Get customer API call to get merchant customer ID @@ -881,6 +928,8 @@ constructor( naviPaySessionAttributes = getNaviPaySessionAttributes(), ) + val isConsentGivenForOtpAutoRead = + sharedPreferenceRepository.getBooleanValueSecurely(key = AUTO_READ_OTP_CONSENT_KEY) val bindDeviceAPIResponse = naviPayOnboardingRepository.bindDevice( bindDeviceRequest = @@ -890,20 +939,36 @@ constructor( merchantCustomerId = customerResponse.merchantCustomerId, bindingType = if (isBindingEligibleForSmv) BindingType.SMV else BindingType.SMS, + isAutoOtpPermitted = isConsentGivenForOtpAutoRead, ), metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), ) + updateShowButtonLoader(false) if (!bindDeviceAPIResponse.isSuccessWithData()) { updateDeviceBindingState(DeviceBindingState.Failure) if (bindDeviceAPIResponse.errors?.getOrNull(0)?.code == NON_VERIFIED_SENDER_LOGIN) { - handleCaseForNonVerifiedSenderLogin(bindDeviceAPIResponse = bindDeviceAPIResponse) + handleCaseForNonVerifiedSenderLoginOrResendOtp() } else { onBindingError(bindDeviceAPIResponse) } return } + _bindingProgress.update { 0f } + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.BindingInProgressBottomSheet( + rolodexTitleList = + naviPayOnboardingConfig.config.deviceBindingProgressTitleList, + descriptionText = + resourceProvider.getString( + resId = R.string.np_device_binding_progress_description + ), + ), + ) + this@NaviPayOnboardingViewModel.bindDeviceResponse = bindDeviceAPIResponse.data!! if (isBindingEligibleForSmv) { @@ -913,6 +978,27 @@ constructor( } } + private fun updateShowButtonLoader(showButtonLoader: Boolean) { + _showButtonLoader.update { showButtonLoader } + } + + private fun updateAutoReadOtpVerificationState( + autoReadOtpVerificationState: AutoReadOtpVerificationState + ) { + _autoReadOtpVerificationState.update { autoReadOtpVerificationState } + } + + fun onReVerifyOrReSendButtonClicked() { + viewModelScope.launch(Dispatchers.IO) { + naviPayAnalytics.onResendOtpClicked( + naviPaySessionAttributes = getNaviPaySessionAttributes(), + provider = getNetworkProvider(), + otpVerificationState = autoReadOtpVerificationState.value.name, + ) + handleCaseForNonVerifiedSenderLoginOrResendOtp() + } + } + private suspend fun processBindDeviceResponseForSms(provider: NetworkProvider) { naviPayAnalytics.onSmsBindingVMNReceived( serviceProviders = bindDeviceResponse.smsBindingData!!.serviceProviders, @@ -1427,15 +1513,19 @@ constructor( DeviceBindingState.Success -> { updateBindingProgress(progress = 1f) } + DeviceBindingState.Initiated -> { updateBindingProgress(progress = 0.05f) } + DeviceBindingState.Binding -> { updateBindingProgress(progress = 0.65f) } + DeviceBindingState.Verifying -> { updateBindingProgress(progress = 0.80f) } + else -> { updateBindingProgress(progress = 0f) } @@ -1521,10 +1611,12 @@ constructor( updateEnabledAccountTypes(enabledAccountTypes = listOf(AccountType.SAVINGS)) handleAction(action = NaviPayOnBoardingActions.AccountAddition) } + NaviPayCustomerStatus.LINKED_VPA -> { syncLiteAccountInfo() initiateAddAccountFlows() } + else -> { updateEnabledAccountTypes(enabledAccountTypes = listOf(AccountType.SAVINGS)) handleAction(action = NaviPayOnBoardingActions.E2EOnboarding) @@ -1635,25 +1727,207 @@ constructor( ) } - private fun handleCaseForNonVerifiedSenderLogin( - bindDeviceAPIResponse: RepoResult - ) { - val error = getError(response = bindDeviceAPIResponse) - naviPayAnalytics.onNonVerifiedSenderLogin(error = error) + fun autoReadOtpConsentDenyClicked() { + viewModelScope.launch(Dispatchers.IO) { + otpTimerJob?.cancel() + updateOtpTimeOut(otpTimeOut = 30) + updateGeneratedOtp("") + + naviPayAnalytics.onOtpAutoReadCtaClicked( + provider = getNetworkProvider(), + ctaAction = DENY, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.DENIED) + naviPayAnalytics.onAutoReadOtpConsentDeniedBottomSheetLanded( + provider = getNetworkProvider(), + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + } + } + + fun onSmsConsentBottomSheetVisible() { + naviPayAnalytics.onSmsConsentBottomSheetVisible( + onboardingSource = onboardingSource.value, + provider = getNetworkProvider(), + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + } + + fun autoReadOtpConsentAllowClicked(message: String?) { + viewModelScope.launch(Dispatchers.IO) { + val generatedOtp = parseCode(message = message) + if (generatedOtp.isNullOrBlank()) return@launch + + updateGeneratedOtp(otp = generatedOtp) + + naviPayAnalytics.onOtpAutoReadCtaClicked( + provider = getNetworkProvider(), + ctaAction = ALLOW, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.VERIFYING) + otpTimerJob?.cancel() + updateOtpTimeOut(otpTimeOut = 30) + sharedPreferenceRepository.setBooleanValueSecurely( + key = AUTO_READ_OTP_CONSENT_KEY, + value = true, + ) + + val verifyOtpResponse = + naviCommonRepository.verifyOtp( + verifyOtpRequest = + VerifyOtpRequest(otpToken = generatedOtpToken, otp = generatedOtp), + metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), + tenantId = TenantId.UPI_AUTO_READ_OTP.name, + ) + if (!verifyOtpResponse.isSuccessWithData()) { + val isOtpValidationAttemptExhausted = + verifyOtpResponse.errors?.getOrNull(0)?.code == TOO_MANY_OTP_VALIDATION_ATTEMPTS + if (isOtpValidationAttemptExhausted) { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetStateChange = false, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.AutoReadOtpVerificationLimitReached, + ) + } else { + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.FAILED) + } + updateGeneratedOtp("") + naviPayAnalytics.onOtpAutoReadVerificationFailed( + provider = getNetworkProvider(), + naviPaySessionAttributes = getNaviPaySessionAttributes(), + isOtpValidationAttemptExhausted = isOtpValidationAttemptExhausted, + ) + return@launch + } + startSimBinding() + } + } + + private fun handleAutoReadOtpDisabledCase() { + naviPayAnalytics.onNonVerifiedSenderLogin() updateBottomSheetUIState( showBottomSheet = true, bottomSheetStateChange = false, bottomSheetUIState = NaviPayOnboardingBottomSheetType.NonVerifiedSenderLoginError( - titleText = error.title, - descriptionText = error.description, - primaryButtonText = error.firstPrimaryButtonConfig?.text ?: "", - secondaryButtonText = error.firstSecondaryButtonConfig?.text ?: "", - actionType = error.firstPrimaryButtonConfig?.action, + titleText = + resourceProvider.getString(R.string.np_non_verified_sender_login_title), + descriptionText = + resourceProvider.getString(R.string.np_non_verified_sender_login_desc), + primaryButtonText = resourceProvider.getString(R.string.np_logout), + secondaryButtonText = resourceProvider.getString(R.string.cancel), + actionType = NaviPayButtonAction.Redirect(url = DeeplinkConstants.LOGOUT), ), ) } + private suspend fun handleCaseForNonVerifiedSenderLoginOrResendOtp() { + val isAutoReadOtpDisabled = + FirebaseRemoteConfigHelper.getBoolean( + key = NAVI_PAY_AUTO_READ_OTP_DISABLED, + defaultValue = false, + ) + + if (isAutoReadOtpDisabled) { + handleAutoReadOtpDisabledCase() + return + } + + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.DEFAULT) + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = NaviPayOnboardingBottomSheetType.AutoReadOtp, + ) + + startOtpTimer() + + val generateOtpResponse = + naviCommonRepository.submitPhoneNumber( + generateOtpRequest = + GenerateOtpRequest( + recipient = BaseUtils.getPhoneNumber().orEmpty(), + deliveryType = "TEXT", + isRecipientEdited = false, + resend = false, + ), + tenantId = TenantId.UPI_AUTO_READ_OTP.name, + metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), + ) + + if (!generateOtpResponse.isSuccessWithData()) { + val isOtpGenerationAttemptExhausted = + generateOtpResponse.errors?.getOrNull(0)?.code == TOO_MANY_OTP_GENERATE_ATTEMPTS + if (isOtpGenerationAttemptExhausted) { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.AutoReadOtpVerificationLimitReached, + bottomSheetStateChange = false, + ) + } else { + updateBottomSheetUIState( + showBottomSheet = true, + bottomSheetUIState = + NaviPayOnboardingBottomSheetType.OtpNotGeneratedError( + title = + resourceProvider.getString( + resId = R.string.np_otp_generation_failed_title + ), + description = + resourceProvider.getString( + resId = R.string.np_otp_generation_failed_desc + ), + primaryButtonText = resourceProvider.getString(resId = R.string.retry), + secondaryButtonText = + resourceProvider.getString(resId = R.string.cancel), + ), + ) + } + naviPayAnalytics.onOtpAutoReadGenerationFailed( + provider = getNetworkProvider(), + naviPaySessionAttributes = getNaviPaySessionAttributes(), + isOtpGenerationAttemptExhausted = isOtpGenerationAttemptExhausted, + ) + otpTimerJob?.cancel() + updateOtpTimeOut(otpTimeOut = 30) + updateGeneratedOtp("") + return + } + + generatedOtpToken = generateOtpResponse.data?.otpToken.orEmpty() + + otpTimerJob?.join() + + if (otpTimeOut.value == 0) { + updateAutoReadOtpVerificationState(AutoReadOtpVerificationState.TIMEOUT) + updateOtpTimeOut(otpTimeOut = 30) + naviPayAnalytics.onOtpAutoReadTimeout( + provider = getNetworkProvider(), + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + return + } + } + + private suspend fun startOtpTimer() { + otpTimerJob?.cancel() + otpTimerJob = + viewModelScope.launch(Dispatchers.IO) { + for (i in OTP_AUTO_READ_TIMEOUT_IN_SECONDS downTo 0) { + updateOtpTimeOut(otpTimeOut = i) + delay(1.seconds) + } + } + } + + private fun updateOtpTimeOut(otpTimeOut: Int) { + _otpTimeOut.update { otpTimeOut } + } + @OptIn(DelicateCoroutinesApi::class) private fun syncLiteAccountInfo() { GlobalScope.safeLaunch(coroutineDispatcherProvider.io) { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/GatewayResponseCodes.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/GatewayResponseCodes.kt index fd6df6c877..b4844d4d49 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/utils/GatewayResponseCodes.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/GatewayResponseCodes.kt @@ -17,6 +17,6 @@ const val MAPPING_DOES_NOT_EXIST = "ERR_JPMM2" const val MCC_MISMATCH = "ERR_J09" const val INVALID_UPI_ID = "ERR_J02" const val SELF_TRANSFER_ERROR = "ERR_U96" -const val NON_VERIFIED_SENDER_LOGIN = "ERR_BIND_DEVICE_FRAUD_AUTO_OTP_EXCEPTION" +const val NON_VERIFIED_SENDER_LOGIN = "ERR_AUTO_READ_OTP_NOT_PERMITTED" const val INVALID_PIN = "ERR_ZM" const val INSUFFICIENT_FUNDS = "ERR_Z9" 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 b47340d200..e3f5577d73 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 @@ -7,6 +7,8 @@ package com.navi.pay.utils +import com.navi.common.utils.Constants.AUTO_READ_OTP_CONSENT_KEY + const val SOURCE_MODULE = "SOURCE_MODULE" const val BANK_LIST_IN_DB_REFRESH_MIN_TIMESTAMP = 86400000L // 1 day const val CONFIG_IN_DB_REFRESH_MIN_TIMESTAMP = 86400000L // 1 day @@ -68,6 +70,7 @@ const val NAVI_PAY_UPI_NUMBER_LINK_SUCCESSFUL_LOTTIE = "navi-pay-upi-number-link const val NAVI_PAY_NEXT_ACTION_CHEVRON_WHITE_LOTTIE = "navi-pay-next-action-chevron-white.lottie" const val NAVI_PAY_THUNDER_ANIMATION_UPI_LITE = "thunder_animation_upi_lite.lottie" const val NAVI_PAY_PROTECT_ARC_PENDING_LOTTIE = "navi-pay-arc-pending-loader.lottie" +const val NAVI_PAY_AUTO_READ_LOTTIE = "navi-pay-auto-read.lottie" // Shared Preference constants const val KEY_DEVICE_FINGERPRINT = "naviPayKeyDeviceFingerPrint" @@ -82,7 +85,8 @@ const val KEY_IS_FIRST_TRANSACTION_SUCCESSFUL = "naviPayIsFirstTransactionSucces const val PENDING_REQUEST_REFRESHED_REQUIRED = "pendingRequestRefreshRequired" const val KEY_NPCI_TOKEN = "npciToken" const val KEY_NPCI_TOKEN_STORED_TIME = "npciTokenStoredTime" -val NAVI_PAY_ENCRYPT_SHARED_PREF_DATA_KEYS = listOf(KEY_NPCI_TOKEN, KEY_NPCI_TOKEN_STORED_TIME) +val NAVI_PAY_ENCRYPT_SHARED_PREF_DATA_KEYS = + listOf(KEY_NPCI_TOKEN, KEY_NPCI_TOKEN_STORED_TIME, AUTO_READ_OTP_CONSENT_KEY) const val KEY_NPCI_TOKEN_EXPIRY_IN_DAYS = "npciTokenExpriyInDays" // Shared Preference constants ends here @@ -226,6 +230,9 @@ const val ACTIVATE = "Activate" const val NAVI_PAY_UNKNOWN = "Unknown" const val COIN_IMAGE_MAPPING_ID = "imageId" const val ONE_DAY_MILLIS = 24 * 60 * 60 * 1000 +const val DENY = "DENY" +const val ALLOW = "ALLOW" +const val OTP_AUTO_READ_TIMEOUT_IN_SECONDS = 30 // Offer experience const val TXN_AMOUNT = "TXN_AMOUNT" @@ -379,6 +386,10 @@ const val RETRY_INTERVAL_IN_SECONDS = 0.0 const val NAVI_PAY_TH_EXTERNAL_METADATA_CLIENT_NAME_KEY = "npClientName" +// Auto read otp error codes +const val TOO_MANY_OTP_GENERATE_ATTEMPTS = "TOO_MANY_OTP_GENERATE_ATTEMPTS" +const val TOO_MANY_OTP_VALIDATION_ATTEMPTS = "TOO_MANY_OTP_VALIDATION_ATTEMPTS" + // Navi pay rewards constants const val NAVI_PAY_NUDGE_DETAILS_PREFIX_TEXT = "Win upto" const val NAVI_PAY_NUDGE_DETAILS_REWARD_TYPE = "NAVI_COIN" 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 new file mode 100644 index 0000000000..f98f85df63 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/utils/OtpReceiveListener.kt @@ -0,0 +1,21 @@ +/* + * + * * 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) + } + } +} diff --git a/android/navi-pay/src/main/res/drawable/ic_auto_read_still_frame.xml b/android/navi-pay/src/main/res/drawable/ic_auto_read_still_frame.xml new file mode 100644 index 0000000000..58253b8c23 --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_auto_read_still_frame.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/navi-pay/src/main/res/drawable/ic_bank_default.xml b/android/navi-pay/src/main/res/drawable/ic_bank_default.xml new file mode 100644 index 0000000000..1e3a022a91 --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_bank_default.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/android/navi-pay/src/main/res/drawable/ic_np_read_otp.xml b/android/navi-pay/src/main/res/drawable/ic_np_read_otp.xml new file mode 100644 index 0000000000..e6f97ceb21 --- /dev/null +++ b/android/navi-pay/src/main/res/drawable/ic_np_read_otp.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/android/navi-pay/src/main/res/drawable/ic_np_rupee_card.xml b/android/navi-pay/src/main/res/drawable/ic_np_rupee_card.xml deleted file mode 100644 index 10248b092f..0000000000 --- a/android/navi-pay/src/main/res/drawable/ic_np_rupee_card.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - diff --git a/android/navi-pay/src/main/res/drawable/ic_np_sim_logo_gray.xml b/android/navi-pay/src/main/res/drawable/ic_np_sim_logo_gray.xml index 154d9e1a6e..343f0c9c2f 100644 --- a/android/navi-pay/src/main/res/drawable/ic_np_sim_logo_gray.xml +++ b/android/navi-pay/src/main/res/drawable/ic_np_sim_logo_gray.xml @@ -3,80 +3,22 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - - - - - - - + + + + + diff --git a/android/navi-pay/src/main/res/values/strings.xml b/android/navi-pay/src/main/res/values/strings.xml index 1df1219881..4fc0187ab8 100644 --- a/android/navi-pay/src/main/res/values/strings.xml +++ b/android/navi-pay/src/main/res/values/strings.xml @@ -816,6 +816,24 @@ This fees is charged by merchants to process credit transactions. Tip removed Tip added + Auto-read OTP verification + To enable Navi UPI, your SIM will be auto-read & the keyboard would be disabled until then + Verifying your OTP + Please do not go back or close the app. This usually takes a few seconds. + OTP verification failed + We are having trouble reading the OTP. Tap on resend OTP and continue + Still waiting for the OTP? + It might take a few moments. If you don’t receive one soon, tap below to resend OTP + Auto-read OTP required to use Navi UPI + To proceed with Navi UPI setup, SIM verification via auto-read OTP is mandatory. Tap below to restart verification. + Resend OTP + Resend OTP in + Restart verification + Verification limit reached + You have reached the maximum limit of 3 verification attempts. Please try again after 24 hours. + Mobile verification failed + We couldn\'t verify your mobile number. Please try again. + Verify your number %s Super fast payments No wait, no hassle! Bank server down? @@ -830,4 +848,7 @@ Current balance: ₹ 1,000 Expiring in %s days + Retry OTP validation + Please logout and try logging in again. Ensure that the mobile number used for UPI registration is in your device and the OTP is auto-entered. + Logout \ No newline at end of file