Update CBS Mobile App codebase
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
@@ -6,6 +7,35 @@
|
||||
<!-- Required for showing notifications on Android 13+ (Notifee). -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Reads the SIM's own number so registration can offer it instead of
|
||||
asking the customer to type it. Both are needed: READ_PHONE_STATE for
|
||||
the SIM and subscription list, READ_PHONE_NUMBERS for the MSISDN on
|
||||
Android 13+. Neither is required for the app to work — a handset whose
|
||||
carrier never provisioned the number, or a customer who declines, falls
|
||||
back to typing it. -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||
|
||||
<!-- react-native-blob-util declares broad storage permissions in its own
|
||||
manifest, which the merger would otherwise fold into this app's.
|
||||
Statement PDFs are written to the app's cache directory and handed
|
||||
straight to a viewer or the share sheet, so nothing here ever touches
|
||||
shared storage. Removed rather than inherited: a banking app asking
|
||||
for the whole device's files is a Play Store review question and a
|
||||
customer-facing one, and a permission that is never exercised is worse
|
||||
than useless. Removing them is safe only while nothing calls the
|
||||
library's `fs.dirs.DownloadDir`/`SDCardDir` or its DownloadManager
|
||||
path — check that before adding either. -->
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"
|
||||
tools:node="remove" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
@@ -14,6 +44,7 @@
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.bankcbs.mobile.sim.SimCardPackage
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
@@ -14,8 +15,8 @@ class MainApplication : Application(), ReactApplication {
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
// Local module, so there is nothing for autolinking to find.
|
||||
add(SimCardPackage())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.bankcbs.mobile.sim
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
|
||||
/**
|
||||
* Reads what the handset can tell us about its SIM, so registration can offer
|
||||
* the customer's own number instead of asking them to type it.
|
||||
*
|
||||
* Worth being blunt about the limits: the MSISDN is only available if the
|
||||
* carrier actually provisioned it onto the SIM, and most Indian operators do
|
||||
* not. `getPhoneNumber()` returning blank is the common case, not the error
|
||||
* case — so this module reports what it found and never pretends. The caller
|
||||
* must always keep a manual-entry path.
|
||||
*/
|
||||
class SimCardModule(reactContext: ReactApplicationContext) :
|
||||
ReactContextBaseJavaModule(reactContext) {
|
||||
|
||||
override fun getName() = NAME
|
||||
|
||||
private fun hasPermission(permission: String) =
|
||||
reactApplicationContext.checkSelfPermission(permission) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
|
||||
/**
|
||||
* Reduces whatever the platform hands back to the ten significant digits the
|
||||
* API expects. Carriers return the number in every shape going — `+91…`,
|
||||
* `0091…`, with spaces — and the backend wants exactly ten digits.
|
||||
*/
|
||||
private fun normalise(raw: String?): String? {
|
||||
val digits = raw?.filter { it.isDigit() } ?: return null
|
||||
|
||||
return when {
|
||||
digits.length == 10 -> digits
|
||||
digits.length > 10 -> digits.takeLast(10)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscriptionManager() =
|
||||
reactApplicationContext.getSystemService(SubscriptionManager::class.java)
|
||||
|
||||
/**
|
||||
* The number provisioned on one specific subscription, or null.
|
||||
*
|
||||
* Two paths, because the supported one moved: from API 33 the answer comes
|
||||
* from `SubscriptionManager.getPhoneNumber(subId)` and needs
|
||||
* READ_PHONE_NUMBERS; below that the only source is the deprecated
|
||||
* `SubscriptionInfo.number`, which is blank on most carriers.
|
||||
*/
|
||||
private fun numberFor(subscriptions: SubscriptionManager?, info: SubscriptionInfo): String? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
hasPermission(Manifest.permission.READ_PHONE_NUMBERS)
|
||||
) {
|
||||
try {
|
||||
normalise(subscriptions?.getPhoneNumber(info.subscriptionId))?.let { return it }
|
||||
} catch (error: SecurityException) {
|
||||
// Fall through to the legacy field.
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
return try {
|
||||
normalise(info.number)
|
||||
} catch (error: SecurityException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every active subscription, one entry per populated slot.
|
||||
*
|
||||
* This is what lets a dual-SIM customer say which line their bank has, rather
|
||||
* than the app guessing. It reports each slot even when the number is null —
|
||||
* the carrier and the slot are still enough for someone to recognise which
|
||||
* SIM is theirs, which is the whole point of asking.
|
||||
*
|
||||
* Requires READ_PHONE_STATE; without it the list is empty rather than an
|
||||
* error, and the caller falls back to manual entry.
|
||||
*/
|
||||
private fun readSims(): WritableArray {
|
||||
val sims = Arguments.createArray()
|
||||
|
||||
if (!hasPermission(Manifest.permission.READ_PHONE_STATE)) {
|
||||
return sims
|
||||
}
|
||||
|
||||
val subscriptions = subscriptionManager()
|
||||
|
||||
val active =
|
||||
try {
|
||||
subscriptions?.activeSubscriptionInfoList
|
||||
} catch (error: SecurityException) {
|
||||
null
|
||||
} ?: return sims
|
||||
|
||||
active.forEach { info ->
|
||||
sims.pushMap(
|
||||
Arguments.createMap().apply {
|
||||
putInt("subscriptionId", info.subscriptionId)
|
||||
// Slots are 0-based here; the UI presents them as SIM 1 / SIM 2.
|
||||
putInt("slotIndex", info.simSlotIndex)
|
||||
|
||||
val carrier = info.carrierName?.toString()?.takeIf { it.isNotBlank() }
|
||||
if (carrier == null) putNull("carrierName") else putString("carrierName", carrier)
|
||||
|
||||
// The label the customer gave the SIM in Android settings, when they
|
||||
// set one — more recognisable than the carrier for someone with two
|
||||
// lines on the same network.
|
||||
val label = info.displayName?.toString()?.takeIf { it.isNotBlank() }
|
||||
if (label == null) putNull("displayName") else putString("displayName", label)
|
||||
|
||||
val number = numberFor(subscriptions, info)
|
||||
if (number == null) putNull("phoneNumber") else putString("phoneNumber", number)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return sims
|
||||
}
|
||||
|
||||
private fun readNumber(telephony: TelephonyManager): String? {
|
||||
// API 33 moved the supported path to SubscriptionManager; getLine1Number
|
||||
// is deprecated there and returns blank more often than not.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
hasPermission(Manifest.permission.READ_PHONE_NUMBERS)
|
||||
) {
|
||||
val subscriptions =
|
||||
reactApplicationContext.getSystemService(SubscriptionManager::class.java)
|
||||
|
||||
val active =
|
||||
try {
|
||||
subscriptions?.activeSubscriptionInfoList
|
||||
} catch (error: SecurityException) {
|
||||
null
|
||||
}
|
||||
|
||||
// A dual-SIM handset can have a number on either slot, so take the first
|
||||
// one the carrier actually provisioned rather than assuming slot 0.
|
||||
active?.forEach { info ->
|
||||
val candidate =
|
||||
try {
|
||||
subscriptions?.getPhoneNumber(info.subscriptionId)
|
||||
} catch (error: SecurityException) {
|
||||
null
|
||||
}
|
||||
|
||||
normalise(candidate)?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
return try {
|
||||
normalise(telephony.line1Number)
|
||||
} catch (error: SecurityException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun getSimInfo(promise: Promise) {
|
||||
try {
|
||||
val telephony =
|
||||
reactApplicationContext.getSystemService(TelephonyManager::class.java)
|
||||
|
||||
if (telephony == null) {
|
||||
promise.resolve(
|
||||
Arguments.createMap().apply {
|
||||
putBoolean("hasSim", false)
|
||||
putBoolean("hasPermission", false)
|
||||
putBoolean("canReadNumbers", false)
|
||||
putNull("phoneNumber")
|
||||
putNull("carrierName")
|
||||
putArray("sims", Arguments.createArray())
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
* The two grants do different jobs and are reported separately.
|
||||
*
|
||||
* READ_PHONE_STATE enumerates the slots. READ_PHONE_NUMBERS is what
|
||||
* `SubscriptionManager.getPhoneNumber()` demands from Android 13 onward,
|
||||
* and below 13 either grant will satisfy the legacy field.
|
||||
*
|
||||
* These used to be collapsed into one OR'd `granted` flag, which said
|
||||
* "permitted" whenever READ_PHONE_STATE alone had been given. On Android
|
||||
* 13 that reads every slot and no numbers — and the caller, seeing
|
||||
* permission granted and no numbers, blamed the carrier and told the
|
||||
* customer to visit a branch over a permission the app had never asked
|
||||
* for.
|
||||
*/
|
||||
val hasState = hasPermission(Manifest.permission.READ_PHONE_STATE)
|
||||
val hasNumbers =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
hasPermission(Manifest.permission.READ_PHONE_NUMBERS)
|
||||
val canReadNumbers =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
hasNumbers
|
||||
} else {
|
||||
hasState || hasNumbers
|
||||
}
|
||||
|
||||
val hasSim = telephony.simState == TelephonyManager.SIM_STATE_READY
|
||||
|
||||
val result =
|
||||
Arguments.createMap().apply {
|
||||
putBoolean("hasSim", hasSim)
|
||||
putBoolean("hasPermission", hasState)
|
||||
putBoolean("canReadNumbers", canReadNumbers)
|
||||
|
||||
val number = if (canReadNumbers && hasSim) readNumber(telephony) else null
|
||||
|
||||
if (number == null) putNull("phoneNumber") else putString("phoneNumber", number)
|
||||
|
||||
val carrier = telephony.simOperatorName?.takeIf { it.isNotBlank() }
|
||||
|
||||
if (carrier == null) putNull("carrierName") else putString("carrierName", carrier)
|
||||
|
||||
// Enumerated separately from the single `phoneNumber` above, which
|
||||
// stays for the case a caller only wants a best-guess prefill.
|
||||
putArray("sims", if (hasState) readSims() else Arguments.createArray())
|
||||
}
|
||||
|
||||
promise.resolve(result)
|
||||
} catch (error: Exception) {
|
||||
// Never reject: a handset that will not talk about its SIM is a case the
|
||||
// registration screen handles, not an error worth crashing a flow over.
|
||||
promise.resolve(
|
||||
Arguments.createMap().apply {
|
||||
putBoolean("hasSim", false)
|
||||
putBoolean("hasPermission", false)
|
||||
putBoolean("canReadNumbers", false)
|
||||
putNull("phoneNumber")
|
||||
putNull("carrierName")
|
||||
putArray("sims", Arguments.createArray())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NAME = "SimCard"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.bankcbs.mobile.sim
|
||||
|
||||
import android.view.View
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.uimanager.ReactShadowNode
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
|
||||
class SimCardPackage : ReactPackage {
|
||||
|
||||
override fun createNativeModules(
|
||||
reactContext: ReactApplicationContext,
|
||||
): List<NativeModule> = listOf(SimCardModule(reactContext))
|
||||
|
||||
override fun createViewManagers(
|
||||
reactContext: ReactApplicationContext,
|
||||
): List<ViewManager<out View, out ReactShadowNode<*>>> = emptyList()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Android API 28+ blocks plain HTTP, and a release build does not get the
|
||||
blanket `usesCleartextTraffic` exemption that debug builds do. Without this,
|
||||
every call to the local CBS backend fails with a generic network error and
|
||||
looks exactly like the server being down.
|
||||
|
||||
Deliberately NOT `<base-config cleartextTrafficPermitted="true">`: that
|
||||
would drop TLS enforcement for every host the app ever talks to. Only the
|
||||
development backend and the Metro dev server are listed, so anything else
|
||||
keeps the platform default of requiring HTTPS.
|
||||
|
||||
Before this app ships, the backend needs TLS and this file should go — a
|
||||
production build must not carry a cleartext exemption for any host.
|
||||
-->
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<!-- CBS backend. Keep in sync with `API_ORIGIN` in src/config/env.ts —
|
||||
a host missing from this list has its requests dropped with no
|
||||
error the app can see.
|
||||
|
||||
First entry is the one `API_ORIGIN` currently points at; the rest
|
||||
are addresses this build has been aimed at before. -->
|
||||
<domain includeSubdomains="true">mobile-banking-backend.lincpay</domain>
|
||||
<domain includeSubdomains="false">10.209.40.168</domain>
|
||||
<domain includeSubdomains="false">192.168.6.165</domain>
|
||||
<domain includeSubdomains="false">192.168.1.11</domain>
|
||||
<domain includeSubdomains="false">192.168.6.128</domain>
|
||||
<domain includeSubdomains="false">192.168.1.10</domain>
|
||||
|
||||
<!-- Metro, reached over `adb reverse` in a debug build. -->
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
|
||||
<!-- The host machine as seen from an Android emulator. -->
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -0,0 +1 @@
|
||||
sdk.dir=C:\\Users\\Lincpay\\AppData\\Local\\Android\\Sdk
|
||||
Reference in New Issue
Block a user