Update CBS Mobile App codebase

This commit is contained in:
Abin Toppo
2026-09-21 13:21:25 +05:30
parent 58af541536
commit 2dcb10775c
339 changed files with 26750 additions and 16767 deletions
+32 -1
View File
@@ -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.
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>
+1
View File
@@ -0,0 +1 @@
sdk.dir=C:\\Users\\Lincpay\\AppData\\Local\\Android\\Sdk