diff --git a/.github/workflows/generate_build.yml b/.github/workflows/generate_build.yml index ab893336bc..011a04c6de 100644 --- a/.github/workflows/generate_build.yml +++ b/.github/workflows/generate_build.yml @@ -105,10 +105,10 @@ jobs: run: echo ${{ secrets.RELEASE_STORE_FILE }} | base64 -d >> app/navi-release-key.jks - name: Build - APK - ${{ inputs.environment }}-${{ inputs.type }} if: inputs.output == 'APK' - run: ./gradlew package${{ inputs.environment }}${{ inputs.type }}UniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} + run: ./gradlew package${{ inputs.environment }}${{ inputs.type }}UniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} -PNAVIPAY_SMV_BASE_URL=${{ secrets.NAVIPAY_SMV_BASE_URL }} -PNAVIPAY_SMV_CLIENT_ID=${{ secrets.NAVIPAY_SMV_CLIENT_ID }} - name: Build - AAB - ${{ inputs.environment }}-${{ inputs.type }} if: inputs.output == 'AAB' - run: ./gradlew :app:bundle${{ inputs.environment }}${{ inputs.type }} -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} + run: ./gradlew :app:bundle${{ inputs.environment }}${{ inputs.type }} -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} -PNAVIPAY_SMV_BASE_URL=${{ secrets.NAVIPAY_SMV_BASE_URL }} -PNAVIPAY_SMV_CLIENT_ID=${{ secrets.NAVIPAY_SMV_CLIENT_ID }} - name: Upload - ${{ inputs.output }} - ${{ inputs.environment }}-${{ inputs.type }} uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/macrobenchmark.yml b/.github/workflows/macrobenchmark.yml index 69cfd035c9..2da8350c88 100644 --- a/.github/workflows/macrobenchmark.yml +++ b/.github/workflows/macrobenchmark.yml @@ -47,7 +47,7 @@ jobs: - name: Export Release Store File run: echo ${{ secrets.RELEASE_STORE_FILE }} | base64 -d >> app/navi-release-key.jks - name: Build - APK - app - run: ./gradlew packageQaReleaseUniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} + run: ./gradlew packageQaReleaseUniversalApk -PRELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }} -PRELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }} -PRELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }} -PBASE_URL=${{ secrets.BASE_URL }} -PADS_ANALYTICS_BASE_URL=${{ secrets.ADS_ANALYTICS_BASE_URL }} -PALFRED_API_KEY=${{ secrets.ALFRED_API_KEY }} -PAPPSFLYER_KEY=${{ secrets.APPSFLYER_KEY }} -PHYPERVERGE_APP_ID=${{ secrets.HYPERVERGE_APP_ID }} -PHYPERVERGE_APP_KEY=${{ secrets.HYPERVERGE_APP_KEY }} -PMQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} -PMQTT_USERNAME=${{ secrets.MQTT_USERNAME }} -PPULSE_BASE_URL=${{ secrets.PULSE_BASE_URL }} -PSSL_PINNING_KEY=${{ secrets.SSL_PINNING_KEY }} -PYOUTUBE_KEY=${{ secrets.YOUTUBE_KEY }} -PFACEBOOK_APP_ID=${{ secrets.FACEBOOK_APP_ID }} -PTRUECALLER_KEY=${{ secrets.TRUECALLER_KEY }} -PGI_RAZORPAY_KEY=${{ secrets.GI_RAZORPAY_KEY }} -PGOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }} -PCODEPUSH_DEPLOYMENT_KEY=${{ secrets.CODEPUSH_DEPLOYMENT_KEY }} -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=${{ secrets.NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT }} -PNAVIPAY_SMV_BASE_URL=${{ secrets.NAVIPAY_SMV_BASE_URL }} -PNAVIPAY_SMV_CLIENT_ID=${{ secrets.NAVIPAY_SMV_CLIENT_ID }} - name: Build - APK - benchmark run: ./gradlew benchmark:assembleQaBenchmark - name: Authenticate Cloud SDK diff --git a/Dockerfile b/Dockerfile index e0db5071ae..baa35ad732 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,7 +64,9 @@ RUN --mount=type=secret,id=RELEASE_STORE_PASSWORD \ -PGI_RAZORPAY_KEY=$(cat /run/secrets/GI_RAZORPAY_KEY) \ -PGOOGLE_MAPS_KEY=$(cat /run/secrets/GOOGLE_MAPS_KEY) \ -PCODEPUSH_DEPLOYMENT_KEY=$(cat /run/secrets/CODEPUSH_DEPLOYMENT_KEY) \ - -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=$(cat /run/secrets/NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT) + -PNAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT=$(cat /run/secrets/NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT) \ + -PNAVIPAY_SMV_BASE_URL=$(cat /run/secrets/NAVIPAY_SMV_BASE_URL) \ + -PNAVIPAY_SMV_CLIENT_ID=$(cat /run/secrets/NAVIPAY_SMV_CLIENT_ID) RUN --mount=type=secret,id=FLAVOR \ --mount=type=secret,id=NEXUS_URL \ diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..3a93e05a43 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,18 @@ + + + + + + + airtel.in + sekuramobile.com + jio.com + passport.airtel.in + partnerapi.jio.com + + diff --git a/android/app/src/qa/res/xml/network_security_config.xml b/android/app/src/qa/res/xml/network_security_config.xml index ad427445dc..17f23aae45 100644 --- a/android/app/src/qa/res/xml/network_security_config.xml +++ b/android/app/src/qa/res/xml/network_security_config.xml @@ -2,12 +2,19 @@ + + airtel.in + sekuramobile.com + jio.com + passport.airtel.in + partnerapi.jio.com + diff --git a/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt b/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt index c9064588a6..671418abe3 100644 --- a/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/models/LitmusExperimentResponse.kt @@ -15,6 +15,7 @@ data class LitmusExperimentResponse( ) data class VariantInfo( + @SerializedName("name") val name: String, @SerializedName("enabled") val enabled: Boolean, @SerializedName("payload") val payload: Map?, ) diff --git a/android/navi-pay/build.gradle b/android/navi-pay/build.gradle index 7715a257c8..50f3cafe40 100644 --- a/android/navi-pay/build.gradle +++ b/android/navi-pay/build.gradle @@ -51,11 +51,17 @@ android { isDefault true dimension "app" buildConfigField 'String', 'NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT', formatString('LOCAL') + buildConfigField 'String', 'NAVIPAY_SMV_BASE_URL', formatString('https://api.sandbox.bureau.id/v2/auth/initiate') + buildConfigField 'String', 'NAVIPAY_SMV_CLIENT_ID', formatString('093f0fc3-bbe5-4944-a6c3-1bf410ae4237') } prod { dimension "app" - if (project.hasProperty('NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT')) { + if (project.hasProperty('NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT') + && project.hasProperty('NAVIPAY_SMV_BASE_URL') + && project.hasProperty('NAVIPAY_SMV_CLIENT_ID')) { buildConfigField 'String', 'NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT', formatString("$NAVIPAY_FIRESTORE_CUSTOMER_DATA_SALT") + buildConfigField 'String', 'NAVIPAY_SMV_BASE_URL', formatString("$NAVIPAY_SMV_BASE_URL") + buildConfigField 'String', 'NAVIPAY_SMV_CLIENT_ID', formatString("$NAVIPAY_SMV_CLIENT_ID") } } } diff --git a/android/navi-pay/src/main/AndroidManifest.xml b/android/navi-pay/src/main/AndroidManifest.xml index 9435c0ab00..e7bfbca81e 100644 --- a/android/navi-pay/src/main/AndroidManifest.xml +++ b/android/navi-pay/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ xmlns:tools="http://schemas.android.com/tools"> + 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 eb1df25660..b8954142d8 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 @@ -23,6 +23,7 @@ import com.navi.pay.common.model.view.CheckBalanceAnalyticsEventData import com.navi.pay.common.model.view.DeviceData import com.navi.pay.common.model.view.DeviceDetails import com.navi.pay.common.model.view.NaviPayErrorConfig +import com.navi.pay.common.model.view.NetworkProvider import com.navi.pay.common.model.view.SimInfo import com.navi.pay.common.setup.model.NaviPayCustomerStatus import com.navi.pay.common.utils.NaviPayCommonUtils.toGetSimInfo @@ -41,6 +42,7 @@ import com.navi.pay.onboarding.account.add.model.view.AccountType import com.navi.pay.onboarding.account.add.model.view.BankEntity import com.navi.pay.onboarding.account.detail.model.view.LinkedAccountEntity import com.navi.pay.onboarding.account.linked.model.view.LinkedAccountsScreenSource +import com.navi.pay.onboarding.binding.model.network.BindingType import com.navi.pay.onboarding.binding.model.network.CustomerResponse import com.navi.pay.onboarding.binding.model.network.ServiceProvider import com.navi.pay.onboarding.faq.model.view.UpiVideoEntity @@ -256,17 +258,19 @@ class NaviPayAnalytics private constructor() { ) } - fun smsSentSuccessfullyStartingPolling( + fun onStartBindStatusPolling( onboardingSource: String? = null, naviPaySessionAttributes: Map? = null, + bindingType: BindingType, ) { NaviTrackEvent.trackEventOnClickStream( - eventName = "NaviPay_Dev_smsSentSuccessfullyStartingPolling", + eventName = "NaviPay_Dev_onStartBindStatusPolling", eventValues = mapOf( "naviPayOnboardingSource" to onboardingSource.orEmpty(), "naviPayOnboardingSessionId" to naviPaySessionAttributes?.get("naviPayOnboardingSessionId").orEmpty(), + "bindingType" to bindingType.name, ), ) } @@ -426,15 +430,15 @@ class NaviPayAnalytics private constructor() { } fun startSimBinding( - simInfo: SimInfo?, + provider: NetworkProvider, onboardingSource: String? = null, naviPaySessionAttributes: Map? = null, ) { NaviTrackEvent.trackEventOnClickStream( "NaviPay_Setup_SimBinding_Start", mapOf( - Pair("simProvider", simInfo?.carrierName.orEmpty()), - Pair("simId", simInfo?.subscriptionId.orEmpty()), + Pair("simProvider", provider.name), + Pair("simId", provider.ssid), Pair("naviPayOnboardingSource", onboardingSource.orEmpty()), Pair( "naviPayOnboardingSessionId", @@ -640,6 +644,52 @@ class NaviPayAnalytics private constructor() { ), ) } + + fun onSmvExperimentDisabled() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_SmvExperimentDisabled") + } + + fun onProviderNotSupportedForSmv( + displayableCarrierName: String, + smvSupportedProviders: List, + ) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_ProviderNotSupportedForSmv", + mapOf( + "displayableCarrierName" to displayableCarrierName, + "smvSupportedProviders" to smvSupportedProviders.toString(), + ), + ) + } + + fun onSmvPreviouslyTriggered() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_SmvPreviouslyTriggered") + } + + fun onSelectedSimIsNotDefaultDataSim(selectedSimId: String, defaultDataSimId: String) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_SelectedSimIsNotDefaultDataSim", + mapOf("selectedSimId" to selectedSimId, "defaultDataSimId" to defaultDataSimId), + ) + } + + fun onRouteNetworkViaCellularResult(result: Boolean) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_RouteNetworkViaCellularResult", + mapOf("result" to result.toString()), + ) + } + + fun onInitiateSmvResponse(smvInitiateResponse: String?) { + NaviTrackEvent.trackEventOnClickStream( + "NaviPay_InitiateSmvResponse", + mapOf("smvInitiateResponse" to smvInitiateResponse.toString()), + ) + } + + fun onSmvInitiated() { + NaviTrackEvent.trackEventOnClickStream("NaviPay_SmvInitiated") + } } inner class NaviPayPermission { diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt index dcce90fd52..57647fb8bb 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/connectivity/NaviPayNetworkConnectivity.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2024 by Navi Technologies Limited + * * Copyright © 2024-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -9,11 +9,18 @@ package com.navi.pay.common.connectivity import android.content.Context import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network import android.net.NetworkCapabilities +import android.net.NetworkRequest import com.navi.pay.common.model.view.SimInfo import com.navi.pay.common.utils.NaviPayCommonUtils import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.time.Duration +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout interface NaviPayNetworkConnectivity { fun isInternetConnected(): Boolean @@ -21,6 +28,14 @@ interface NaviPayNetworkConnectivity { fun isAirplaneModeOn(): Boolean fun getCurrentSimInfoList(): List + + /** + * @throws TimeoutCancellationException if the binding process takes longer than the specified + * timeout. + */ + suspend fun bindNetworkToCellular(timeout: Duration): Boolean + + fun unbindNetwork() } class NaviPayNetworkConnectivityImpl @Inject constructor(@ApplicationContext val context: Context) : @@ -29,13 +44,14 @@ class NaviPayNetworkConnectivityImpl @Inject constructor(@ApplicationContext val override fun isInternetConnected(): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val networkCapabilities = connectivityManager.activeNetwork ?: return false - val actNw = connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false + val activeNetwork = connectivityManager.activeNetwork ?: return false + val activeNetworkCapability = + connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false val result = when { - actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true - actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true - actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true else -> false } @@ -49,4 +65,64 @@ class NaviPayNetworkConnectivityImpl @Inject constructor(@ApplicationContext val override fun getCurrentSimInfoList(): List { return NaviPayCommonUtils.getCurrentSimInfoList(context = context) } + + override suspend fun bindNetworkToCellular(timeout: Duration): Boolean { + + return withTimeout(timeout = timeout) { + suspendCancellableCoroutine { continuation -> + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.activeNetwork + val activeNetworkCapability = + connectivityManager.getNetworkCapabilities(activeNetwork) + + if ( + activeNetwork != null && + activeNetworkCapability != null && + activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + ) { + continuation.resume(true) + return@suspendCancellableCoroutine + } + + val networkRequest = + NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + + val networkCallback = + object : NetworkCallback() { + + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager.boundNetworkForProcess != network) { + connectivityManager.bindProcessToNetwork(network) + } + continuation.resume(true) + } + + override fun onUnavailable() { + super.onUnavailable() + if (continuation.isActive) { + continuation.resume(false) + } + } + } + + connectivityManager.requestNetwork(networkRequest, networkCallback) + + continuation.invokeOnCancellation { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + } + } + } + + override fun unbindNetwork() { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + connectivityManager.bindProcessToNetwork(null) + } } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt index 8f19ab2231..32211249ed 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/common/utils/NaviPayCommonUtils.kt @@ -9,7 +9,6 @@ package com.navi.pay.common.utils import android.Manifest import android.app.Activity -import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -21,7 +20,6 @@ import android.os.Bundle import android.os.VibrationEffect import android.os.Vibrator import android.provider.Settings -import android.telephony.SmsManager import android.telephony.SubscriptionManager import android.view.View import androidx.activity.result.ActivityResultLauncher @@ -40,7 +38,6 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.deeplink.DeepLinkManager import com.navi.base.deeplink.util.DeeplinkConstants import com.navi.base.deeplink.util.DeeplinkConstants.PRODUCT_HELP_PAGE @@ -69,7 +66,6 @@ import com.navi.common.upi.WITHOUT_ONBOARDING_FLOW import com.navi.common.utils.CommonRootDeviceUtil import com.navi.common.utils.Constants.AT_THE_RATE import com.navi.common.utils.capitalize -import com.navi.common.utils.log import com.navi.pay.BuildConfig import com.navi.pay.R import com.navi.pay.analytics.NaviPayAnalytics @@ -103,8 +99,6 @@ import com.navi.pay.utils.COMMA import com.navi.pay.utils.DOT_IFSC_DOT_NPCI import com.navi.pay.utils.HEX_FORMAT import com.navi.pay.utils.HYPHEN -import com.navi.pay.utils.INTENT_ACTION_SMS_DELIVERED -import com.navi.pay.utils.INTENT_ACTION_SMS_SENT import com.navi.pay.utils.KEY_CUSTOMER_STATUS import com.navi.pay.utils.KEY_DEVICE_FINGERPRINT import com.navi.pay.utils.KEY_UPI_LITE_ACTIVE @@ -180,7 +174,7 @@ object NaviPayCommonUtils { return ssidList } - private fun displayableCarrierName(actualCarrierName: String?): String { + fun displayableCarrierName(actualCarrierName: String?): String { return when { actualCarrierName == null -> EMPTY actualCarrierName.contains(other = CARRIER_JIO, ignoreCase = true) -> CARRIER_JIO @@ -226,71 +220,6 @@ object NaviPayCommonUtils { naviPayAnalytics.onAppSettingsScreenLaunched() } - fun sendSMS( - destinationNumberList: List, - messageContent: String, - subscriptionId: Int, - activityContext: Context, - ) { - if ( - ActivityCompat.checkSelfPermission(activityContext, Manifest.permission.SEND_SMS) != - PackageManager.PERMISSION_GRANTED - ) { - return - } - - if (isAirplaneModeOn(context = activityContext)) { - return - } - - val smsManager = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - activityContext - .getSystemService(SmsManager::class.java) - .createForSubscriptionId(subscriptionId) - } else { - SmsManager.getSmsManagerForSubscriptionId(subscriptionId) - } - - val sentPendingIntent = - PendingIntent.getBroadcast( - activityContext, - (0..1000).random(), - Intent(INTENT_ACTION_SMS_SENT), - PendingIntent.FLAG_IMMUTABLE, - ) - - val deliveredPendingIntent = - PendingIntent.getBroadcast( - activityContext, - (0..1000).random(), - Intent(INTENT_ACTION_SMS_DELIVERED), - PendingIntent.FLAG_IMMUTABLE, - ) - - try { - NaviTrackEvent.trackEventOnClickStream( - eventName = "dev_navipay_send_sms", - eventValues = - mapOf( - "destinationNumberList" to destinationNumberList.toString(), - "messageContentLength" to messageContent.length.toString(), - ), - ) - destinationNumberList.forEach { - smsManager.sendTextMessage( - it, - null, - messageContent, - sentPendingIntent, - deliveredPendingIntent, - ) - } - } catch (e: Exception) { - e.log() - } - } - fun getNaviPayAccessEligibility(context: Context): NaviPayAccessEligibility { return if ( !FirebaseRemoteConfigHelper.getRawBoolean(FirebaseRemoteConfigHelper.NAVI_UPI_ENABLED) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt index 58b122bd07..537b8ae360 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/di/NaviPayModule.kt @@ -61,6 +61,7 @@ import com.navi.pay.management.moneytransfer.scanpay.LightSensorManagerImpl import com.navi.pay.management.paytocontacts.PhoneContactManager import com.navi.pay.management.paytocontacts.PhoneContactManagerImpl import com.navi.pay.network.NaviPayHttpClient +import com.navi.pay.network.retrofit.NaviPayExternalRetrofitService import com.navi.pay.network.retrofit.NaviPayRetrofitService import com.navi.pay.npcicl.NpciSessionHandler import com.navi.pay.npcicl.NpciSessionHandlerImpl @@ -124,6 +125,15 @@ object NaviPayNetworkModule { .addConverterFactory(GsonConverterFactory.create(deserializer)) .build() + @Singleton + @Provides + fun providesNaviPayExternalApiService(): NaviPayExternalRetrofitService = + Retrofit.Builder() + .baseUrl("https://api.github.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(NaviPayExternalRetrofitService::class.java) + @Singleton @Provides fun providesNaviPayApiService(@NaviPayRetrofit retrofit: Retrofit): NaviPayRetrofitService = diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayExternalRetrofitService.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayExternalRetrofitService.kt new file mode 100644 index 0000000000..a1c93cc363 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayExternalRetrofitService.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.network.retrofit + +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.QueryMap +import retrofit2.http.Url + +interface NaviPayExternalRetrofitService { + @GET + suspend fun initiateSmv(@Url url: String, @QueryMap params: Map): Response +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt index f76c308c66..41c7c59a7f 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/network/retrofit/NaviPayRetrofitService.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2022-2024 by Navi Technologies Limited + * * Copyright © 2022-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -129,7 +129,7 @@ interface NaviPayRetrofitService { @Body addAccountRequest: AddAccountRequest ): Response> - @POST("/gateway-service/$NAVI_PAY_API_VERSION/navipay/bind-device") + @POST("/gateway-service/$NAVI_PAY_API_VERSION2/navipay/bind-device") suspend fun bindDevice( @Body bindDeviceRequest: BindDeviceRequest ): Response> @@ -144,7 +144,7 @@ interface NaviPayRetrofitService { @Body declineDeviceRequest: DeclineDeviceRequest ): Response> - @POST("/gateway-service/v2/navipay/account/linked-accounts") + @POST("/gateway-service/$NAVI_PAY_API_VERSION2/navipay/account/linked-accounts") suspend fun fetchLinkedAccounts( @Body linkedAccountsRequest: LinkedAccountsRequest ): Response> 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 19b6520c1d..e13a2e3488 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 @@ -16,4 +16,5 @@ data class BindDeviceRequest( @SerializedName("udfParameters") val udfParameters: String = "{}", @SerializedName("merchantCustomerId") val merchantCustomerId: String, @SerializedName("deviceData") val deviceData: DeviceData, + @SerializedName("bindingType") val bindingType: BindingType, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt index f359636938..b7b04ac017 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceResponse.kt @@ -11,10 +11,20 @@ import com.google.gson.annotations.SerializedName data class BindDeviceResponse( @SerializedName("merchantCustomerId") val merchantCustomerId: String, + @SerializedName("internalDeviceId") val internalDeviceId: String, + @SerializedName("smsBindingData") val smsBindingData: SmsBindingData?, + @SerializedName("smvBindingData") val smvBindingData: SmvBindingData?, +) + +data class SmvBindingData( + @SerializedName("smvContent") val smvContent: String, + @SerializedName("aggregator") val aggregator: String, +) + +data class SmsBindingData( @SerializedName("serviceProviders") val serviceProviders: List, @SerializedName("smsContent") val smsContent: String, @SerializedName("expiryTimestamp") val expiryTimestamp: String, - @SerializedName("internalDeviceId") val internalDeviceId: String, ) data class ServiceProvider( diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt index 4777e21ba5..19d35aae1e 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindDeviceStatusRequest.kt @@ -14,4 +14,5 @@ data class BindDeviceStatusRequest( @SerializedName("deviceData") val deviceData: DeviceData, @SerializedName("internalDeviceId") val internalDeviceId: String, @SerializedName("merchantCustomerId") val merchantCustomerId: String, + @SerializedName("bindingType") val bindingType: BindingType, ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt new file mode 100644 index 0000000000..5bb96afd21 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/BindingType.kt @@ -0,0 +1,13 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.onboarding.binding.model.network + +enum class BindingType { + SMV, + SMS, +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt index 0785f88104..027b9ff038 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/NaviPayOnboardingConfig.kt @@ -16,21 +16,14 @@ data class NaviPayOnboardingConfig( ) data class NaviPayOnboardingConfigContent( + @SerializedName("simSelectionSheetTitleV2") val simSelectionSheetTitleV2: String = "Verify your number %s", - @SerializedName("singleSimSelectionSheetDescription") - val singleSimSelectionSheetDescriptionV2: String = - "We will send an SMS from your number. Please do not press back or close the app.", - @SerializedName("multipleSimSelectionSheetDescription") - val multipleSimSelectionSheetDescriptionV2: String = - "We will send an SMS from your number. Please select the linked SIM and do not close the app.", @SerializedName("simSelectionSheetTitle") val simSelectionSheetTitle: String = "Validate mobile number", @SerializedName("simSelectionSheetDescription") val simSelectionSheetDescription: String = "We will send an SMS from {{PHONE_NUMBER}}.", @SerializedName("multipleSimSheetDescriptionLine2") val multipleSimSheetDescriptionLine2: String = "Please select your sim provider.", - @SerializedName("smsChargesText") - val smsChargesText: String = "*Standard SMS charges will apply", @SerializedName("declineDeviceBindingTitle") val declineDeviceBindingTitle: String = "Mobile verification failed", @SerializedName("declineDeviceBindingDescription") @@ -42,9 +35,6 @@ data class NaviPayOnboardingConfigContent( val declineDeviceBindingSecondaryButtonText: String = "Cancel", @SerializedName("deviceBindingProgressTitle") val deviceBindingProgressTitle: String = "Verifying your mobile number", - @SerializedName("deviceBindingProgressDescription") - val deviceBindingProgressDescription: String = - "Please do not press back or close the app. This usually takes a few seconds.", @SerializedName("deviceBindingProgressTitleList") val deviceBindingProgressTitleList: List = listOf("Verifying your number", "Please wait, taking longer than usual"), @@ -70,4 +60,6 @@ data class NaviPayOnboardingConfigContent( @SerializedName("smsVerificationLocallyExpiredDescription") val smsVerificationLocallyExpiredDescription: String = "NPCI verification request has expired. Please try again.", + @SerializedName("smvSupportedProviders") + val smvSupportedProviders: List = listOf("Jio", "VI"), ) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/SmvInitiateResponse.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/SmvInitiateResponse.kt new file mode 100644 index 0000000000..d57e1ae6d9 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/network/SmvInitiateResponse.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.onboarding.binding.model.network + +import com.google.gson.annotations.SerializedName + +data class SmvInitiateResponse(@SerializedName("code") val code: String) diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt new file mode 100644 index 0000000000..b63605115f --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/DeviceBindingState.kt @@ -0,0 +1,30 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.onboarding.binding.model.view + +sealed class DeviceBindingState { + data object None : DeviceBindingState() + + data object Initiated : DeviceBindingState() + + data object Binding : DeviceBindingState() + + data object Verifying : DeviceBindingState() + + data object Success : DeviceBindingState() + + data object Failure : DeviceBindingState() + + data object DeclineBinding : DeviceBindingState() + + companion object { + fun DeviceBindingState.isVerificationOngoing(): Boolean { + return this == Binding || this == Verifying + } + } +} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmsVerificationState.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmsVerificationState.kt deleted file mode 100644 index 21beedcbac..0000000000 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmsVerificationState.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * - * * Copyright © 2024 by Navi Technologies Limited - * * All rights reserved. Strictly confidential - * - */ - -package com.navi.pay.onboarding.binding.model.view - -sealed class SmsVerificationState { - data object None : SmsVerificationState() - - data object Initiated : SmsVerificationState() - - data object Sending : SmsVerificationState() - - data object Verifying : SmsVerificationState() - - data object Success : SmsVerificationState() - - data object Failure : SmsVerificationState() - - data object DeclineBinding : SmsVerificationState() - - companion object { - fun SmsVerificationState.isSmsVerificationOngoing(): Boolean { - return this == Sending || this == Verifying - } - } -} diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmvEligibilityStatus.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmvEligibilityStatus.kt new file mode 100644 index 0000000000..45bd04f332 --- /dev/null +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/model/view/SmvEligibilityStatus.kt @@ -0,0 +1,10 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.pay.onboarding.binding.model.view + +data class SmvEligibilityStatus(val isEligible: Boolean, val isSmsPermissionEnabled: Boolean) 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 cc34759cf4..aa5ef73480 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 @@ -10,6 +10,7 @@ package com.navi.pay.onboarding.binding.repository import com.navi.common.checkmate.model.MetricInfo import com.navi.common.network.models.RepoResult import com.navi.common.network.retrofit.ResponseCallback +import com.navi.pay.network.retrofit.NaviPayExternalRetrofitService import com.navi.pay.network.retrofit.NaviPayRetrofitService import com.navi.pay.onboarding.binding.model.network.BindDeviceRequest import com.navi.pay.onboarding.binding.model.network.BindDeviceResponse @@ -18,10 +19,14 @@ import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusResponse import com.navi.pay.onboarding.binding.model.network.DeclineDeviceRequest import com.navi.pay.onboarding.binding.model.network.DeclineDeviceResponse import javax.inject.Inject +import retrofit2.Response class NaviPayOnboardingRepository @Inject -constructor(private val naviPayRetrofitService: NaviPayRetrofitService) : ResponseCallback() { +constructor( + private val naviPayRetrofitService: NaviPayRetrofitService, + private val naviPayExternalRetrofitService: NaviPayExternalRetrofitService, +) : ResponseCallback() { suspend fun bindDevice( bindDeviceRequest: BindDeviceRequest, @@ -58,4 +63,8 @@ constructor(private val naviPayRetrofitService: NaviPayRetrofitService) : Respon metricInfo = metricInfo, ) } + + suspend fun initiateSmv(url: String, params: Map): Response { + return naviPayExternalRetrofitService.initiateSmv(url = url, params = params) + } } 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 b8206f427a..e64edf5bf9 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 @@ -45,7 +45,6 @@ import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.common.model.view.NaviPayScreenType import com.navi.pay.common.model.view.NaviPermissionResult import com.navi.pay.common.model.view.PermissionResult -import com.navi.pay.common.model.view.SimInfo import com.navi.pay.common.model.view.rememberMultiplePermissions import com.navi.pay.common.theme.color.NaviPayColor import com.navi.pay.common.utils.ErrorEventHandler @@ -57,7 +56,6 @@ import com.navi.pay.destinations.NaviPayPermissionScreenV2Destination import com.navi.pay.onboarding.account.add.model.view.EnabledAccountAdditionTypes import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetType import com.navi.pay.onboarding.binding.model.view.OnboardingDeviceData -import com.navi.pay.onboarding.binding.model.view.SmsVerificationState import com.navi.pay.onboarding.binding.viewmodel.NaviPayOnboardingViewModel import com.navi.pay.onboarding.common.NaviPayOnBoardingActions import com.navi.pay.permission.model.view.PermissionState @@ -143,25 +141,11 @@ fun NaviPayOnboardingScreen( val coroutineScope = rememberCoroutineScope() val bottomSheetStateHolder by naviPayOnboardingViewModel.bottomSheetStateHolder.collectAsStateWithLifecycle() - val smsVerificationState by - naviPayOnboardingViewModel.smsVerificationState.collectAsStateWithLifecycle() - val selectedSimInfo by naviPayOnboardingViewModel.selectedSimInfo.collectAsStateWithLifecycle() val enabledAccountTypes by naviPayOnboardingViewModel.enabledAccountTypes.collectAsStateWithLifecycle() val preferredBankCode by naviPayOnboardingViewModel.preferredBankCode.collectAsStateWithLifecycle() - selectedSimInfo?.let { - ObserverSmsVerificationState( - naviPayOnboardingActivity = naviPayOnboardingActivity, - selectedSimInfo = it, - smsVerificationState = smsVerificationState, - naviPayOnboardingViewModel = naviPayOnboardingViewModel, - naviPayAnalytics = naviPayAnalytics, - onboardingSource = onboardingSource, - ) - } - val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true, confirmValueChange = { false }) @@ -224,7 +208,7 @@ fun NaviPayOnboardingScreen( context = naviPayOnboardingActivity.applicationContext ), ) - naviPayOnboardingViewModel.startSimBinding() + naviPayOnboardingViewModel.confirmSimSelection() } NaviPermissionResult.HardDenied -> { naviPayAnalytics.onPermissionDenied( @@ -595,54 +579,6 @@ private fun InitLifecycleListener(naviPayOnboardingViewModel: NaviPayOnboardingV } } -@Composable -private fun ObserverSmsVerificationState( - naviPayOnboardingActivity: NaviPayOnboardingActivity, - selectedSimInfo: SimInfo, - smsVerificationState: SmsVerificationState, - naviPayOnboardingViewModel: NaviPayOnboardingViewModel, - naviPayAnalytics: NaviPayAnalytics.NaviPaySetup, - onboardingSource: String, -) { - when (smsVerificationState) { - SmsVerificationState.Sending -> { - LaunchedEffect(Unit) { - val serviceProviderNumberList = - naviPayOnboardingViewModel.bindDeviceResponse.serviceProviders.map { it.number } - NaviPayCommonUtils.sendSMS( - destinationNumberList = serviceProviderNumberList, - messageContent = naviPayOnboardingViewModel.bindDeviceResponse.smsContent, - subscriptionId = selectedSimInfo.subscriptionId.toInt(), - activityContext = naviPayOnboardingActivity.baseContext, - ) - - naviPayAnalytics.startSimBinding( - simInfo = selectedSimInfo, - onboardingSource = onboardingSource, - naviPaySessionAttributes = - naviPayOnboardingViewModel.getNaviPaySessionAttributes(), - ) - naviPayOnboardingViewModel.checkForSMSSentAndStartPolling( - vmnNumbers = serviceProviderNumberList, - smsContent = naviPayOnboardingViewModel.bindDeviceResponse.smsContent, - ) - } - } - SmsVerificationState.Success -> { - LaunchedEffect(Unit) { - naviPayAnalytics.simBindingSuccess( - onboardingSource = onboardingSource, - naviPaySessionAttributes = - naviPayOnboardingViewModel.getNaviPaySessionAttributes(), - ) - naviPayOnboardingViewModel.addScanAndPayLauncherWidget() - naviPayOnboardingViewModel.handleNavigationOnCustomerStatus() - } - } - else -> Unit - } -} - private fun onAllPermissionsGrantedForOnboarding( applicationContext: Context, naviPayOnboardingViewModel: NaviPayOnboardingViewModel, diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt index ecdbeaccca..1b8fd4e01f 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/ui/SimSelectionContent.kt @@ -40,9 +40,10 @@ import com.navi.pay.common.model.view.SimInfo import com.navi.pay.common.model.view.displayableIndex import com.navi.pay.common.theme.color.NaviPayColor import com.navi.pay.common.ui.ImageWithBackground -import com.navi.pay.common.ui.ThemeRoundedButton +import com.navi.pay.common.ui.LoaderRoundedButton import com.navi.pay.onboarding.binding.viewmodel.NaviPayOnboardingViewModel import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS +import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE import com.navi.pay.utils.clickableDebounce @Composable @@ -66,15 +67,8 @@ fun SimSelectionContent( ) } - val description = remember { - if (currentSimInfoList.size > 1) { - naviPayOnboardingViewModel.naviPayOnboardingConfig.config - .multipleSimSelectionSheetDescriptionV2 - } else { - naviPayOnboardingViewModel.naviPayOnboardingConfig.config - .singleSimSelectionSheetDescriptionV2 - } - } + val isSmvEligibilityCheckOngoing by + naviPayOnboardingViewModel.isSmvEligibilityCheckOngoing.collectAsStateWithLifecycle() Column( modifier = @@ -101,7 +95,13 @@ fun SimSelectionContent( Spacer(modifier = Modifier.height(8.dp)) NaviText( - text = description, + text = + stringResource( + id = + if (currentSimInfoList.size > 1) + R.string.np_multi_sim_selection_sheet_description + else R.string.np_single_sim_selection_sheet_description + ), fontSize = 14.sp, fontFamily = naviFontFamily, fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), @@ -144,7 +144,7 @@ fun SimSelectionContent( Spacer(modifier = Modifier.height(8.dp)) NaviText( - text = naviPayOnboardingViewModel.naviPayOnboardingConfig.config.smsChargesText, + text = stringResource(R.string.sms_verification_charges_info), fontSize = 10.sp, fontFamily = naviFontFamily, fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), @@ -153,9 +153,12 @@ fun SimSelectionContent( Spacer(modifier = Modifier.height(32.dp)) - ThemeRoundedButton( + LoaderRoundedButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.verify), + enabled = !isSmvEligibilityCheckOngoing, + lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE, + showLoader = isSmvEligibilityCheckOngoing, ) { if (currentSimInfoList.size == 1) { naviPayAnalytics.onSingleSimConfirmationClick( 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 0ec8682642..85a6076083 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 @@ -16,12 +16,15 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.navi.design.font.FontWeightEnum import com.navi.design.font.getFontWeight import com.navi.design.font.naviFontFamily @@ -29,10 +32,11 @@ import com.navi.naviwidgets.extensions.NaviText import com.navi.pay.R import com.navi.pay.analytics.NaviPayAnalytics import com.navi.pay.common.theme.color.NaviPayColor -import com.navi.pay.common.ui.ThemeRoundedButton +import com.navi.pay.common.ui.LoaderRoundedButton import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetType import com.navi.pay.onboarding.binding.viewmodel.NaviPayOnboardingViewModel import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS +import com.navi.pay.utils.NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE @Composable fun SingleSimConfirmationBottomSheetContent( @@ -54,10 +58,8 @@ fun SingleSimConfirmationBottomSheetContent( ) } - val description = remember { - naviPayOnboardingViewModel.naviPayOnboardingConfig.config - .singleSimSelectionSheetDescriptionV2 - } + val isSmvEligibilityCheckOngoing by + naviPayOnboardingViewModel.isSmvEligibilityCheckOngoing.collectAsStateWithLifecycle() Column( modifier = @@ -82,7 +84,7 @@ fun SingleSimConfirmationBottomSheetContent( Spacer(modifier = Modifier.height(8.dp)) NaviText( - text = description, + text = stringResource(id = R.string.np_single_sim_selection_sheet_description), fontSize = 14.sp, fontFamily = naviFontFamily, fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), @@ -102,17 +104,21 @@ fun SingleSimConfirmationBottomSheetContent( Spacer(modifier = Modifier.height(32.dp)) - ThemeRoundedButton( + LoaderRoundedButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.verify), - ) { - val selectedSim = currentSimInfoList[0] - naviPayAnalytics.onSingleSimConfirmationClick( - simInfo = selectedSim, - isFirstTimeUserExperience = false, - ) - naviPayOnboardingViewModel.selectSim(selectedSimInfo = selectedSim) - naviPayOnboardingViewModel.confirmSimSelection() - } + enabled = !isSmvEligibilityCheckOngoing, + lottieFileName = NAVI_PAY_PRIMARY_CTA_LOADER_LOTTIE, + showLoader = isSmvEligibilityCheckOngoing, + onClick = { + val selectedSim = currentSimInfoList[0] + naviPayAnalytics.onSingleSimConfirmationClick( + simInfo = selectedSim, + isFirstTimeUserExperience = false, + ) + naviPayOnboardingViewModel.selectSim(selectedSimInfo = selectedSim) + naviPayOnboardingViewModel.confirmSimSelection() + }, + ) } } 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 13ebc49d14..653bc7ac03 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 @@ -10,6 +10,7 @@ package com.navi.pay.onboarding.binding.viewmodel import android.app.Activity import android.content.Intent import android.os.Build +import android.telephony.SubscriptionManager import androidx.activity.result.ActivityResultLauncher import androidx.annotation.StringRes import androidx.lifecycle.SavedStateHandle @@ -17,8 +18,11 @@ import androidx.lifecycle.viewModelScope import com.google.gson.reflect.TypeToken import com.navi.base.AppServiceManager import com.navi.base.cache.datastore.DataStoreHelper +import com.navi.base.cache.model.NaviCacheEntity +import com.navi.base.cache.repository.NaviCacheRepository 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 @@ -29,8 +33,10 @@ 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.PermissionSubmitRepository +import com.navi.common.usecase.LitmusExperimentsUseCase import com.navi.common.utils.EMPTY import com.navi.common.utils.NaviApiPoller +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 @@ -72,14 +78,16 @@ import com.navi.pay.onboarding.binding.model.network.BindDeviceRequest import com.navi.pay.onboarding.binding.model.network.BindDeviceResponse import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusRequest import com.navi.pay.onboarding.binding.model.network.BindDeviceStatusResponse +import com.navi.pay.onboarding.binding.model.network.BindingType import com.navi.pay.onboarding.binding.model.network.CustomerRequest import com.navi.pay.onboarding.binding.model.network.DeclineDeviceRequest import com.navi.pay.onboarding.binding.model.network.NaviPayOnboardingConfig +import com.navi.pay.onboarding.binding.model.view.DeviceBindingState +import com.navi.pay.onboarding.binding.model.view.DeviceBindingState.Companion.isVerificationOngoing import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetStateHolder import com.navi.pay.onboarding.binding.model.view.NaviPayOnboardingBottomSheetType import com.navi.pay.onboarding.binding.model.view.OnboardingDeviceData -import com.navi.pay.onboarding.binding.model.view.SmsVerificationState -import com.navi.pay.onboarding.binding.model.view.SmsVerificationState.Companion.isSmsVerificationOngoing +import com.navi.pay.onboarding.binding.model.view.SmvEligibilityStatus import com.navi.pay.onboarding.binding.repository.NaviPayOnboardingRepository import com.navi.pay.onboarding.common.NaviPayOnBoardingActions import com.navi.pay.onboarding.common.NaviPayOnboardingActionsType @@ -91,7 +99,9 @@ import com.navi.pay.permission.utils.PermissionUtils import com.navi.pay.tstore.list.usecase.SyncOrderHistoryUseCase import com.navi.pay.utils.DS_KEY_NAVI_PAY_CUSTOMER_STATUS 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 +import com.navi.pay.utils.NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED import com.navi.pay.utils.NAVI_PAY_ENCRYPT_SHARED_PREF_DATA_KEYS import com.navi.pay.utils.NAVI_PAY_GREEN_TICK_LOTTIE import com.navi.pay.utils.NAVI_PAY_NON_ENCRYPT_SHARED_PREF_DATA_KEYS @@ -110,6 +120,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.BufferOverflow @@ -123,6 +134,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.serialization.json.Json +import org.json.JSONObject @HiltViewModel class NaviPayOnboardingViewModel @@ -153,7 +165,9 @@ constructor( private val liteAccountSyncUseCase: LiteAccountSyncUseCase, private val permissionSubmitRepository: PermissionSubmitRepository, private val syncUpiLiteMandateInfoUseCase: SyncUpiLiteMandateInfoUseCase, - private val savedStateHandle: SavedStateHandle, + private val litmusExperimentsUseCase: LitmusExperimentsUseCase, + private val naviCacheRepository: NaviCacheRepository, + savedStateHandle: SavedStateHandle, ) : NaviPayBaseVM() { private val naviPayAnalytics: NaviPayAnalytics.NaviPaySetup = @@ -175,12 +189,9 @@ constructor( ) val onboardingAction = _onboardingAction.asSharedFlow() - private val _isFirstTimeUserExperience = MutableStateFlow(false) - val isFirstTimeUserExperience = _isFirstTimeUserExperience.asStateFlow() + private val isFirstTimeUserExperience = MutableStateFlow(false) - private val _smsVerificationState = - MutableStateFlow(SmsVerificationState.None) - val smsVerificationState = _smsVerificationState.asStateFlow() + private val deviceBindingState = MutableStateFlow(DeviceBindingState.None) private val _simInfoList = MutableStateFlow( @@ -199,7 +210,7 @@ constructor( val requestPermission = _requestPermission.asSharedFlow() private var deviceData: DeviceData? = null - lateinit var bindDeviceResponse: BindDeviceResponse + private lateinit var bindDeviceResponse: BindDeviceResponse private val deferredApiCallList = mutableListOf>() private val _isHardUpdateRequired = MutableSharedFlow(replay = 1) @@ -235,6 +246,9 @@ constructor( private val _finishActivity = MutableSharedFlow(replay = 1) val finishActivity = _finishActivity.asSharedFlow() + private val _isSmvEligibilityCheckOngoing = MutableStateFlow(false) + val isSmvEligibilityCheckOngoing = _isSmvEligibilityCheckOngoing.asStateFlow() + private val _redirectToSetPinAndFinishActivity = MutableSharedFlow(replay = 1) val redirectToSetPinAndFinishActivity = _redirectToSetPinAndFinishActivity.asSharedFlow() @@ -246,7 +260,12 @@ constructor( var naviPayOnboardingConfig = NaviPayOnboardingConfig() private var isAwayFromMainScreen = false - private val naviApiPoller by lazy { NaviApiPoller(repeatInterval = POLLING_INTERVAL) } + private val naviApiPoller by lazy { + NaviApiPoller( + repeatInterval = POLLING_INTERVAL, + totalPollingDurationInMillis = MAX_POLLING_DURATION_IN_MILLIS, + ) + } private val naviPayDefaultConfig = NaviPayDefaultConfig() val appUpgradeData = @@ -263,6 +282,7 @@ constructor( private companion object { private val POLLING_INTERVAL = 2.5.seconds + private const val MAX_POLLING_DURATION_IN_MILLIS = 60_000L private const val TAG_CUSTOMER_SETUP_ERROR = "CUSTOMER_SETUP_ERROR" } @@ -348,7 +368,7 @@ constructor( } fun updateIsFirstTimeUserExperience(isFirstTimeUserExperience: Boolean) { - _isFirstTimeUserExperience.update { isFirstTimeUserExperience } + this.isFirstTimeUserExperience.update { isFirstTimeUserExperience } } private fun updateBindingProgress(progress: Float) { @@ -470,10 +490,6 @@ constructor( showLoadingBottomSheet(isVisible = false) onSetupSuccess(customerStatus = naviPaySetupStatus.customerStatus) } - else -> { - showLoadingBottomSheet(isVisible = false) - updateOnboardingAction(onboardingAction = NaviPayOnBoardingActions.FinishScreen) - } } } } @@ -588,7 +604,7 @@ constructor( } private fun isDeviceVerificationOngoing(): Boolean { - return smsVerificationState.value.isSmsVerificationOngoing() + return deviceBindingState.value.isVerificationOngoing() } private fun showLoadingBottomSheet(isVisible: Boolean) { @@ -657,21 +673,37 @@ constructor( fun confirmSimSelection() { viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { - val multipleStatePermission = - PermissionUtils.permissionDataMap.getOrElse( - FIRST_TIME_SCREEN_PERMISSION_KEY, - defaultValue = { emptyList() }, - ) + val smvEligibilityStatus = getSmvEligibilityStatus(provider = getNetworkProvider()) - if ( - permissionStateProvider.isPermissionGranted( - permissionList = multipleStatePermission.flatMap { it.qualifierList } - ) - ) { - startSimBinding() - } else { - _requestPermission.emit(FIRST_TIME_SCREEN_PERMISSION_KEY) + if (!smvEligibilityStatus.isEligible) { + checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv = false) + return@safeLaunch } + + // SMV is eligible cases + if (!smvEligibilityStatus.isSmsPermissionEnabled) { + startSimBinding(isBindingEligibleForSmv = true) + } else { + checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv = true) + } + } + } + + private suspend fun checkForBindingPermissionAndStartBinding(isBindingEligibleForSmv: Boolean) { + val multipleStatePermission = + PermissionUtils.permissionDataMap.getOrElse( + FIRST_TIME_SCREEN_PERMISSION_KEY, + defaultValue = { emptyList() }, + ) + + if ( + permissionStateProvider.isPermissionGranted( + permissionList = multipleStatePermission.flatMap { it.qualifierList } + ) + ) { + startSimBinding(isBindingEligibleForSmv = isBindingEligibleForSmv) + } else { + _requestPermission.emit(FIRST_TIME_SCREEN_PERMISSION_KEY) } } @@ -757,145 +789,205 @@ constructor( fun getNaviPaySessionAttributes(): Map = naviPaySessionHelper.getNaviPaySessionAttributes() - fun startSimBinding() { - viewModelScope.safeLaunch(Dispatchers.IO) { - naviPayAnalytics.onPermissionGranted( + private suspend fun startSimBinding(isBindingEligibleForSmv: Boolean) { + naviPayAnalytics.onPermissionGranted( + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + smsPermissionGiven = true, + phonePermissionGiven = true, + ) + if (selectedSimInfo.value == null || userPhoneNumber.isBlank()) { + notifyError() + naviPayAnalytics.onSimDataNotFoundBottomDevEvent( + simInfoList = simInfoList.value, + userPhoneNumber = userPhoneNumber, onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), - smsPermissionGiven = true, - phonePermissionGiven = true, ) - if (selectedSimInfo.value == null || userPhoneNumber.isBlank()) { - notifyError() - naviPayAnalytics.onSimDataNotFoundBottomDevEvent( - simInfoList = simInfoList.value, - userPhoneNumber = userPhoneNumber, - onboardingSource = onboardingSource.value, - naviPaySessionAttributes = getNaviPaySessionAttributes(), - ) - return@safeLaunch - } - _bindingProgress.update { 0f } - updateBottomSheetUIState( - showBottomSheet = true, - bottomSheetUIState = - NaviPayOnboardingBottomSheetType.BindingInProgressBottomSheet( - rolodexTitleList = - naviPayOnboardingConfig.config.deviceBindingProgressTitleList, - descriptionText = - naviPayOnboardingConfig.config.deviceBindingProgressDescription, + 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 + ), + ), + ) + updateDeviceBindingState(DeviceBindingState.Initiated) + + // Step 1: Get customer API call to get merchant customer ID + deviceData = + DeviceData( + deviceFingerPrint = "", + provider = + NetworkProvider( + ssid = selectedSimInfo.value?.subscriptionId ?: EMPTY, + phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber", + simSlot = selectedSimInfo.value?.simSlotIndex.toString(), + name = selectedSimInfo.value?.carrierName ?: EMPTY, ), - ) - updateSmsVerificationState(SmsVerificationState.Initiated) - - // Step 1: Get customer API call to get merchant customer ID - deviceData = - DeviceData( - deviceFingerPrint = "", - provider = - NetworkProvider( - ssid = selectedSimInfo.value?.subscriptionId ?: EMPTY, - phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber", - simSlot = selectedSimInfo.value?.simSlotIndex.toString(), - name = selectedSimInfo.value?.carrierName ?: EMPTY, - ), - deviceId = onboardingDeviceData.deviceId, - packageName = onboardingDeviceData.packageName, - ) - - naviPayAnalytics.onGetCustomerCallBeforeBindingStarts( - deviceData = deviceData, - onboardingSource = onboardingSource.value, - naviPaySessionAttributes = getNaviPaySessionAttributes(), + deviceId = onboardingDeviceData.deviceId, + packageName = onboardingDeviceData.packageName, ) - val customerAPIResponse = - commonRepository.getCustomer( - customerRequest = CustomerRequest(deviceData = deviceData!!), - metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), - ) + naviPayAnalytics.onGetCustomerCallBeforeBindingStarts( + deviceData = deviceData, + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) - if (!customerAPIResponse.isSuccessWithData()) { - updateSmsVerificationState(SmsVerificationState.Failure) - onBindingError(customerAPIResponse) - return@safeLaunch + val customerAPIResponse = + commonRepository.getCustomer( + customerRequest = CustomerRequest(deviceData = deviceData!!), + metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), + ) + + if (!customerAPIResponse.isSuccessWithData()) { + updateDeviceBindingState(DeviceBindingState.Failure) + onBindingError(customerAPIResponse) + return + } + + val customerResponse = customerAPIResponse.data!! + + // Step 2: Bind device API call to get SMS content & VMNs list + + val provider = getNetworkProvider() + + val deviceDetails = + DeviceDetails( + deviceId = onboardingDeviceData.deviceId, + manufacturer = Build.MANUFACTURER, + model = Build.MODEL, + osVersion = Build.VERSION.SDK_INT.toString(), + packageName = onboardingDeviceData.packageName, + provider = provider, + ) + + naviPayAnalytics.onBindDeviceCallForBinding( + deviceDetails = deviceDetails, + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + + val bindDeviceAPIResponse = + naviPayOnboardingRepository.bindDevice( + bindDeviceRequest = + BindDeviceRequest( + deviceData = deviceData!!, + deviceDetails = deviceDetails, + merchantCustomerId = customerResponse.merchantCustomerId, + bindingType = + if (isBindingEligibleForSmv) BindingType.SMV else BindingType.SMS, + ), + metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), + ) + + if (!bindDeviceAPIResponse.isSuccessWithData()) { + updateDeviceBindingState(DeviceBindingState.Failure) + if (bindDeviceAPIResponse.errors?.getOrNull(0)?.code == NON_VERIFIED_SENDER_LOGIN) { + handleCaseForNonVerifiedSenderLogin(bindDeviceAPIResponse = bindDeviceAPIResponse) + } else { + onBindingError(bindDeviceAPIResponse) } + return + } - val customerResponse = customerAPIResponse.data!! + this@NaviPayOnboardingViewModel.bindDeviceResponse = bindDeviceAPIResponse.data!! - // Step 2: Bind device API call to get SMS content & VMNs list - - val deviceDetails = - DeviceDetails( - deviceId = onboardingDeviceData.deviceId, - manufacturer = Build.MANUFACTURER, - model = Build.MODEL, - osVersion = Build.VERSION.SDK_INT.toString(), - packageName = onboardingDeviceData.packageName, - provider = - NetworkProvider( - ssid = selectedSimInfo.value?.subscriptionId ?: EMPTY, - phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber", - simSlot = selectedSimInfo.value?.simSlotIndex.toString(), - name = selectedSimInfo.value?.carrierName ?: EMPTY, - ), - ) - - naviPayAnalytics.onBindDeviceCallForBinding( - deviceDetails = deviceDetails, - onboardingSource = onboardingSource.value, - naviPaySessionAttributes = getNaviPaySessionAttributes(), - ) - - val bindDeviceAPIResponse = - naviPayOnboardingRepository.bindDevice( - bindDeviceRequest = - BindDeviceRequest( - deviceData = deviceData!!, - deviceDetails = deviceDetails, - merchantCustomerId = customerResponse.merchantCustomerId, - ), - metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), - ) - - if (!bindDeviceAPIResponse.isSuccessWithData()) { - updateSmsVerificationState(SmsVerificationState.Failure) - if (bindDeviceAPIResponse.errors?.getOrNull(0)?.code == NON_VERIFIED_SENDER_LOGIN) { - handleCaseForNonVerifiedSenderLogin( - bindDeviceAPIResponse = bindDeviceAPIResponse - ) - } else { - onBindingError(bindDeviceAPIResponse) - } - return@safeLaunch - } - - this@NaviPayOnboardingViewModel.bindDeviceResponse = bindDeviceAPIResponse.data!! - - naviPayAnalytics.onSmsBindingVMNReceived( - serviceProviders = - this@NaviPayOnboardingViewModel.bindDeviceResponse.serviceProviders, - onboardingSource = onboardingSource.value, - naviPaySessionAttributes = getNaviPaySessionAttributes(), - ) - - // Step 3: Send SMS to VMNs and trigger startStatusPolling() - updateSmsVerificationState(SmsVerificationState.Sending) + if (isBindingEligibleForSmv) { + processBindDeviceResponseForSmv() + } else { + processBindDeviceResponseForSms(provider = provider) } } - fun checkForSMSSentAndStartPolling(vmnNumbers: List, smsContent: String) { + private suspend fun processBindDeviceResponseForSms(provider: NetworkProvider) { + naviPayAnalytics.onSmsBindingVMNReceived( + serviceProviders = bindDeviceResponse.smsBindingData!!.serviceProviders, + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + + // Step 3: Send SMS to VMNs and trigger startStatusPolling() + updateDeviceBindingState(DeviceBindingState.Binding) + + sendSms(provider = provider) + + checkForSMSSentAndStartPolling() + } + + private suspend fun sendSms(provider: NetworkProvider) { + + val serviceProviderNumberList = + bindDeviceResponse.smsBindingData!!.serviceProviders.map { it.number } + + smsManager.sendSms( + destinationNumberList = serviceProviderNumberList, + messageContent = bindDeviceResponse.smsBindingData!!.smsContent, + subscriptionId = selectedSimInfo.value?.subscriptionId?.toIntOrNull() ?: 0, + ) + + naviPayAnalytics.startSimBinding( + provider = provider, + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + } + + private suspend fun processBindDeviceResponseForSmv() { + + updateDeviceBindingState(DeviceBindingState.Binding) + naviPayAnalytics.onSmvInitiated() + val smvContent = bindDeviceResponse.smvBindingData?.smvContent ?: "" + + val initiateSmvResponse = + naviPayOnboardingRepository.initiateSmv( + params = + mapOf( + "clientId" to BuildConfig.NAVIPAY_SMV_CLIENT_ID, + "transactionId" to smvContent, + "msisdn" to "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber", + ), + url = BuildConfig.NAVIPAY_SMV_BASE_URL, + ) + + naviPayAnalytics.onInitiateSmvResponse( + smvInitiateResponse = initiateSmvResponse.raw().toString() + ) + naviPayNetworkConnectivity.unbindNetwork() + + if (initiateSmvResponse.code() == 200 && initiateSmvResponse.body() == null) { + startStatusPolling(bindingType = BindingType.SMV) + } else { + // Mark in cache that SMV was triggered & failed + markSmvTriggeredAndFailed() + updateBottomSheetUIState(showBottomSheet = false) + updateDeviceBindingState(deviceBindingState = DeviceBindingState.Failure) + notifyError(errorConfig = getGenericErrorConfig().copy(cancelable = false)) + } + } + + private fun checkForSMSSentAndStartPolling() { viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { naviPayAnalytics.checkForSMSSentInDevice( onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), ) + val vmnNumbers = bindDeviceResponse.smsBindingData!!.serviceProviders.map { it.number } + val smsContent = bindDeviceResponse.smsBindingData!!.smsContent val startTime = System.currentTimeMillis() while (System.currentTimeMillis() - startTime < SMS_SENT_CHECK_TIMEOUT) { delay(timeMillis = 1000) if (checkIfMessageIsSent(vmnNumbers = vmnNumbers, smsContent = smsContent)) { - startStatusPolling() + startStatusPolling(bindingType = BindingType.SMS) return@safeLaunch } } @@ -912,7 +1004,7 @@ constructor( naviPaySessionAttributes = getNaviPaySessionAttributes(), ) - updateSmsVerificationState(SmsVerificationState.Failure) + updateDeviceBindingState(DeviceBindingState.Failure) updateBottomSheetUIState(showBottomSheet = false) notifyError( errorConfig = @@ -945,27 +1037,43 @@ constructor( private suspend fun checkIfMessageIsSent(vmnNumbers: List, smsContent: String) = smsManager.sentSmsVerification(numberList = vmnNumbers, messageContent = smsContent) - @Suppress("UNCHECKED_CAST") - private suspend fun startStatusPolling() { + private suspend fun startStatusPolling(bindingType: BindingType) { - updateSmsVerificationState(SmsVerificationState.Verifying) - naviPayAnalytics.smsSentSuccessfullyStartingPolling( + updateDeviceBindingState(DeviceBindingState.Verifying) + naviPayAnalytics.onStartBindStatusPolling( onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), + bindingType = bindingType, ) naviApiPoller - .startPolling { - naviPayOnboardingRepository.getBindDeviceStatus( - bindDeviceStatusRequest = - BindDeviceStatusRequest( - deviceData = deviceData!!, - internalDeviceId = bindDeviceResponse.internalDeviceId, - merchantCustomerId = bindDeviceResponse.merchantCustomerId, - ), - metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), - ) - } + .startPolling( + onTimeout = { + updateBottomSheetUIState(showBottomSheet = false) + notifyError( + errorConfig = getGenericErrorConfig().copy(cancelable = false) + ) // TODO: Update messaging + naviPayAnalytics.simBindingFailure( + errorType = "Binding timeout", + error = null, + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + updateDeviceBindingState(DeviceBindingState.Failure) + }, + onPollExecute = { + naviPayOnboardingRepository.getBindDeviceStatus( + bindDeviceStatusRequest = + BindDeviceStatusRequest( + deviceData = deviceData!!, + internalDeviceId = bindDeviceResponse.internalDeviceId, + merchantCustomerId = bindDeviceResponse.merchantCustomerId, + bindingType = bindingType, + ), + metricInfo = getMetricInfo(screenName = NAVI_PAY_SETUP), + ) + }, + ) .collectLatest { try { if (!naviApiPoller.isPollerActive()) { @@ -979,6 +1087,7 @@ constructor( deviceId = onboardingDeviceData.deviceId, packageName = onboardingDeviceData.packageName, merchantCustomerId = bindDeviceResponse.merchantCustomerId, + bindingType = bindingType, ) } catch (exception: Exception) { updateBottomSheetUIState(showBottomSheet = false) @@ -989,7 +1098,7 @@ constructor( onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), ) - updateSmsVerificationState(SmsVerificationState.Failure) + updateDeviceBindingState(DeviceBindingState.Failure) naviApiPoller.stopPolling() } } @@ -1000,20 +1109,24 @@ constructor( deviceId: String, packageName: String, merchantCustomerId: String, + bindingType: BindingType, ) { if (!bindDeviceStatusAPIResponse.isSuccessWithData()) { when (bindDeviceStatusAPIResponse.errors?.getOrNull(0)?.code) { SMS_VERIFICATION_PENDING -> {} else -> { naviPayAnalytics.simBindingFailure( - errorType = SmsVerificationState.Failure.toString(), + errorType = DeviceBindingState.Failure.toString(), error = bindDeviceStatusAPIResponse.errors?.firstOrNull(), onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), ) - updateSmsVerificationState(SmsVerificationState.Failure) + updateDeviceBindingState(DeviceBindingState.Failure) naviApiPoller.stopPolling() onBindingError(bindDeviceStatusAPIResponse) + if (bindingType == BindingType.SMV) { + markSmvTriggeredAndFailed() + } } } return @@ -1056,7 +1169,32 @@ constructor( deviceInfoProvider.saveMerchantCustomerId(merchantCustomerId = merchantCustomerId) - updateSmsVerificationState(SmsVerificationState.Success) + handleDeviceBindingSuccess() + } + + private fun handleDeviceBindingSuccess() { + + updateDeviceBindingState(DeviceBindingState.Success) + + naviPayAnalytics.simBindingSuccess( + onboardingSource = onboardingSource.value, + naviPaySessionAttributes = getNaviPaySessionAttributes(), + ) + + naviPayWidgetManager.addScanAndPayLauncherWidget() + + handleNavigationOnCustomerStatus() + } + + private suspend fun markSmvTriggeredAndFailed() { + naviCacheRepository.save( + naviCacheEntity = + NaviCacheEntity( + key = NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED, + value = "true", + version = 1, + ) + ) } fun declineDeviceBinding() { @@ -1070,7 +1208,7 @@ constructor( naviApiPoller.stopPolling() - updateSmsVerificationState(SmsVerificationState.DeclineBinding) + updateDeviceBindingState(DeviceBindingState.DeclineBinding) naviPayAnalytics.onDeclineDevicePopupShown( onboardingSource = onboardingSource.value, naviPaySessionAttributes = getNaviPaySessionAttributes(), @@ -1115,7 +1253,7 @@ constructor( } } - fun handleNavigationOnCustomerStatus() { + private fun handleNavigationOnCustomerStatus() { viewModelScope.launch(Dispatchers.IO) { val updatedDeviceData = deviceData ?: deviceInfoProvider.getDeviceData() @@ -1268,21 +1406,21 @@ constructor( ) } - private fun updateSmsVerificationState( - smsVerificationState: SmsVerificationState = SmsVerificationState.None + private fun updateDeviceBindingState( + deviceBindingState: DeviceBindingState = DeviceBindingState.None ) { - _smsVerificationState.value = smsVerificationState - when (smsVerificationState) { - SmsVerificationState.Success -> { + this.deviceBindingState.value = deviceBindingState + when (deviceBindingState) { + DeviceBindingState.Success -> { updateBindingProgress(progress = 1f) } - SmsVerificationState.Initiated -> { + DeviceBindingState.Initiated -> { updateBindingProgress(progress = 0.05f) } - SmsVerificationState.Sending -> { + DeviceBindingState.Binding -> { updateBindingProgress(progress = 0.65f) } - SmsVerificationState.Verifying -> { + DeviceBindingState.Verifying -> { updateBindingProgress(progress = 0.80f) } else -> { @@ -1305,6 +1443,14 @@ constructor( } } + private fun getNetworkProvider() = + NetworkProvider( + ssid = selectedSimInfo.value?.subscriptionId.orEmpty(), + phoneNumber = "$INDIA_COUNTRY_CODE_WITHOUT_PLUS$userPhoneNumber", + simSlot = selectedSimInfo.value?.simSlotIndex.toString(), + name = selectedSimInfo.value?.carrierName ?: com.navi.base.utils.EMPTY, + ) + @OptIn(ExperimentalCoroutinesApi::class) fun clearReplayCacheForHardUpdateParam() { _isHardUpdateRequired.resetReplayCache() @@ -1353,8 +1499,6 @@ constructor( } } - fun addScanAndPayLauncherWidget() = naviPayWidgetManager.addScanAndPayLauncherWidget() - private fun handleFlowSpecificOnboardingIntent() { viewModelScope.safeLaunch(coroutineDispatcherProvider.io) { val customerStatus = naviPayCustomerStatusHandler.getCustomerStatus() @@ -1516,6 +1660,104 @@ constructor( } } + private suspend fun getSmvEligibilityStatus(provider: NetworkProvider): SmvEligibilityStatus { + + // TODO: To be removed for SMV go live + return SmvEligibilityStatus(isEligible = false, isSmsPermissionEnabled = true) + + _isSmvEligibilityCheckOngoing.update { true } + + val smvLitmusExperimentResponse = + litmusExperimentsUseCase.execute(experimentName = LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING) + + val isSmvExperimentEnabled = smvLitmusExperimentResponse?.variant?.enabled.orFalse() + + val isSmsPermissionEnabled = + if (isSmvExperimentEnabled) { + val experimentPayload = + smvLitmusExperimentResponse?.variant?.payload?.get("value") as? String + + JSONObject(experimentPayload ?: "").optBoolean("sms_permission") + } else { + false + } + + if (!isSmvExperimentEnabled) { + naviPayAnalytics.onSmvExperimentDisabled() + + _isSmvEligibilityCheckOngoing.update { false } + return SmvEligibilityStatus( + isEligible = false, + isSmsPermissionEnabled = isSmsPermissionEnabled, + ) + } + + // Check if provider supports SMV + if ( + NaviPayCommonUtils.displayableCarrierName(actualCarrierName = provider.name) !in + naviPayOnboardingConfig.config.smvSupportedProviders + ) { + naviPayAnalytics.onProviderNotSupportedForSmv( + displayableCarrierName = + NaviPayCommonUtils.displayableCarrierName(actualCarrierName = provider.name), + smvSupportedProviders = naviPayOnboardingConfig.config.smvSupportedProviders, + ) + _isSmvEligibilityCheckOngoing.update { false } + return SmvEligibilityStatus( + isEligible = false, + isSmsPermissionEnabled = isSmsPermissionEnabled, + ) + } + + val isSmvTriggeredPreviously = + naviCacheRepository + .get(key = NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED) + ?.value + ?.toBoolean() ?: false + + if (isSmvTriggeredPreviously) { + naviPayAnalytics.onSmvPreviouslyTriggered() + _isSmvEligibilityCheckOngoing.update { false } + return SmvEligibilityStatus( + isEligible = false, + isSmsPermissionEnabled = isSmsPermissionEnabled, + ) + } + + // Check if SIM selected for binding is default data SIM + val defaultDataSubscriptionId = SubscriptionManager.getDefaultDataSubscriptionId() + + if (provider.ssid != defaultDataSubscriptionId.toString()) { + naviPayAnalytics.onSelectedSimIsNotDefaultDataSim( + selectedSimId = provider.ssid, + defaultDataSimId = defaultDataSubscriptionId.toString(), + ) + _isSmvEligibilityCheckOngoing.update { false } + return SmvEligibilityStatus( + isEligible = false, + isSmsPermissionEnabled = isSmsPermissionEnabled, + ) + } + + // Finally check if routing to cellular succeeded or not + val routeNetworkViaCellularResult = checkAndRouteNetworkViaCellular() + naviPayAnalytics.onRouteNetworkViaCellularResult(result = routeNetworkViaCellularResult) + + _isSmvEligibilityCheckOngoing.update { false } + return SmvEligibilityStatus( + isEligible = routeNetworkViaCellularResult, + isSmsPermissionEnabled = isSmsPermissionEnabled, + ) + } + + private suspend fun checkAndRouteNetworkViaCellular(): Boolean { + return try { + naviPayNetworkConnectivity.bindNetworkToCellular(timeout = 3.seconds) + } catch (exception: TimeoutCancellationException) { + false + } + } + override val screenName: String get() = NAVI_PAY_SETUP } diff --git a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt index bd32c80ffe..f881179414 100644 --- a/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt +++ b/android/navi-pay/src/main/kotlin/com/navi/pay/onboarding/binding/viewmodel/SmsManager.kt @@ -7,16 +7,32 @@ package com.navi.pay.onboarding.binding.viewmodel +import android.Manifest +import android.app.PendingIntent import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build import android.provider.Telephony import android.provider.Telephony.Sms +import androidx.core.app.ActivityCompat +import com.navi.analytics.utils.NaviTrackEvent import com.navi.common.utils.log +import com.navi.pay.common.utils.NaviPayCommonUtils.isAirplaneModeOn +import com.navi.pay.utils.INTENT_ACTION_SMS_DELIVERED +import com.navi.pay.utils.INTENT_ACTION_SMS_SENT import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlin.math.min interface SmsManager { suspend fun sentSmsVerification(numberList: List, messageContent: String): Boolean + + suspend fun sendSms( + destinationNumberList: List, + messageContent: String, + subscriptionId: Int, + ) } class SmsManagerImpl @Inject constructor(@ApplicationContext private val context: Context) : @@ -78,4 +94,68 @@ class SmsManagerImpl @Inject constructor(@ApplicationContext private val context return false } } + + override suspend fun sendSms( + destinationNumberList: List, + messageContent: String, + subscriptionId: Int, + ) { + if ( + ActivityCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) != + PackageManager.PERMISSION_GRANTED + ) { + return + } + + if (isAirplaneModeOn(context = context)) { + return + } + + val smsManager = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + context + .getSystemService(android.telephony.SmsManager::class.java) + .createForSubscriptionId(subscriptionId) + } else { + android.telephony.SmsManager.getSmsManagerForSubscriptionId(subscriptionId) + } + + val sentPendingIntent = + PendingIntent.getBroadcast( + context, + (0..1000).random(), + Intent(INTENT_ACTION_SMS_SENT), + PendingIntent.FLAG_IMMUTABLE, + ) + + val deliveredPendingIntent = + PendingIntent.getBroadcast( + context, + (0..1000).random(), + Intent(INTENT_ACTION_SMS_DELIVERED), + PendingIntent.FLAG_IMMUTABLE, + ) + + try { + NaviTrackEvent.trackEventOnClickStream( + eventName = "dev_navipay_send_sms", + eventValues = + mapOf( + "destinationNumberList" to destinationNumberList.toString(), + "messageContentLength" to messageContent.length.toString(), + ), + ) + destinationNumberList.forEach { + smsManager.sendTextMessage( + it, + null, + messageContent, + sentPendingIntent, + deliveredPendingIntent, + ) + } + } catch (e: Exception) { + e.log() + } + } } 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 0fdb8ce1cd..3a12e72e0f 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 @@ -157,6 +157,8 @@ const val NAVI_PAY_SETTING_QR_PAGER_ANIMATION_COUNTER = "settingQrPagerAnimation const val NAVI_PAY_AUTO_POPUP_SCRATCH_CARD_COUNTER = "autoPopupScratchCardCounter" const val KEY_UPI_LITE_MANDATE_INFO = "liteMandateInfo" const val KEY_UPI_LITE_LAST_NOTIFICATION_TIMESTAMP = "upiLiteLastNotificationTimestamp" +const val NAVI_PAY_DEVICE_BINDING_IS_SMV_TRIGGERED_AND_FAILED = + "naviPayDeviceBindingIsSmvTriggeredAndFailed" // Sync DB keys const val NAVI_PAY_SYNC_TABLE_TRANSACTION_HISTORY_KEY = "transactionHistoryKey" @@ -177,6 +179,7 @@ const val LITMUS_EXPERIMENT_NAVIPAY_CHECK_BALANCE_DURING_TRANSACTION = const val LITMUS_EXPERIMENT_NAVIPAY_CHRISTMAS_CELEBRATION = "NaviPay-exp-rwd-holiday-animation" const val LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER = "NaviPay-frequent-contact-in-qr-scanner" +const val LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING = "NaviPay-exp-smv-binding" val NAVI_PAY_LITMUS_EXPERIMENTS = listOf( LITMUS_EXPERIMENT_NAVIPAY_LITE_AUTO_TOP_UP, @@ -185,6 +188,7 @@ val NAVI_PAY_LITMUS_EXPERIMENTS = LITMUS_EXPERIMENT_NAVIPAY_CHECK_BALANCE_DURING_TRANSACTION, LITMUS_EXPERIMENT_NAVIPAY_CHRISTMAS_CELEBRATION, LITMUS_EXPERIMENT_NAVIPAY_FREQUENT_CONTACT_IN_QR_SCANNER, + LITMUS_EXPERIMENT_NAVIPAY_SMV_BINDING, ) // Generic diff --git a/android/navi-pay/src/main/res/raw/navi_pay_mock.json b/android/navi-pay/src/main/res/raw/navi_pay_mock.json index ed65a499e8..228e35b06c 100644 --- a/android/navi-pay/src/main/res/raw/navi_pay_mock.json +++ b/android/navi-pay/src/main/res/raw/navi_pay_mock.json @@ -1,224 +1,11 @@ { - "NPAY_UPI_Onboarding": { - "version": 1, - "config": { - "otherUpiDetails": [ - { - "title": "Receive money from any UPI app", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/PhonePe_logo.png" - } - ], - "items": [ - { - "title": "Payment methods", - "items": [ - { - "title": "Bank\naccount", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS", - "needsResult": true - } - }, - { - "title": "Credit\nline", - "tag": "New", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS", - "needsResult": true - } - }, - { - "title": "RuPay\ncredit card", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS", - "needsResult": true - } - }, - { - "title": "UPI\nLite", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_UPI_LITE", - "needsResult": true - } - } - ] - }, - { - "title": "UPI options", - "items": [ - { - "title": "UPI\nGlobal", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_MANDATE_SCREEN", - "needsResult": true - } - }, - { - "title": "Payment\nrequest", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_PENDING_REQUESTS_SCREEN", - "needsResult": true - } - }, - { - "title": "Manage\nautopay", - "badgeCount": "3", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_MANDATE_SCREEN", - "needsResult": true - } - }, - { - "title": "Self\ntransfer", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_SELF_TRANSFER", - "needsResult": true - } - }, - { - "title": "UPI\nnumber", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_UPI_NUMBER_SCREEN", - "needsResult": true - } - }, - { - "title": "Blocked\nusers", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_BLOCKED_USERS_SCREEN", - "needsResult": true - } - }, - { - "title": "De-register", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_BLOCKED_USERS_SCREEN", - "needsResult": true - } - } - ] - } - ], - "addAccountIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Add-Account-Profile-Icon.png", - "nonOnboardedQrIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Non-Onboarded-QR.png" - } - }, - "NPAY_UPI_Non_Onboarding": { - "version": 1, - "config": { - "otherUpiDetails": [ - { - "title": "Receive money from any UPI app", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/PhonePe_logo.png" - } - ], - "items": [ - { - "title": "Payment methods", - "items": [ - { - "title": "Bank\naccount", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS", - "needsResult": true - } - }, - { - "title": "Credit\nline", - "tag": "New", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS", - "needsResult": true - } - }, - { - "title": "RuPay\ncredit card", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_LINKED_ACCOUNTS", - "needsResult": true - } - }, - { - "title": "UPI\nLite", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_UPI_LITE", - "needsResult": true - } - } - ] - }, - { - "title": "UPI options", - "items": [ - { - "title": "UPI\nGlobal", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_MANDATE_SCREEN", - "needsResult": true - } - }, - { - "title": "Payment\nrequest", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_PENDING_REQUESTS_SCREEN", - "needsResult": true - } - }, - { - "title": "Manage\nautopay", - "badgeCount": "3", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_MANDATE_SCREEN", - "needsResult": true - } - }, - { - "title": "Self\ntransfer", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_SELF_TRANSFER", - "needsResult": true - } - }, - { - "title": "UPI\nnumber", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_UPI_NUMBER_SCREEN", - "needsResult": true - } - }, - { - "title": "Blocked\nusers", - "iconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Profile_Placeholder.png", - "actionData": { - "url": "naviPay/NAVI_PAY_BLOCKED_USERS_SCREEN", - "needsResult": true - } - } - ] - } - ], - "addAccountIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Add-Account-Profile-Icon.png", - "nonOnboardedQrIconUrl": "https://public-assets.prod.navi-sa.in/navi-pay/png/Non-Onboarded-QR.png" + "BindDeviceResponse": { + "merchantCustomerId": "12345", + "internalDeviceId": "5678", + "smvBindingData": { + "url": "https://api.sandbox.bureau.id/v2/auth/initiate", + "clientId": "093f0fc3-bbe5-4944-a6c3-1bf410ae4237", + "aggregator": "bureau" } } } \ No newline at end of file diff --git a/android/navi-pay/src/main/res/values/strings.xml b/android/navi-pay/src/main/res/values/strings.xml index e273b3df14..dfca81f1c3 100644 --- a/android/navi-pay/src/main/res/values/strings.xml +++ b/android/navi-pay/src/main/res/values/strings.xml @@ -14,7 +14,15 @@ To verify your phone number for UPI payments. Phone To verify your SIM card with your registered mobile number. - *Standard SMS charges will apply + *Standard SMS charges may apply + Select the linked SIM to verify your number. Please do not go back or close the app. + We will verify your mobile number. Please do not go back or close the app. + Please do not go back or close the app. This usually takes a few seconds. + Proceed + Select bank + Add your bank account linked with +91-%s + +91-%s + Search bank here Popular banks No bank accounts found No bank accounts found linked to this phone number for the selected bank