Compare commits
23
Commits
ecbbb3ed6b
..
v1.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89a9731797
|
||
|
|
50badc7d54
|
||
|
|
d4f86bb738
|
||
|
|
b35f44f35b
|
||
|
|
1a58ce8b54
|
||
|
|
2dd84ec50a
|
||
|
|
33651ca107
|
||
|
|
ae307e3118
|
||
|
|
3ab75bff92
|
||
|
|
1753d648bd
|
||
|
|
423b0bf1e1
|
||
|
|
d713047970
|
||
|
|
d59a6fad82
|
||
|
|
8e47101401
|
||
|
|
9431a90cd0
|
||
|
|
3a10f36c39
|
||
|
|
27c428d1f6
|
||
|
|
7b4f650f4e
|
||
|
|
00e109562b
|
||
|
|
cd4b3fef8b
|
||
|
|
389344a192
|
||
|
|
04af4e1bbd
|
||
|
|
6197152f6e
|
Generated
+15
-2
@@ -3,7 +3,7 @@
|
|||||||
<component name="deploymentTargetSelector">
|
<component name="deploymentTargetSelector">
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DIALOG" />
|
||||||
<DropdownSelection timestamp="2026-05-15T13:54:16.798188666Z">
|
<DropdownSelection timestamp="2026-05-15T13:54:16.798188666Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
@@ -11,7 +11,20 @@
|
|||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
<DialogSelection />
|
<DialogSelection>
|
||||||
|
<targets>
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=683a9830" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</targets>
|
||||||
|
</DialogSelection>
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "sh.sar.basedbank"
|
applicationId = "sh.sar.basedbank"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0"
|
versionName = "1.0.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,9 @@ dependencies {
|
|||||||
// RecyclerView for accounts list
|
// RecyclerView for accounts list
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
|
|
||||||
|
// CircularProgressDrawable for spinning search icons
|
||||||
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
// OkHttp for API calls
|
// OkHttp for API calls
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.11.0")
|
implementation("com.squareup.okhttp3:okhttp:4.11.0")
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.home.HomeActivity"
|
android:name=".ui.home.HomeActivity"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:windowSoftInputMode="adjustPan" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.home.QrScannerActivity"
|
android:name=".ui.home.QrScannerActivity"
|
||||||
|
|||||||
@@ -19,11 +19,19 @@ class BasedBankApp : Application() {
|
|||||||
var fullName: String = ""
|
var fullName: String = ""
|
||||||
var mibSession: MibSession? = null
|
var mibSession: MibSession? = null
|
||||||
var mibProfiles: List<MibProfile> = emptyList()
|
var mibProfiles: List<MibProfile> = emptyList()
|
||||||
var bmlSession: BmlSession? = null
|
/** Active BML sessions keyed by loginId (= BML username). */
|
||||||
|
val bmlSessions: MutableMap<String, BmlSession> = mutableMapOf()
|
||||||
var bmlAccounts: List<MibAccount> = emptyList()
|
var bmlAccounts: List<MibAccount> = emptyList()
|
||||||
var fahipaySession: FahipaySession? = null
|
var fahipaySession: FahipaySession? = null
|
||||||
var fahipayAccounts: List<MibAccount> = emptyList()
|
var fahipayAccounts: List<MibAccount> = emptyList()
|
||||||
|
|
||||||
|
/** Returns the BML session for the given account (matched via loginTag). */
|
||||||
|
fun bmlSessionFor(account: MibAccount): BmlSession? =
|
||||||
|
bmlSessions[account.loginTag.removePrefix("bml_")]
|
||||||
|
|
||||||
|
/** Returns any available BML session (for non-account-specific operations). */
|
||||||
|
fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull()
|
||||||
|
|
||||||
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
||||||
val mibMutex = Mutex()
|
val mibMutex = Mutex()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -18,8 +21,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.databinding.ActivityLockBinding
|
import sh.sar.basedbank.databinding.ActivityLockBinding
|
||||||
import sh.sar.basedbank.ui.home.HomeActivity
|
import sh.sar.basedbank.ui.home.HomeActivity
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.SecureRandom
|
|
||||||
import javax.crypto.SecretKeyFactory
|
import javax.crypto.SecretKeyFactory
|
||||||
import javax.crypto.spec.PBEKeySpec
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
|
||||||
@@ -31,7 +32,6 @@ class LockActivity : AppCompatActivity() {
|
|||||||
private lateinit var salt: String
|
private lateinit var salt: String
|
||||||
private lateinit var storedHash: String
|
private lateinit var storedHash: String
|
||||||
private var biometricsEnabled = false
|
private var biometricsEnabled = false
|
||||||
private var isLegacyFormat = false
|
|
||||||
private var isVerifying = false
|
private var isVerifying = false
|
||||||
|
|
||||||
private val lockPrefs get() = getSharedPreferences("lock_attempts", MODE_PRIVATE)
|
private val lockPrefs get() = getSharedPreferences("lock_attempts", MODE_PRIVATE)
|
||||||
@@ -39,28 +39,32 @@ class LockActivity : AppCompatActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val MAX_ATTEMPTS = 5
|
private const val MAX_ATTEMPTS = 5
|
||||||
private const val LOCKOUT_MS = 30_000L
|
private const val LOCKOUT_MS = 30_000L
|
||||||
|
const val EXTRA_RESUME = "resume"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
binding = ActivityLockBinding.inflate(layoutInflater)
|
binding = ActivityLockBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
val isLight = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = isLight
|
||||||
|
isAppearanceLightNavigationBars = isLight
|
||||||
|
}
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
|
||||||
|
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||||
method = prefs.getString("security_method", "pin") ?: "pin"
|
method = prefs.getString("security_method", "pin") ?: "pin"
|
||||||
biometricsEnabled = prefs.getBoolean("biometrics_enabled", false)
|
biometricsEnabled = prefs.getBoolean("biometrics_enabled", false)
|
||||||
|
|
||||||
// Try new encrypted format first; fall back to legacy SHA-256
|
val stored = CredentialStore(this).loadSecurityHash() ?: run { finish(); return }
|
||||||
val stored = CredentialStore(this).loadSecurityHash()
|
salt = stored.first
|
||||||
if (stored != null) {
|
storedHash = stored.second
|
||||||
salt = stored.first
|
|
||||||
storedHash = stored.second
|
|
||||||
isLegacyFormat = false
|
|
||||||
} else {
|
|
||||||
salt = prefs.getString("security_salt", "") ?: ""
|
|
||||||
storedHash = prefs.getString("security_hash", "") ?: ""
|
|
||||||
isLegacyFormat = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method == "pin") {
|
if (method == "pin") {
|
||||||
binding.viewPin.visibility = View.VISIBLE
|
binding.viewPin.visibility = View.VISIBLE
|
||||||
@@ -149,7 +153,6 @@ class LockActivity : AppCompatActivity() {
|
|||||||
val ok = withContext(Dispatchers.Default) { verify(entered) }
|
val ok = withContext(Dispatchers.Default) { verify(entered) }
|
||||||
isVerifying = false
|
isVerifying = false
|
||||||
if (ok) {
|
if (ok) {
|
||||||
migrateIfNeeded(entered)
|
|
||||||
resetFailures()
|
resetFailures()
|
||||||
proceed()
|
proceed()
|
||||||
} else {
|
} else {
|
||||||
@@ -174,7 +177,6 @@ class LockActivity : AppCompatActivity() {
|
|||||||
val ok = withContext(Dispatchers.Default) { verify(entered) }
|
val ok = withContext(Dispatchers.Default) { verify(entered) }
|
||||||
isVerifying = false
|
isVerifying = false
|
||||||
if (ok) {
|
if (ok) {
|
||||||
migrateIfNeeded(entered)
|
|
||||||
resetFailures()
|
resetFailures()
|
||||||
proceed()
|
proceed()
|
||||||
} else {
|
} else {
|
||||||
@@ -216,30 +218,8 @@ class LockActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun verify(input: String): Boolean {
|
private fun verify(input: String): Boolean {
|
||||||
if (storedHash.isBlank()) return false
|
if (storedHash.isBlank()) return false
|
||||||
return if (isLegacyFormat) {
|
val saltBytes = Base64.decode(salt, Base64.NO_WRAP)
|
||||||
sha256Legacy(salt + input) == storedHash
|
return pbkdf2(input, saltBytes) == storedHash
|
||||||
} else {
|
|
||||||
val saltBytes = Base64.decode(salt, Base64.NO_WRAP)
|
|
||||||
pbkdf2(input, saltBytes) == storedHash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On the first successful unlock after legacy SHA-256 format is detected,
|
|
||||||
* transparently migrate to PBKDF2 + CredentialStore.
|
|
||||||
*/
|
|
||||||
private fun migrateIfNeeded(input: String) {
|
|
||||||
if (!isLegacyFormat) return
|
|
||||||
try {
|
|
||||||
val newSalt = ByteArray(16).also { SecureRandom().nextBytes(it) }
|
|
||||||
val newHash = pbkdf2(input, newSalt)
|
|
||||||
val saltB64 = Base64.encodeToString(newSalt, Base64.NO_WRAP)
|
|
||||||
CredentialStore(this).saveSecurityHash(saltB64, newHash)
|
|
||||||
// Remove legacy plaintext fields
|
|
||||||
getSharedPreferences("prefs", MODE_PRIVATE).edit()
|
|
||||||
.remove("security_salt").remove("security_hash").apply()
|
|
||||||
isLegacyFormat = false
|
|
||||||
} catch (_: Exception) { /* migration will retry next unlock */ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun triggerBiometric() {
|
private fun triggerBiometric() {
|
||||||
@@ -270,8 +250,12 @@ class LockActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun proceed() {
|
private fun proceed() {
|
||||||
startActivity(Intent(this, HomeActivity::class.java))
|
if (intent.getBooleanExtra(EXTRA_RESUME, false)) {
|
||||||
finish()
|
finish()
|
||||||
|
} else {
|
||||||
|
startActivity(Intent(this, HomeActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Brute-force tracking ──────────────────────────────────────────────────
|
// ── Brute-force tracking ──────────────────────────────────────────────────
|
||||||
@@ -308,9 +292,4 @@ class LockActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Legacy: raw SHA-256(salt + input) — only used for migration path. */
|
|
||||||
private fun sha256Legacy(input: String) = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(input.toByteArray()).joinToString("") { "%02x".format(it) }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,17 +166,17 @@ class BmlLoginFlow {
|
|||||||
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
|
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
|
||||||
|
|
||||||
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
|
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
|
||||||
val accounts = fetchAccounts(session)
|
val accounts = fetchAccounts(session, "bml_$username")
|
||||||
return Pair(session, accounts)
|
return Pair(session, accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchAccounts(session: BmlSession): List<MibAccount> {
|
fun fetchAccounts(session: BmlSession, loginTag: String): List<MibAccount> {
|
||||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
||||||
val code = resp.code
|
val code = resp.code
|
||||||
val json = resp.body?.string()
|
val json = resp.body?.string()
|
||||||
resp.close()
|
resp.close()
|
||||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
return parseDashboard(json ?: return emptyList(), "bml_${session.deviceId}")
|
return parseDashboard(json ?: return emptyList(), loginTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
|
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
|
||||||
@@ -324,11 +324,11 @@ class BmlLoginFlow {
|
|||||||
return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false }
|
return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
fun fetchContacts(session: BmlSession, loginId: String): List<MibBeneficiary> {
|
||||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute()
|
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute()
|
||||||
val json = resp.body?.string() ?: return emptyList()
|
val json = resp.body?.string() ?: return emptyList()
|
||||||
resp.close()
|
resp.close()
|
||||||
return parseContacts(json)
|
return parseContacts(json, loginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -634,29 +634,28 @@ class BmlLoginFlow {
|
|||||||
internalId = internalId
|
internalId = internalId
|
||||||
))
|
))
|
||||||
} else if (accountType == "Card") {
|
} else if (accountType == "Card") {
|
||||||
|
val isVisible = item.optBoolean("account_visible", false)
|
||||||
|
if (!isVisible) continue // debit cards and other hidden cards — skip
|
||||||
val isPrepaid = item.optBoolean("prepaid_card", false)
|
val isPrepaid = item.optBoolean("prepaid_card", false)
|
||||||
if (isPrepaid) {
|
val cardBalance = item.optJSONObject("cardBalance")
|
||||||
val cardBalance = item.optJSONObject("cardBalance")
|
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
||||||
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
||||||
prepaidCards.add(MibAccount(
|
prepaidCards.add(MibAccount(
|
||||||
profileName = "Personal",
|
profileName = "Personal",
|
||||||
profileType = "BML_PREPAID",
|
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
||||||
accountNumber = accountNumber,
|
accountNumber = accountNumber,
|
||||||
accountBriefName = product,
|
accountBriefName = item.optString("alias").ifBlank { product },
|
||||||
currencyName = currency,
|
currencyName = currency,
|
||||||
accountTypeName = product,
|
accountTypeName = product,
|
||||||
availableBalance = "%.2f".format(available),
|
availableBalance = "%.2f".format(available),
|
||||||
currentBalance = "%.2f".format(cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0),
|
currentBalance = "%.2f".format(current),
|
||||||
blockedAmount = "0.00",
|
blockedAmount = "0.00",
|
||||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||||
statusDesc = status,
|
statusDesc = status,
|
||||||
profileImageHash = null,
|
profileImageHash = null,
|
||||||
loginTag = loginTag,
|
loginTag = loginTag,
|
||||||
internalId = internalId
|
internalId = internalId
|
||||||
))
|
))
|
||||||
} else {
|
|
||||||
// Linked debit cards have no independent balance or account link — skip
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,7 +693,7 @@ class BmlLoginFlow {
|
|||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseContacts(json: String): List<MibBeneficiary> {
|
private fun parseContacts(json: String, loginId: String = ""): List<MibBeneficiary> {
|
||||||
val root = JSONObject(json)
|
val root = JSONObject(json)
|
||||||
if (!root.optBoolean("success")) return emptyList()
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList()
|
val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList()
|
||||||
@@ -715,7 +714,8 @@ class BmlLoginFlow {
|
|||||||
benefStatus = item.optString("status", "S"),
|
benefStatus = item.optString("status", "S"),
|
||||||
transferCyDesc = item.optString("currency", "MVR"),
|
transferCyDesc = item.optString("currency", "MVR"),
|
||||||
customerImgHash = null,
|
customerImgHash = null,
|
||||||
benefCategoryId = "BML"
|
benefCategoryId = "BML",
|
||||||
|
profileId = loginId
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class AccountHistoryAdapter(
|
|||||||
private val iconUrlCache = mutableMapOf<String, Bitmap>()
|
private val iconUrlCache = mutableMapOf<String, Bitmap>()
|
||||||
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
|
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
|
||||||
var onIconUrlNeeded: ((url: String) -> Unit)? = null
|
var onIconUrlNeeded: ((url: String) -> Unit)? = null
|
||||||
|
var onTransferClick: ((MibAccount) -> Unit)? = null
|
||||||
|
|
||||||
fun updateImage(counterpartyName: String, bitmap: Bitmap) {
|
fun updateImage(counterpartyName: String, bitmap: Bitmap) {
|
||||||
imageCache[counterpartyName] = bitmap
|
imageCache[counterpartyName] = bitmap
|
||||||
@@ -164,6 +165,7 @@ class AccountHistoryAdapter(
|
|||||||
} else {
|
} else {
|
||||||
b.llHeaderBlocked.visibility = View.GONE
|
b.llHeaderBlocked.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
b.btnHeaderTransfer.setOnClickListener { onTransferClick?.invoke(acc) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun friendlyType(raw: String): String {
|
private fun friendlyType(raw: String): String {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
@@ -82,6 +83,9 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
adapter = AccountHistoryAdapter(account)
|
adapter = AccountHistoryAdapter(account)
|
||||||
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
||||||
adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) }
|
adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) }
|
||||||
|
adapter.onTransferClick = { acc ->
|
||||||
|
(activity as? HomeActivity)?.navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFrom(acc))
|
||||||
|
}
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
@@ -130,7 +134,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isMib() = !account.profileType.startsWith("BML") && account.profileType != "FAHIPAY"
|
private fun isMib() = !account.profileType.startsWith("BML") && account.profileType != "FAHIPAY"
|
||||||
private fun isBmlCard() = account.profileType == "BML_PREPAID"
|
private fun isBmlCard() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
private fun isFahipay() = account.profileType == "FAHIPAY"
|
private fun isFahipay() = account.profileType == "FAHIPAY"
|
||||||
|
|
||||||
private fun hasMore(): Boolean = when {
|
private fun hasMore(): Boolean = when {
|
||||||
@@ -185,7 +189,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
isBmlCard() -> {
|
isBmlCard() -> {
|
||||||
val session = app.bmlSession ?: return@withContext emptyList()
|
val session = app.bmlSessionFor(account) ?: return@withContext emptyList()
|
||||||
val cal = Calendar.getInstance()
|
val cal = Calendar.getInstance()
|
||||||
cal.add(Calendar.MONTH, -cardMonthOffset)
|
cal.add(Calendar.MONTH, -cardMonthOffset)
|
||||||
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
|
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
|
||||||
@@ -199,7 +203,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val session = app.bmlSession ?: return@withContext emptyList()
|
val session = app.bmlSessionFor(account) ?: return@withContext emptyList()
|
||||||
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
||||||
session = session,
|
session = session,
|
||||||
accountId = account.internalId,
|
accountId = account.internalId,
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ package sh.sar.basedbank.ui.home
|
|||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.databinding.ItemAccountBinding
|
import sh.sar.basedbank.databinding.ItemAccountBinding
|
||||||
import sh.sar.basedbank.databinding.ItemCardBinding
|
import sh.sar.basedbank.databinding.ItemCardBinding
|
||||||
import sh.sar.basedbank.databinding.ItemProfileHeaderBinding
|
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||||
|
import sh.sar.basedbank.util.BmlDashboardParser
|
||||||
|
import sh.sar.basedbank.util.MibAccountParser
|
||||||
|
|
||||||
class AccountsAdapter(
|
class AccountsAdapter(
|
||||||
accounts: List<MibAccount>,
|
accounts: List<MibAccount>,
|
||||||
@@ -21,7 +22,7 @@ class AccountsAdapter(
|
|||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private sealed class Item {
|
private sealed class Item {
|
||||||
data class SectionTitle(val label: String, val chip: String) : Item()
|
data class SectionTitle(val label: String) : Item()
|
||||||
data class Account(val account: MibAccount) : Item()
|
data class Account(val account: MibAccount) : Item()
|
||||||
data class Card(val account: MibAccount) : Item()
|
data class Card(val account: MibAccount) : Item()
|
||||||
}
|
}
|
||||||
@@ -35,19 +36,40 @@ class AccountsAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
||||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" }
|
val nonPrepaid = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||||
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" }
|
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||||
|
|
||||||
if (regular.isNotEmpty()) {
|
// Group non-prepaid accounts by their derived section title, preserving order
|
||||||
add(Item.SectionTitle("Accounts", ""))
|
val groups = LinkedHashMap<String, MutableList<MibAccount>>()
|
||||||
regular.forEach { add(Item.Account(it)) }
|
for (acc in nonPrepaid) {
|
||||||
|
val title = sectionTitle(acc)
|
||||||
|
groups.getOrPut(title) { mutableListOf() }.add(acc)
|
||||||
}
|
}
|
||||||
|
for ((title, group) in groups) {
|
||||||
|
add(Item.SectionTitle(title))
|
||||||
|
group.forEach { add(Item.Account(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
if (prepaid.isNotEmpty()) {
|
if (prepaid.isNotEmpty()) {
|
||||||
add(Item.SectionTitle("Cards", "BML"))
|
add(Item.SectionTitle("Cards · Bank of Maldives"))
|
||||||
prepaid.forEach { add(Item.Card(it)) }
|
prepaid.forEach { add(Item.Card(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sectionTitle(account: MibAccount): String {
|
||||||
|
val profileLabel = when (account.profileType) {
|
||||||
|
"0" -> "Personal"
|
||||||
|
"1" -> "Business"
|
||||||
|
else -> account.profileName
|
||||||
|
}
|
||||||
|
val bank = when {
|
||||||
|
account.profileType.startsWith("BML") -> "Bank of Maldives"
|
||||||
|
account.profileType == "FAHIPAY" -> "Fahipay"
|
||||||
|
else -> "Maldives Islamic Bank"
|
||||||
|
}
|
||||||
|
return if (profileLabel.isNotBlank()) "$profileLabel · $bank" else bank
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = when (items[position]) {
|
override fun getItemViewType(position: Int) = when (items[position]) {
|
||||||
is Item.SectionTitle -> TYPE_HEADER
|
is Item.SectionTitle -> TYPE_HEADER
|
||||||
is Item.Account -> TYPE_ACCOUNT
|
is Item.Account -> TYPE_ACCOUNT
|
||||||
@@ -57,7 +79,7 @@ class AccountsAdapter(
|
|||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
TYPE_HEADER -> SectionViewHolder(ItemProfileHeaderBinding.inflate(inflater, parent, false))
|
TYPE_HEADER -> SectionViewHolder(ItemDateHeaderBinding.inflate(inflater, parent, false))
|
||||||
TYPE_CARD -> CardViewHolder(ItemCardBinding.inflate(inflater, parent, false))
|
TYPE_CARD -> CardViewHolder(ItemCardBinding.inflate(inflater, parent, false))
|
||||||
else -> AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
|
else -> AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
|
||||||
}
|
}
|
||||||
@@ -73,16 +95,10 @@ class AccountsAdapter(
|
|||||||
|
|
||||||
override fun getItemCount() = items.size
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
private inner class SectionViewHolder(private val binding: ItemProfileHeaderBinding) :
|
private inner class SectionViewHolder(private val binding: ItemDateHeaderBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(item: Item.SectionTitle) {
|
fun bind(item: Item.SectionTitle) {
|
||||||
binding.tvProfileName.text = item.label
|
binding.tvDateHeader.text = item.label
|
||||||
if (item.chip.isNotEmpty()) {
|
|
||||||
binding.tvProfileType.text = item.chip
|
|
||||||
binding.tvProfileType.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.tvProfileType.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,18 +107,12 @@ class AccountsAdapter(
|
|||||||
fun bind(account: MibAccount) {
|
fun bind(account: MibAccount) {
|
||||||
binding.tvAccountName.text = account.accountBriefName
|
binding.tvAccountName.text = account.accountBriefName
|
||||||
binding.tvAccountNumber.text = account.accountNumber
|
binding.tvAccountNumber.text = account.accountNumber
|
||||||
binding.tvPillBank.text = when {
|
val label = if (account.profileType.startsWith("BML"))
|
||||||
account.profileType.startsWith("BML") -> "BML"
|
BmlDashboardParser.productLabel(account.accountTypeName)
|
||||||
account.profileType == "FAHIPAY" -> "FP"
|
else
|
||||||
else -> null
|
MibAccountParser.productLabel(account.accountTypeName)
|
||||||
}
|
binding.tvPillType.text = label
|
||||||
binding.tvPillType.text = friendlyAccountType(account.accountTypeName)
|
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||||
binding.tvPillProfile.text = when (account.profileType) {
|
|
||||||
"0" -> "Personal"
|
|
||||||
"1" -> "Business"
|
|
||||||
else -> account.profileName
|
|
||||||
}
|
|
||||||
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
|
|
||||||
binding.root.setOnClickListener { onAccountClick(account) }
|
binding.root.setOnClickListener { onAccountClick(account) }
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
copyToClipboard(it.context, account.accountNumber)
|
copyToClipboard(it.context, account.accountNumber)
|
||||||
@@ -114,15 +124,10 @@ class AccountsAdapter(
|
|||||||
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(account: MibAccount) {
|
fun bind(account: MibAccount) {
|
||||||
val brand = cardBrand(account.accountTypeName)
|
binding.ivCardBrand.setImageResource(cardBrandIcon(account.accountTypeName))
|
||||||
binding.tvCardBrand.text = brand.label
|
binding.tvCardName.text = account.accountBriefName
|
||||||
binding.tvCardBrand.background = GradientDrawable().apply {
|
binding.tvCardNumber.text = account.accountNumber
|
||||||
shape = GradientDrawable.RECTANGLE
|
binding.tvCardProduct.text = BmlDashboardParser.productLabel(account.accountTypeName)
|
||||||
cornerRadius = 100f
|
|
||||||
setColor(Color.parseColor(brand.color))
|
|
||||||
}
|
|
||||||
binding.tvCardName.text = account.accountBriefName
|
|
||||||
binding.tvCardNumber.text = account.accountNumber
|
|
||||||
binding.layoutCardBalance.visibility = View.VISIBLE
|
binding.layoutCardBalance.visibility = View.VISIBLE
|
||||||
binding.tvCardBalance.text = "${account.currencyName} ${account.availableBalance}"
|
binding.tvCardBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||||
|
|
||||||
@@ -150,29 +155,12 @@ class AccountsAdapter(
|
|||||||
Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun friendlyAccountType(raw: String): String {
|
private fun cardBrandIcon(productName: String): Int = when {
|
||||||
val u = raw.trim().uppercase()
|
|
||||||
return when {
|
|
||||||
u == "SAVINGS ACCOUNT" ||
|
|
||||||
u == "SAVING ACCOUNT" -> "Savings"
|
|
||||||
u == "CURRENT ACCOUNT" ||
|
|
||||||
u == "CURRENT ACCOUNT(PERSONAL)" ||
|
|
||||||
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
|
|
||||||
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
|
|
||||||
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
|
|
||||||
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
|
|
||||||
else -> raw.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class Brand(val label: String, val color: String)
|
|
||||||
|
|
||||||
private fun cardBrand(productName: String): Brand = when {
|
|
||||||
productName.contains("AMEX", ignoreCase = true) ||
|
productName.contains("AMEX", ignoreCase = true) ||
|
||||||
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> Brand("AMEX", "#016FD0")
|
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> R.drawable.americanexpress
|
||||||
productName.contains("VISA", ignoreCase = true) -> Brand("VISA", "#1A1F71")
|
productName.contains("VISA", ignoreCase = true) -> R.drawable.visa
|
||||||
productName.contains("MASTERCARD", ignoreCase = true) -> Brand("MC", "#FF5F00")
|
productName.contains("MASTERCARD", ignoreCase = true) -> R.drawable.mastercard
|
||||||
else -> Brand("CARD", "#555555")
|
else -> R.drawable.ic_nav_card
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.Filter
|
||||||
|
import android.widget.Filterable
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import sh.sar.basedbank.util.ContactsCache
|
import sh.sar.basedbank.util.ContactsCache
|
||||||
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -43,7 +49,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
private data class DestinationOption(
|
private data class DestinationOption(
|
||||||
val label: String,
|
val label: String,
|
||||||
val isBml: Boolean,
|
val isBml: Boolean,
|
||||||
val mibProfile: MibProfile? = null
|
val mibProfile: MibProfile? = null,
|
||||||
|
val bmlLoginId: String? = null,
|
||||||
|
val subtitle: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
private var destinations: List<DestinationOption> = emptyList()
|
private var destinations: List<DestinationOption> = emptyList()
|
||||||
@@ -86,18 +94,44 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
for (profile in app.mibProfiles) {
|
for (profile in app.mibProfiles) {
|
||||||
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile))
|
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile))
|
||||||
}
|
}
|
||||||
if (app.bmlSession != null) {
|
val store = CredentialStore(requireContext())
|
||||||
list.add(DestinationOption("BML · Personal", isBml = true))
|
for ((loginId, _) in app.bmlSessions) {
|
||||||
|
val ownerName = store.loadBmlUserProfile(loginId)?.fullName?.takeIf { it.isNotBlank() } ?: loginId
|
||||||
|
val profileName = app.bmlAccounts.firstOrNull { it.loginTag == "bml_$loginId" }?.profileName ?: ""
|
||||||
|
list.add(DestinationOption("BML · $ownerName", isBml = true, bmlLoginId = loginId, subtitle = profileName))
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupDestinationDropdown() {
|
private fun setupDestinationDropdown() {
|
||||||
val labels = destinations.map { it.label }
|
val adapter = object : ArrayAdapter<DestinationOption>(requireContext(), android.R.layout.simple_list_item_2, destinations) {
|
||||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, labels)
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
|
||||||
|
getDropDownView(position, convertView, parent)
|
||||||
|
|
||||||
|
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val view = convertView ?: LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_2, parent, false)
|
||||||
|
val opt = destinations[position]
|
||||||
|
view.findViewById<TextView>(android.R.id.text1).text = opt.label
|
||||||
|
val text2 = view.findViewById<TextView>(android.R.id.text2)
|
||||||
|
if (opt.subtitle.isNotBlank()) {
|
||||||
|
text2.text = opt.subtitle
|
||||||
|
text2.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
text2.visibility = View.GONE
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilter() = object : Filter() {
|
||||||
|
override fun performFiltering(c: CharSequence?) = FilterResults().apply { values = destinations; count = destinations.size }
|
||||||
|
override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged()
|
||||||
|
override fun convertResultToString(r: Any?) = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
binding.actvDestination.setAdapter(adapter)
|
binding.actvDestination.setAdapter(adapter)
|
||||||
binding.actvDestination.setOnItemClickListener { _, _, position, _ ->
|
binding.actvDestination.setOnItemClickListener { _, _, position, _ ->
|
||||||
selectedDest = destinations[position]
|
selectedDest = destinations[position]
|
||||||
|
binding.actvDestination.setText(destinations[position].label, false)
|
||||||
clearLookupResult()
|
clearLookupResult()
|
||||||
updateMibOnlyVisibility()
|
updateMibOnlyVisibility()
|
||||||
binding.btnSave.isEnabled = false
|
binding.btnSave.isEnabled = false
|
||||||
@@ -129,6 +163,22 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startLookupLoading() {
|
||||||
|
val spinner = CircularProgressDrawable(requireContext()).apply {
|
||||||
|
setStyle(CircularProgressDrawable.DEFAULT)
|
||||||
|
setColorSchemeColors(com.google.android.material.color.MaterialColors.getColor(
|
||||||
|
requireView(), com.google.android.material.R.attr.colorPrimary, Color.GRAY))
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
binding.tilAccount.endIconDrawable = spinner
|
||||||
|
binding.tilAccount.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopLookupLoading() {
|
||||||
|
binding.tilAccount.isEnabled = true
|
||||||
|
binding.tilAccount.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupAccountSearch() {
|
private fun setupAccountSearch() {
|
||||||
binding.tilAccount.setEndIconOnClickListener { performLookup() }
|
binding.tilAccount.setEndIconOnClickListener { performLookup() }
|
||||||
}
|
}
|
||||||
@@ -167,7 +217,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tilAccount.isEnabled = false
|
startLookupLoading()
|
||||||
binding.tilDestination.isEnabled = false
|
binding.tilDestination.isEnabled = false
|
||||||
binding.btnSave.isEnabled = false
|
binding.btnSave.isEnabled = false
|
||||||
|
|
||||||
@@ -175,7 +225,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
if (dest.isBml) lookupForBml(input) else lookupForMib(dest, input)
|
if (dest.isBml) lookupForBml(input) else lookupForMib(dest, input)
|
||||||
}
|
}
|
||||||
binding.tilAccount.isEnabled = true
|
stopLookupLoading()
|
||||||
binding.tilDestination.isEnabled = true
|
binding.tilDestination.isEnabled = true
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
showLookupResult(result, input)
|
showLookupResult(result, input)
|
||||||
@@ -186,7 +236,8 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupForBml(input: String): BmlAccountValidation? {
|
private fun lookupForBml(input: String): BmlAccountValidation? {
|
||||||
val bmlSess = app.bmlSession ?: return null
|
val loginId = selectedDest?.bmlLoginId ?: return null
|
||||||
|
val bmlSess = app.bmlSessions[loginId] ?: return null
|
||||||
val bmlFlow = BmlLoginFlow()
|
val bmlFlow = BmlLoginFlow()
|
||||||
|
|
||||||
// 1) Try BML validate
|
// 1) Try BML validate
|
||||||
@@ -236,7 +287,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
if (mibResult != null) return mibResult
|
if (mibResult != null) return mibResult
|
||||||
|
|
||||||
// MIB lookup failed (e.g. BML USD account) — fall back to BML validate
|
// MIB lookup failed (e.g. BML USD account) — fall back to BML validate
|
||||||
val bmlSess = app.bmlSession ?: return null
|
val bmlSess = app.anyBmlSession() ?: return null
|
||||||
return try { BmlLoginFlow().validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
return try { BmlLoginFlow().validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +392,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
binding.tilAlias.error = null
|
binding.tilAlias.error = null
|
||||||
|
|
||||||
binding.btnSave.isEnabled = false
|
binding.btnSave.isEnabled = false
|
||||||
|
binding.btnSave.text = "Saving..."
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
@@ -352,13 +404,15 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
binding.btnSave.isEnabled = true
|
binding.btnSave.isEnabled = true
|
||||||
|
binding.btnSave.text = "Save"
|
||||||
Toast.makeText(requireContext(), R.string.contact_save_failed, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.contact_save_failed, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveToBml(alias: String): Boolean {
|
private fun saveToBml(alias: String): Boolean {
|
||||||
val bmlSess = app.bmlSession ?: return false
|
val loginId = selectedDest?.bmlLoginId ?: return false
|
||||||
|
val bmlSess = app.bmlSessions[loginId] ?: return false
|
||||||
val lookup = bmlLookup ?: return false
|
val lookup = bmlLookup ?: return false
|
||||||
val bmlFlow = BmlLoginFlow()
|
val bmlFlow = BmlLoginFlow()
|
||||||
val account = lookup.account
|
val account = lookup.account
|
||||||
@@ -425,12 +479,13 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
requireActivity().lifecycleScope.launch(Dispatchers.IO) {
|
requireActivity().lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (dest.isBml) {
|
if (dest.isBml) {
|
||||||
val bmlSess = app.bmlSession ?: return@launch
|
val loginId = dest.bmlLoginId ?: return@launch
|
||||||
val fresh = BmlLoginFlow().fetchContacts(bmlSess)
|
val bmlSess = app.bmlSessions[loginId] ?: return@launch
|
||||||
|
val fresh = BmlLoginFlow().fetchContacts(bmlSess, loginId)
|
||||||
val existing = viewModel.contacts.value ?: emptyList()
|
val existing = viewModel.contacts.value ?: emptyList()
|
||||||
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
|
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
|
||||||
viewModel.contacts.postValue(merged)
|
viewModel.contacts.postValue(merged)
|
||||||
ContactsCache.saveBml(requireContext(), fresh)
|
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, fresh)
|
||||||
} else {
|
} else {
|
||||||
val profile = dest.mibProfile ?: return@launch
|
val profile = dest.mibProfile ?: return@launch
|
||||||
val mibSess = app.mibSession ?: return@launch
|
val mibSess = app.mibSession ?: return@launch
|
||||||
|
|||||||
@@ -122,6 +122,12 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
attachMediator(initialPages)
|
attachMediator(initialPages)
|
||||||
|
|
||||||
binding.etSheetSearch.addTextChangedListener { pagerAdapter.rebuildAll() }
|
binding.etSheetSearch.addTextChangedListener { pagerAdapter.rebuildAll() }
|
||||||
|
binding.etSheetSearch.setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
val allIndex = pagerAdapter.pages.indexOfFirst { it.tag == null }
|
||||||
|
if (allIndex >= 0) binding.viewPager.setCurrentItem(allIndex, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||||
val pages = buildList {
|
val pages = buildList {
|
||||||
@@ -203,11 +209,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val fromAccount = accounts.find { it.accountNumber == fromAccountNumber }
|
val fromAccount = accounts.find { it.accountNumber == fromAccountNumber }
|
||||||
val fromCurrency = fromAccount?.currencyName ?: ""
|
val fromCurrency = fromAccount?.currencyName ?: ""
|
||||||
val fromLoginTag = fromAccount?.loginTag ?: ""
|
val fromLoginTag = fromAccount?.loginTag ?: ""
|
||||||
val fromIsCard = fromAccount?.profileType == "BML_PREPAID"
|
val fromIsCard = fromAccount?.profileType == "BML_PREPAID" || fromAccount?.profileType == "BML_CREDIT"
|
||||||
|
|
||||||
if (tabTag == MY_ACCOUNTS_TAG) {
|
if (tabTag == MY_ACCOUNTS_TAG) {
|
||||||
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" }
|
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
|
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||||
|
|
||||||
val filteredRegular = if (search.isBlank()) regularAccounts else regularAccounts.filter {
|
val filteredRegular = if (search.isBlank()) regularAccounts else regularAccounts.filter {
|
||||||
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class ContactsFragment : Fragment() {
|
|||||||
colorHex = contact.bankColor,
|
colorHex = contact.bankColor,
|
||||||
imageHash = contact.customerImgHash
|
imageHash = contact.customerImgHash
|
||||||
)
|
)
|
||||||
(requireActivity() as HomeActivity).showWithBackStack(fragment)
|
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun confirmDelete(contact: MibBeneficiary) {
|
private fun confirmDelete(contact: MibBeneficiary) {
|
||||||
@@ -185,7 +185,7 @@ class ContactsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteBml(contact: MibBeneficiary): Boolean {
|
private fun deleteBml(contact: MibBeneficiary): Boolean {
|
||||||
val sess = app.bmlSession ?: return false
|
val sess = app.bmlSessions[contact.profileId] ?: app.anyBmlSession() ?: return false
|
||||||
val contactId = contact.benefNo.removePrefix("bml_")
|
val contactId = contact.benefNo.removePrefix("bml_")
|
||||||
return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,11 @@ class ContactsFragment : Fragment() {
|
|||||||
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.benefNo } ?: return
|
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.benefNo } ?: return
|
||||||
viewModel.contacts.value = updated
|
viewModel.contacts.value = updated
|
||||||
if (contact.benefCategoryId == "BML") {
|
if (contact.benefCategoryId == "BML") {
|
||||||
ContactsCache.saveBml(requireContext(), updated.filter { it.benefCategoryId == "BML" })
|
updated.filter { it.benefCategoryId == "BML" }
|
||||||
|
.groupBy { it.profileId }
|
||||||
|
.forEach { (loginId, contacts) ->
|
||||||
|
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, contacts)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ContactsCache.save(
|
ContactsCache.save(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class DashboardFragment : Fragment() {
|
|||||||
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
||||||
|
|
||||||
binding.btnTransfer.setOnClickListener {
|
binding.btnTransfer.setOnClickListener {
|
||||||
(requireActivity() as HomeActivity).showWithBackStack(TransferFragment())
|
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer)
|
||||||
}
|
}
|
||||||
binding.btnPayMvQr.setOnClickListener {
|
binding.btnPayMvQr.setOnClickListener {
|
||||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.CountDownTimer
|
import android.os.CountDownTimer
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -14,6 +15,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -55,6 +57,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private val viewModel: HomeViewModel by viewModels()
|
private val viewModel: HomeViewModel by viewModels()
|
||||||
private lateinit var toggle: ActionBarDrawerToggle
|
private lateinit var toggle: ActionBarDrawerToggle
|
||||||
|
private var suppressBottomNavCallback = false
|
||||||
|
|
||||||
private val autolockHandler = Handler(Looper.getMainLooper())
|
private val autolockHandler = Handler(Looper.getMainLooper())
|
||||||
private var warningDialog: AlertDialog? = null
|
private var warningDialog: AlertDialog? = null
|
||||||
@@ -63,24 +66,37 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val warningRunnable = Runnable { showAutolockWarning() }
|
private val warningRunnable = Runnable { showAutolockWarning() }
|
||||||
|
|
||||||
|
private var isLocked = false
|
||||||
|
|
||||||
private val autolockRunnable = Runnable {
|
private val autolockRunnable = Runnable {
|
||||||
countdownTimer?.cancel(); countdownTimer = null
|
countdownTimer?.cancel(); countdownTimer = null
|
||||||
warningDialog?.dismiss(); warningDialog = null
|
warningDialog?.dismiss(); warningDialog = null
|
||||||
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE)
|
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||||
.getString("security_method", null) != null
|
.getString("security_method", null) != null
|
||||||
if (securitySet) {
|
if (securitySet) lock()
|
||||||
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
|
}
|
||||||
finish()
|
|
||||||
}
|
private fun lock() {
|
||||||
|
isLocked = true
|
||||||
|
startActivity(
|
||||||
|
Intent(this, sh.sar.basedbank.LockActivity::class.java)
|
||||||
|
.putExtra(sh.sar.basedbank.LockActivity.EXTRA_RESUME, true)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||||
if (getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("block_screenshots", true)) {
|
if (getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("block_screenshots", true)) {
|
||||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
}
|
}
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
val isLight = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = isLight
|
||||||
|
isAppearanceLightNavigationBars = isLight
|
||||||
|
}
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
|
|
||||||
toggle = ActionBarDrawerToggle(
|
toggle = ActionBarDrawerToggle(
|
||||||
@@ -91,6 +107,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
toggle.syncState()
|
toggle.syncState()
|
||||||
|
|
||||||
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
||||||
|
if (suppressBottomNavCallback) return@setOnItemSelectedListener true
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.nav_dashboard -> { show(DashboardFragment()); true }
|
R.id.nav_dashboard -> { show(DashboardFragment()); true }
|
||||||
R.id.nav_accounts -> { show(AccountsFragment()); true }
|
R.id.nav_accounts -> { show(AccountsFragment()); true }
|
||||||
@@ -117,7 +134,10 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
||||||
viewModel.accounts.value = merged
|
viewModel.accounts.value = merged
|
||||||
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
|
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
|
||||||
if (app.bmlAccounts.isNotEmpty()) AccountCache.saveBml(this, app.bmlAccounts)
|
if (app.bmlAccounts.isNotEmpty()) {
|
||||||
|
val byLoginId = app.bmlAccounts.groupBy { it.loginTag.removePrefix("bml_") }
|
||||||
|
byLoginId.forEach { (loginId, accounts) -> AccountCache.saveBml(this, loginId, accounts) }
|
||||||
|
}
|
||||||
if (app.fahipayAccounts.isNotEmpty()) AccountCache.saveFahipay(this, app.fahipayAccounts)
|
if (app.fahipayAccounts.isNotEmpty()) AccountCache.saveFahipay(this, app.fahipayAccounts)
|
||||||
|
|
||||||
val cachedFinancing = FinancingCache.load(this)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
@@ -126,11 +146,12 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
if (app.bmlSession != null) refreshBmlLimits(app.bmlSession!!)
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
} else {
|
} else {
|
||||||
// Came from lock screen — show caches immediately, refresh everything in background
|
// Came from lock screen — show caches immediately, refresh everything in background
|
||||||
|
val store = CredentialStore(this)
|
||||||
val cachedMib = AccountCache.load(this)
|
val cachedMib = AccountCache.load(this)
|
||||||
val cachedBml = AccountCache.loadBml(this)
|
val cachedBml = AccountCache.loadBml(this, store.getBmlLoginIds())
|
||||||
val cachedFahipay = AccountCache.loadFahipay(this)
|
val cachedFahipay = AccountCache.loadFahipay(this)
|
||||||
val merged = cachedMib + cachedBml + cachedFahipay
|
val merged = cachedMib + cachedBml + cachedFahipay
|
||||||
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
||||||
@@ -139,8 +160,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val cachedLimits = ForeignLimitsCache.load(this)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
val store = CredentialStore(this)
|
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
|
||||||
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store.loadFahipayCredentials(), store)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show dashboard on first create
|
// Show dashboard on first create
|
||||||
@@ -197,17 +217,32 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateTo(itemId: Int) {
|
fun navigateTo(itemId: Int, fragment: Fragment? = null) {
|
||||||
when (itemId) {
|
val dest = fragment ?: when (itemId) {
|
||||||
R.id.nav_dashboard -> show(DashboardFragment())
|
R.id.nav_dashboard -> DashboardFragment()
|
||||||
R.id.nav_accounts -> show(AccountsFragment())
|
R.id.nav_accounts -> AccountsFragment()
|
||||||
R.id.nav_contacts -> show(ContactsFragment())
|
R.id.nav_contacts -> ContactsFragment()
|
||||||
R.id.nav_transfer -> show(TransferFragment())
|
R.id.nav_transfer -> TransferFragment()
|
||||||
R.id.nav_transfer_history -> show(TransferHistoryFragment())
|
R.id.nav_transfer_history -> TransferHistoryFragment()
|
||||||
R.id.nav_finances -> show(FinancingFragment())
|
R.id.nav_finances -> FinancingFragment()
|
||||||
R.id.nav_otp -> show(OtpFragment())
|
R.id.nav_otp -> OtpFragment()
|
||||||
R.id.nav_settings -> show(SettingsFragment())
|
R.id.nav_settings -> SettingsFragment()
|
||||||
else -> Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
|
||||||
|
}
|
||||||
|
show(dest)
|
||||||
|
binding.navigationView.setCheckedItem(itemId)
|
||||||
|
val bottomNavIds = setOf(R.id.nav_dashboard, R.id.nav_accounts, R.id.nav_contacts, R.id.nav_transfer, R.id.nav_more)
|
||||||
|
if (binding.bottomNavigation.visibility == View.VISIBLE && itemId in bottomNavIds) {
|
||||||
|
suppressBottomNavCallback = true
|
||||||
|
binding.bottomNavigation.selectedItemId = itemId
|
||||||
|
suppressBottomNavCallback = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBottomNavVisible(visible: Boolean) {
|
||||||
|
val isBottom = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
|
if (isBottom) {
|
||||||
|
binding.bottomNavigation.visibility = if (visible) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +259,13 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
// Returning from LockActivity — skip the elapsed check and reset state.
|
||||||
|
if (isLocked) {
|
||||||
|
isLocked = false
|
||||||
|
pauseTime = 0L
|
||||||
|
resetAutolockTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
// If we were away long enough to have hit the autolock timeout (e.g. while
|
// If we were away long enough to have hit the autolock timeout (e.g. while
|
||||||
// QrScannerActivity was in the foreground), lock immediately.
|
// QrScannerActivity was in the foreground), lock immediately.
|
||||||
if (pauseTime > 0L) {
|
if (pauseTime > 0L) {
|
||||||
@@ -231,8 +273,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val timeout = getSharedPreferences("prefs", MODE_PRIVATE).getLong("autolock_timeout", 60_000L)
|
val timeout = getSharedPreferences("prefs", MODE_PRIVATE).getLong("autolock_timeout", 60_000L)
|
||||||
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE).getString("security_method", null) != null
|
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE).getString("security_method", null) != null
|
||||||
if (timeout > 0L && elapsed >= timeout && securitySet) {
|
if (timeout > 0L && elapsed >= timeout && securitySet) {
|
||||||
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
|
lock()
|
||||||
finish()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,8 +344,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
if (item.itemId == R.id.action_lock) {
|
if (item.itemId == R.id.action_lock) {
|
||||||
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
|
lock()
|
||||||
finish()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
@@ -314,9 +354,9 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
fun relogin() {
|
fun relogin() {
|
||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
val hasMib = store.hasMibCredentials()
|
val hasMib = store.hasMibCredentials()
|
||||||
val hasBml = store.hasBmlCredentials()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
val hasFahipay = store.hasFahipayCredentials()
|
val hasFahipay = store.hasFahipayCredentials()
|
||||||
if (!hasMib && !hasBml && !hasFahipay) {
|
if (!hasMib && bmlLoginIds.isEmpty() && !hasFahipay) {
|
||||||
startActivity(Intent(this, LoginActivity::class.java))
|
startActivity(Intent(this, LoginActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
@@ -325,24 +365,26 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val current = viewModel.accounts.value ?: emptyList()
|
val current = viewModel.accounts.value ?: emptyList()
|
||||||
viewModel.accounts.value = current.filter { acc ->
|
viewModel.accounts.value = current.filter { acc ->
|
||||||
if (!hasMib && !acc.profileType.startsWith("BML") && acc.profileType != "FAHIPAY") return@filter false
|
if (!hasMib && !acc.profileType.startsWith("BML") && acc.profileType != "FAHIPAY") return@filter false
|
||||||
if (!hasBml && acc.profileType.startsWith("BML")) return@filter false
|
if (acc.profileType.startsWith("BML")) {
|
||||||
|
val loginId = acc.loginTag.removePrefix("bml_")
|
||||||
|
return@filter loginId in bmlLoginIds
|
||||||
|
}
|
||||||
if (!hasFahipay && acc.profileType == "FAHIPAY") return@filter false
|
if (!hasFahipay && acc.profileType == "FAHIPAY") return@filter false
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store.loadFahipayCredentials(), store)
|
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoRefresh(
|
private fun autoRefresh(
|
||||||
mibCreds: CredentialStore.MibCredentials?,
|
mibCreds: CredentialStore.MibCredentials?,
|
||||||
bmlCreds: CredentialStore.BmlCredentials?,
|
|
||||||
fahipayCreds: CredentialStore.FahipayCredentials?,
|
fahipayCreds: CredentialStore.FahipayCredentials?,
|
||||||
store: CredentialStore
|
store: CredentialStore
|
||||||
) {
|
) {
|
||||||
if (mibCreds == null && bmlCreds == null && fahipayCreds == null) return
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
|
if (mibCreds == null && bmlLoginIds.isEmpty() && fahipayCreds == null) return
|
||||||
binding.refreshIndicator.visibility = View.VISIBLE
|
binding.refreshIndicator.visibility = View.VISIBLE
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// MIB and BML login run in parallel
|
|
||||||
val mibJob = mibCreds?.let {
|
val mibJob = mibCreds?.let {
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -358,39 +400,36 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bmlJob = bmlCreds?.let {
|
// One async job per BML login, all run in parallel
|
||||||
async(Dispatchers.IO) {
|
val bmlJobs = bmlLoginIds.mapNotNull { loginId ->
|
||||||
|
val creds = store.loadBmlCredentials(loginId) ?: return@mapNotNull null
|
||||||
|
loginId to async(Dispatchers.IO) {
|
||||||
val bmlFlow = BmlLoginFlow()
|
val bmlFlow = BmlLoginFlow()
|
||||||
val savedToken = store.loadBmlSession()
|
val loginTag = "bml_$loginId"
|
||||||
|
val savedToken = store.loadBmlSession(loginId)
|
||||||
|
|
||||||
// Try cached token first
|
|
||||||
if (savedToken != null) {
|
if (savedToken != null) {
|
||||||
try {
|
try {
|
||||||
val session = BmlSession(savedToken.first, savedToken.second)
|
val session = BmlSession(savedToken.first, savedToken.second)
|
||||||
val accounts = bmlFlow.fetchAccounts(session)
|
val accounts = bmlFlow.fetchAccounts(session, loginTag)
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.bmlSession = session
|
app.bmlSessions[loginId] = session
|
||||||
app.bmlAccounts = accounts
|
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
|
||||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
|
||||||
return@async Pair(session, accounts)
|
return@async Pair(session, accounts)
|
||||||
} catch (_: AuthExpiredException) {
|
} catch (_: AuthExpiredException) {
|
||||||
// Token expired — fall through to full login
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Network or other error — fall through to full login
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full login (token missing or expired)
|
|
||||||
try {
|
try {
|
||||||
val (session, accounts) = bmlFlow.login(it.username, it.password, it.otpSeed)
|
val (session, accounts) = bmlFlow.login(creds.username, creds.password, creds.otpSeed)
|
||||||
store.saveBmlSession(session.accessToken, session.deviceId)
|
store.saveBmlSession(loginId, session.accessToken, session.deviceId)
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.bmlSession = session
|
app.bmlSessions[loginId] = session
|
||||||
app.bmlAccounts = accounts
|
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
|
||||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
|
||||||
Pair(session, accounts)
|
Pair(session, accounts)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
Pair(null, AccountCache.loadBml(this@HomeActivity))
|
Pair(null, AccountCache.loadBml(this@HomeActivity, loginId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,7 +439,6 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val fahipayFlow = FahipayLoginFlow()
|
val fahipayFlow = FahipayLoginFlow()
|
||||||
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
|
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
|
||||||
|
|
||||||
// Try cached session first
|
|
||||||
val savedSession = store.loadFahipaySession()
|
val savedSession = store.loadFahipaySession()
|
||||||
if (savedSession != null) {
|
if (savedSession != null) {
|
||||||
try {
|
try {
|
||||||
@@ -416,15 +454,12 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
||||||
return@async Pair(session, accounts)
|
return@async Pair(session, accounts)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Session expired — fall through to full login
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full re-login (only works if user has no 2FA, or 2FA was skipped)
|
|
||||||
try {
|
try {
|
||||||
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
|
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
|
||||||
if (step.twoFactorRequired) {
|
if (step.twoFactorRequired) {
|
||||||
// Can't auto-complete 2FA — use cached data
|
|
||||||
return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
||||||
}
|
}
|
||||||
val authId = step.authId ?: return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
val authId = step.authId ?: return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
||||||
@@ -447,14 +482,16 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
|
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
|
||||||
val (bmlSession, bmlAccounts) = bmlJob?.await() ?: Pair(null, AccountCache.loadBml(this@HomeActivity))
|
val bmlResults = bmlJobs.map { (_, job) -> job.await() }
|
||||||
|
val bmlAccounts = bmlResults.flatMap { it.second }
|
||||||
val (_, fahipayAccounts) = fahipayJob?.await() ?: Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
val (_, fahipayAccounts) = fahipayJob?.await() ?: Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
||||||
|
|
||||||
|
val app = application as BasedBankApp
|
||||||
|
app.bmlAccounts = bmlAccounts
|
||||||
viewModel.accounts.postValue(mibAccounts + bmlAccounts + fahipayAccounts)
|
viewModel.accounts.postValue(mibAccounts + bmlAccounts + fahipayAccounts)
|
||||||
binding.refreshIndicator.visibility = View.GONE
|
binding.refreshIndicator.visibility = View.GONE
|
||||||
|
|
||||||
val app = application as BasedBankApp
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
if (bmlSession != null) refreshBmlLimits(bmlSession)
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,16 +514,21 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshBmlContacts(app: BasedBankApp) {
|
private fun refreshBmlContacts(app: BasedBankApp) {
|
||||||
val session = app.bmlSession ?: return
|
if (app.bmlSessions.isEmpty()) return
|
||||||
val bmlFlow = BmlLoginFlow()
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val bmlContacts = withContext(Dispatchers.IO) { bmlFlow.fetchContacts(session) }
|
val allBmlContacts = withContext(Dispatchers.IO) {
|
||||||
if (bmlContacts.isNotEmpty()) {
|
app.bmlSessions.flatMap { (loginId, session) ->
|
||||||
ContactsCache.saveBml(this@HomeActivity, bmlContacts)
|
val contacts = BmlLoginFlow().fetchContacts(session, loginId)
|
||||||
|
if (contacts.isNotEmpty()) ContactsCache.saveBml(this@HomeActivity, loginId, contacts)
|
||||||
|
contacts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allBmlContacts.isNotEmpty()) {
|
||||||
|
val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity)
|
||||||
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
|
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
|
||||||
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
||||||
viewModel.contacts.postValue(mergeContacts(mergeContacts(mibContacts, bmlContacts), fahipayContacts))
|
viewModel.contacts.postValue(mergeContacts(mergeContacts(mibContacts, allBmlContacts), fahipayContacts))
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { /* keep cached */ }
|
} catch (_: Exception) { /* keep cached */ }
|
||||||
}
|
}
|
||||||
@@ -494,10 +536,11 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
fun loadAllContacts() {
|
fun loadAllContacts() {
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
|
val store = sh.sar.basedbank.util.CredentialStore(this)
|
||||||
// Populate ViewModel from cache immediately if empty
|
// Populate ViewModel from cache immediately if empty
|
||||||
if (viewModel.contacts.value.isNullOrEmpty()) {
|
if (viewModel.contacts.value.isNullOrEmpty()) {
|
||||||
val cached = mergeContacts(
|
val cached = mergeContacts(
|
||||||
mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this)),
|
mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this, store.getBmlLoginIds())),
|
||||||
ContactsCache.loadFahipay(this)
|
ContactsCache.loadFahipay(this)
|
||||||
)
|
)
|
||||||
if (cached.isNotEmpty()) viewModel.contacts.value = cached
|
if (cached.isNotEmpty()) viewModel.contacts.value = cached
|
||||||
@@ -525,7 +568,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val categories = groups.map { MibBeneficiaryCategory(it.categoryId, it.label, it.contacts.size) }
|
val categories = groups.map { MibBeneficiaryCategory(it.categoryId, it.label, it.contacts.size) }
|
||||||
ContactsCache.saveFahipay(this@HomeActivity, contacts, categories)
|
ContactsCache.saveFahipay(this@HomeActivity, contacts, categories)
|
||||||
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
|
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
|
||||||
val bmlContacts = ContactsCache.loadBml(this@HomeActivity)
|
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
|
||||||
|
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, bmlLoginIds)
|
||||||
viewModel.contacts.postValue(mergeContacts(mergeContacts(mibContacts, bmlContacts), contacts))
|
viewModel.contacts.postValue(mergeContacts(mergeContacts(mibContacts, bmlContacts), contacts))
|
||||||
val mibCategories = ContactsCache.loadCategories(this@HomeActivity)
|
val mibCategories = ContactsCache.loadCategories(this@HomeActivity)
|
||||||
viewModel.contactCategories.postValue(mibCategories + categories)
|
viewModel.contactCategories.postValue(mibCategories + categories)
|
||||||
@@ -571,7 +615,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
if (allContacts.isNotEmpty()) {
|
if (allContacts.isNotEmpty()) {
|
||||||
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
||||||
val bmlContacts = ContactsCache.loadBml(this@HomeActivity)
|
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
|
||||||
|
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, bmlLoginIds)
|
||||||
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
||||||
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
|
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
|
||||||
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
|
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
|
||||||
@@ -603,17 +648,19 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val others = current.filter { it.profileType != "FAHIPAY" }
|
val others = current.filter { it.profileType != "FAHIPAY" }
|
||||||
viewModel.accounts.postValue(others + fresh)
|
viewModel.accounts.postValue(others + fresh)
|
||||||
} else if (src.profileType.startsWith("BML")) {
|
} else if (src.profileType.startsWith("BML")) {
|
||||||
|
val loginId = src.loginTag.removePrefix("bml_")
|
||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.bmlSession ?: return@withContext null
|
val sess = app.bmlSessionFor(src) ?: return@withContext null
|
||||||
try {
|
try {
|
||||||
val accounts = BmlLoginFlow().fetchAccounts(sess)
|
val accounts = BmlLoginFlow().fetchAccounts(sess, src.loginTag)
|
||||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
|
||||||
app.bmlAccounts = accounts
|
val otherBml = app.bmlAccounts.filter { it.loginTag != src.loginTag }
|
||||||
|
app.bmlAccounts = otherBml + accounts
|
||||||
accounts
|
accounts
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
val mibOnly = current.filter { !it.profileType.startsWith("BML") }
|
val otherAccounts = current.filter { !it.profileType.startsWith("BML") || it.loginTag != src.loginTag }
|
||||||
viewModel.accounts.postValue(mibOnly + fresh)
|
viewModel.accounts.postValue(otherAccounts + fresh)
|
||||||
} else {
|
} else {
|
||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.mibSession ?: return@withContext null
|
val sess = app.mibSession ?: return@withContext null
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class MoreFragment : Fragment() {
|
|||||||
private data class NavItem(val id: Int, @DrawableRes val icon: Int, @StringRes val title: Int)
|
private data class NavItem(val id: Int, @DrawableRes val icon: Int, @StringRes val title: Int)
|
||||||
|
|
||||||
private val items = listOf(
|
private val items = listOf(
|
||||||
|
NavItem(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr),
|
||||||
NavItem(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities),
|
NavItem(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities),
|
||||||
NavItem(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history),
|
NavItem(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history),
|
||||||
NavItem(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances),
|
NavItem(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances),
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ class OtpFragment : Fragment() {
|
|||||||
val name = store.loadMibFullName()
|
val name = store.loadMibFullName()
|
||||||
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
|
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
|
||||||
}
|
}
|
||||||
store.loadBmlCredentials()?.let { creds ->
|
for (loginId in store.getBmlLoginIds()) {
|
||||||
val name = store.loadBmlFullName()
|
val creds = store.loadBmlCredentials(loginId) ?: continue
|
||||||
entries.add(OtpEntry(if (name != null) "BML · $name" else "BML", creds.otpSeed))
|
val name = store.loadBmlUserProfile(loginId)?.fullName
|
||||||
|
entries.add(OtpEntry(if (!name.isNullOrBlank()) "BML · $name" else "BML", creds.otpSeed))
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = OtpAdapter(entries)
|
val adapter = OtpAdapter(entries)
|
||||||
@@ -105,13 +106,14 @@ class OtpFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (store.loadBmlFullName() == null) {
|
for (loginId in store.getBmlLoginIds()) {
|
||||||
app.bmlSession?.let { session ->
|
if (store.loadBmlUserProfile(loginId)?.fullName.isNullOrBlank()) {
|
||||||
|
val session = app.bmlSessions[loginId] ?: continue
|
||||||
val info = withContext(Dispatchers.IO) {
|
val info = withContext(Dispatchers.IO) {
|
||||||
try { BmlLoginFlow().fetchUserInfo(session) } catch (_: Exception) { null }
|
try { BmlLoginFlow().fetchUserInfo(session) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
store.saveBmlUserProfile(CredentialStore.BmlUserProfile(
|
store.saveBmlUserProfile(loginId, CredentialStore.BmlUserProfile(
|
||||||
fullName = info.fullName,
|
fullName = info.fullName,
|
||||||
email = info.email,
|
email = info.email,
|
||||||
mobile = info.mobile,
|
mobile = info.mobile,
|
||||||
@@ -119,7 +121,8 @@ class OtpFragment : Fragment() {
|
|||||||
idCard = info.idCard,
|
idCard = info.idCard,
|
||||||
birthdate = info.birthdate
|
birthdate = info.birthdate
|
||||||
))
|
))
|
||||||
val idx = entries.indexOfFirst { it.seed == store.loadBmlCredentials()?.otpSeed }
|
val seed = store.loadBmlCredentials(loginId)?.otpSeed
|
||||||
|
val idx = entries.indexOfFirst { it.seed == seed }
|
||||||
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "BML · ${info.fullName}"); changed = true }
|
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "BML · ${info.fullName}"); changed = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
||||||
@@ -84,8 +87,23 @@ class QrScannerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
binding = ActivityQrScannerBinding.inflate(layoutInflater)
|
binding = ActivityQrScannerBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
// Black camera background — always use light (white) system bar icons
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = false
|
||||||
|
isAppearanceLightNavigationBars = false
|
||||||
|
}
|
||||||
|
val originalBtnMarginBottom = (48 * resources.displayMetrics.density).toInt()
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.btnContainer) { view, insets ->
|
||||||
|
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
(view.layoutParams as android.widget.FrameLayout.LayoutParams).also {
|
||||||
|
it.bottomMargin = originalBtnMarginBottom + bars.bottom
|
||||||
|
view.layoutParams = it
|
||||||
|
}
|
||||||
|
insets
|
||||||
|
}
|
||||||
binding.btnCancel.setOnClickListener { finish() }
|
binding.btnCancel.setOnClickListener { finish() }
|
||||||
binding.btnPickImage.setOnClickListener { pickImageLauncher.launch("image/*") }
|
binding.btnPickImage.setOnClickListener { pickImageLauncher.launch("image/*") }
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
|
||||||
val hasMib = store.hasMibCredentials()
|
val hasMib = store.hasMibCredentials()
|
||||||
val hasBml = store.hasBmlCredentials()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
val hasFahipay = store.hasFahipayCredentials()
|
val hasFahipay = store.hasFahipayCredentials()
|
||||||
|
|
||||||
binding.tvLoginsTitle.visibility = if (hasMib || hasBml || hasFahipay) View.VISIBLE else View.GONE
|
binding.tvLoginsTitle.visibility = if (hasMib || bmlLoginIds.isNotEmpty() || hasFahipay) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
if (hasMib) {
|
if (hasMib) {
|
||||||
val profile = store.loadMibUserProfile()
|
val profile = store.loadMibUserProfile()
|
||||||
@@ -86,10 +86,10 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasBml) {
|
for (loginId in bmlLoginIds) {
|
||||||
val profile = store.loadBmlUserProfile()
|
val profile = store.loadBmlUserProfile(loginId)
|
||||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name)
|
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name)
|
||||||
val profileNames = AccountCache.loadBml(ctx).map { it.profileName }.filter { it.isNotBlank() }.distinct()
|
val profileNames = AccountCache.loadBml(ctx, loginId).map { it.profileName }.filter { it.isNotBlank() }.distinct()
|
||||||
addLoginRow(container, R.drawable.bml_logo_vector, displayName) {
|
addLoginRow(container, R.drawable.bml_logo_vector, displayName) {
|
||||||
showLoginDetails(
|
showLoginDetails(
|
||||||
title = getString(R.string.bml_name),
|
title = getString(R.string.bml_name),
|
||||||
@@ -105,7 +105,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
profileNames.forEach { appendLine(" • $it") }
|
profileNames.forEach { appendLine(" • $it") }
|
||||||
}
|
}
|
||||||
}.trim(),
|
}.trim(),
|
||||||
onLogout = { confirmLogout(getString(R.string.bml_name)) { logoutBml(store) } }
|
onLogout = { confirmLogout(getString(R.string.bml_name)) { logoutBml(store, loginId) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,11 +183,12 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logoutBml(store: CredentialStore) {
|
private fun logoutBml(store: CredentialStore, loginId: String) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
store.clearBmlCredentials(); store.clearBmlSession()
|
store.clearBmlCredentials(loginId); store.clearBmlSession(loginId)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.bmlSession = null; app.bmlAccounts = emptyList()
|
app.bmlSessions.remove(loginId)
|
||||||
|
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$loginId" }
|
||||||
clearAllCaches(ctx)
|
clearAllCaches(ctx)
|
||||||
(activity as HomeActivity).relogin()
|
(activity as HomeActivity).relogin()
|
||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
|
|||||||
@@ -35,11 +35,27 @@ class SettingsSecurityFragment : Fragment() {
|
|||||||
val canUseBiometrics = BiometricManager.from(requireContext())
|
val canUseBiometrics = BiometricManager.from(requireContext())
|
||||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
if (canUseBiometrics) {
|
if (canUseBiometrics) {
|
||||||
binding.rowBiometrics.visibility = View.VISIBLE
|
val unlockEnabled = prefs.getBoolean("biometrics_enabled", false)
|
||||||
binding.switchBiometrics.isChecked = prefs.getBoolean("biometrics_enabled", false)
|
binding.switchBiometrics.isChecked = unlockEnabled
|
||||||
|
binding.switchBiometricsTransfer.isChecked = prefs.getBoolean("biometrics_transfer_confirm", false)
|
||||||
|
binding.switchBiometricsTransfer.isEnabled = unlockEnabled
|
||||||
|
|
||||||
binding.switchBiometrics.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchBiometrics.setOnCheckedChangeListener { _, isChecked ->
|
||||||
prefs.edit().putBoolean("biometrics_enabled", isChecked).apply()
|
prefs.edit().putBoolean("biometrics_enabled", isChecked).apply()
|
||||||
|
binding.switchBiometricsTransfer.isEnabled = isChecked
|
||||||
|
if (!isChecked) {
|
||||||
|
binding.switchBiometricsTransfer.isChecked = false
|
||||||
|
prefs.edit().putBoolean("biometrics_transfer_confirm", false).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.switchBiometricsTransfer.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
prefs.edit().putBoolean("biometrics_transfer_confirm", isChecked).apply()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.tvBiometricsHint.visibility = View.VISIBLE
|
||||||
|
binding.switchBiometrics.isEnabled = false
|
||||||
|
binding.switchBiometricsTransfer.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-lock
|
// Auto-lock
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -60,7 +64,9 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
private var selectedAccount: MibAccount? = null
|
private var selectedAccount: MibAccount? = null
|
||||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||||
private val bmlSession get() = (requireActivity().application as BasedBankApp).bmlSession
|
private fun bmlSessionFor(account: MibAccount?) =
|
||||||
|
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
||||||
|
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
||||||
|
|
||||||
// Resolved recipient info — set after successful lookup or prefill
|
// Resolved recipient info — set after successful lookup or prefill
|
||||||
private var resolvedAccountNumber = ""
|
private var resolvedAccountNumber = ""
|
||||||
@@ -90,6 +96,11 @@ class TransferFragment : Fragment() {
|
|||||||
private const val ARG_SUBTITLE = "contact_subtitle"
|
private const val ARG_SUBTITLE = "contact_subtitle"
|
||||||
private const val ARG_COLOR = "contact_color"
|
private const val ARG_COLOR = "contact_color"
|
||||||
private const val ARG_IMAGE_HASH = "contact_image_hash"
|
private const val ARG_IMAGE_HASH = "contact_image_hash"
|
||||||
|
private const val ARG_FROM_ACCOUNT = "from_account"
|
||||||
|
|
||||||
|
fun newInstanceFrom(account: MibAccount) = TransferFragment().apply {
|
||||||
|
arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) }
|
||||||
|
}
|
||||||
|
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
accountNumber: String,
|
accountNumber: String,
|
||||||
@@ -149,6 +160,22 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startLookupLoading() {
|
||||||
|
val spinner = CircularProgressDrawable(requireContext()).apply {
|
||||||
|
setStyle(CircularProgressDrawable.DEFAULT)
|
||||||
|
setColorSchemeColors(com.google.android.material.color.MaterialColors.getColor(
|
||||||
|
requireView(), com.google.android.material.R.attr.colorPrimary, Color.GRAY))
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
binding.tilTo.endIconDrawable = spinner
|
||||||
|
binding.tilTo.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopLookupLoading() {
|
||||||
|
binding.tilTo.isEnabled = true
|
||||||
|
binding.tilTo.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupFromDropdown() {
|
private fun setupFromDropdown() {
|
||||||
binding.btnClearFromInfo.setOnClickListener {
|
binding.btnClearFromInfo.setOnClickListener {
|
||||||
selectedAccount = null
|
selectedAccount = null
|
||||||
@@ -168,6 +195,16 @@ class TransferFragment : Fragment() {
|
|||||||
updateAmountPrefix(picked)
|
updateAmountPrefix(picked)
|
||||||
showFromCard(picked)
|
showFromCard(picked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val fromNumber = arguments?.getString(ARG_FROM_ACCOUNT)
|
||||||
|
if (fromNumber != null && selectedAccount == null) {
|
||||||
|
val match = accounts.firstOrNull { it.accountNumber == fromNumber }
|
||||||
|
if (match != null) {
|
||||||
|
selectedAccount = match
|
||||||
|
updateAmountPrefix(match)
|
||||||
|
showFromCard(match)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +222,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
val typeLabel = when {
|
val typeLabel = when {
|
||||||
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
||||||
|
account.profileType == "BML_CREDIT" -> "Credit Card"
|
||||||
account.accountTypeName.isNotBlank() -> account.accountTypeName
|
account.accountTypeName.isNotBlank() -> account.accountTypeName
|
||||||
else -> account.profileType
|
else -> account.profileType
|
||||||
}
|
}
|
||||||
@@ -253,7 +291,7 @@ class TransferFragment : Fragment() {
|
|||||||
Toast.makeText(requireContext(), R.string.transfer_select_source_first, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.transfer_select_source_first, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val accountNumber = binding.etTo.text?.toString()?.trim() ?: ""
|
val accountNumber = AccountInputParser.normalize(binding.etTo.text?.toString()?.trim() ?: "")
|
||||||
if (accountNumber.isBlank()) {
|
if (accountNumber.isBlank()) {
|
||||||
Toast.makeText(requireContext(), R.string.transfer_enter_account_first, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.transfer_enter_account_first, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
@@ -284,7 +322,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val mibSess = session
|
val mibSess = session
|
||||||
val bmlSess = bmlSession
|
val bmlSess = bmlSessionFor(selectedAccount)
|
||||||
if (mibSess == null && bmlSess == null) {
|
if (mibSess == null && bmlSess == null) {
|
||||||
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
@@ -292,7 +330,7 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true
|
val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true
|
||||||
|
|
||||||
binding.tilTo.isEnabled = false
|
startLookupLoading()
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
var errorMsg: String? = null
|
var errorMsg: String? = null
|
||||||
@@ -336,7 +374,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.tilTo.isEnabled = true
|
stopLookupLoading()
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
val accounts = viewModel.accounts.value ?: emptyList()
|
val accounts = viewModel.accounts.value ?: emptyList()
|
||||||
val matchedAcc = accounts.firstOrNull { it.accountNumber == info.accountNumber }
|
val matchedAcc = accounts.firstOrNull { it.accountNumber == info.accountNumber }
|
||||||
@@ -371,7 +409,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupFahipayTarget(number: String) {
|
private fun lookupFahipayTarget(number: String) {
|
||||||
binding.tilTo.isEnabled = false
|
startLookupLoading()
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
data class LookupResult(
|
data class LookupResult(
|
||||||
val dhiraagu: DhiraaguClient.Result,
|
val dhiraagu: DhiraaguClient.Result,
|
||||||
@@ -398,7 +436,7 @@ class TransferFragment : Fragment() {
|
|||||||
LookupResult(d, o)
|
LookupResult(d, o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.tilTo.isEnabled = true
|
stopLookupLoading()
|
||||||
|
|
||||||
val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() }
|
val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
@@ -530,7 +568,7 @@ class TransferFragment : Fragment() {
|
|||||||
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
||||||
|
|
||||||
val isSrcBml = src.profileType.startsWith("BML")
|
val isSrcBml = src.profileType.startsWith("BML")
|
||||||
val isSrcCard = src.profileType == "BML_PREPAID"
|
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
||||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||||
val currency = src.currencyName.ifBlank { "MVR" }
|
val currency = src.currencyName.ifBlank { "MVR" }
|
||||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
val allAccounts = viewModel.accounts.value ?: emptyList()
|
||||||
@@ -559,35 +597,40 @@ class TransferFragment : Fragment() {
|
|||||||
?.transferCyDesc?.ifBlank { "MVR" }
|
?.transferCyDesc?.ifBlank { "MVR" }
|
||||||
?: if (isDestMib) "MVR" else "MVR"
|
?: if (isDestMib) "MVR" else "MVR"
|
||||||
val isUsdToMvr = currency.equals("USD", ignoreCase = true) && destCurrency.equals("MVR", ignoreCase = true)
|
val isUsdToMvr = currency.equals("USD", ignoreCase = true) && destCurrency.equals("MVR", ignoreCase = true)
|
||||||
|
val isSrcCredit = src.profileType == "BML_CREDIT"
|
||||||
|
|
||||||
val mainMsg = "Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}"
|
val mainMsg = "Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}"
|
||||||
|
|
||||||
val dialogBuilder = AlertDialog.Builder(requireContext())
|
val doTransfer: () -> Unit = {
|
||||||
.setTitle(R.string.transfer)
|
binding.btnTransfer.isEnabled = false
|
||||||
.setPositiveButton(R.string.transfer_confirm) { _, _ ->
|
(activity as? HomeActivity)?.setRefreshing(true)
|
||||||
binding.btnTransfer.isEnabled = false
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
if (!isSrcBml) {
|
||||||
if (!isSrcBml) {
|
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
||||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
} else {
|
||||||
} else {
|
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||||
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.btnTransfer.isEnabled = true
|
|
||||||
if (ok && receipt != null) {
|
|
||||||
clearForm()
|
|
||||||
val activity = requireActivity() as HomeActivity
|
|
||||||
activity.refreshBalances(src)
|
|
||||||
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
|
||||||
} else if (!ok) {
|
|
||||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.btnTransfer.isEnabled = true
|
||||||
|
(activity as? HomeActivity)?.setRefreshing(false)
|
||||||
|
if (ok && receipt != null) {
|
||||||
|
clearForm()
|
||||||
|
val activity = requireActivity() as HomeActivity
|
||||||
|
activity.refreshBalances(src)
|
||||||
|
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
||||||
|
} else if (!ok) {
|
||||||
|
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialogBuilder = AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.transfer)
|
||||||
|
.setPositiveButton(R.string.transfer_confirm) { _, _ -> doTransfer() }
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
|
||||||
if (isUsdToMvr) {
|
if (isUsdToMvr || isSrcCredit) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
val dp = resources.displayMetrics.density
|
val dp = resources.displayMetrics.density
|
||||||
val container = LinearLayout(ctx).apply {
|
val container = LinearLayout(ctx).apply {
|
||||||
@@ -595,18 +638,61 @@ class TransferFragment : Fragment() {
|
|||||||
setPadding((24 * dp).toInt(), (16 * dp).toInt(), (24 * dp).toInt(), 0)
|
setPadding((24 * dp).toInt(), (16 * dp).toInt(), (24 * dp).toInt(), 0)
|
||||||
}
|
}
|
||||||
container.addView(TextView(ctx).apply { text = mainMsg })
|
container.addView(TextView(ctx).apply { text = mainMsg })
|
||||||
container.addView(TextView(ctx).apply {
|
if (isUsdToMvr) {
|
||||||
text = "⚠ You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
|
container.addView(TextView(ctx).apply {
|
||||||
setTextColor(Color.RED)
|
text = "⚠ You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
|
||||||
textSize = 16f
|
setTextColor(Color.RED)
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
textSize = 16f
|
||||||
setPadding(0, (16 * dp).toInt(), 0, 0)
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
})
|
setPadding(0, (16 * dp).toInt(), 0, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (isSrcCredit) {
|
||||||
|
container.addView(TextView(ctx).apply {
|
||||||
|
text = "⚠ Transferring from a credit card is treated as a cash advance. Cash advance fees will be charged on the 10th of the month."
|
||||||
|
setTextColor(Color.RED)
|
||||||
|
textSize = 16f
|
||||||
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
|
setPadding(0, (16 * dp).toInt(), 0, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
dialogBuilder.setView(container)
|
dialogBuilder.setView(container)
|
||||||
} else {
|
} else {
|
||||||
dialogBuilder.setMessage(mainMsg)
|
dialogBuilder.setMessage(mainMsg)
|
||||||
}
|
}
|
||||||
dialogBuilder.show()
|
|
||||||
|
val dialog = dialogBuilder.show()
|
||||||
|
|
||||||
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
val biometricTransferConfirm = prefs.getBoolean("biometrics_transfer_confirm", false)
|
||||||
|
val canAuth = BiometricManager.from(requireContext())
|
||||||
|
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
if (biometricTransferConfirm && canAuth) {
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
val prompt = BiometricPrompt(this, ContextCompat.getMainExecutor(requireContext()),
|
||||||
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
|
dialog.dismiss()
|
||||||
|
doTransfer()
|
||||||
|
}
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
if (errorCode != BiometricPrompt.ERROR_CANCELED &&
|
||||||
|
errorCode != BiometricPrompt.ERROR_USER_CANCELED &&
|
||||||
|
errorCode != BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
|
||||||
|
Toast.makeText(requireContext(), errString, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onAuthenticationFailed() { /* keep dialog open */ }
|
||||||
|
})
|
||||||
|
prompt.authenticate(
|
||||||
|
BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle(getString(R.string.biometric_transfer_title))
|
||||||
|
.setSubtitle("$currency $amountStr → $destDisplay")
|
||||||
|
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doMibTransfer(
|
private fun doMibTransfer(
|
||||||
@@ -689,8 +775,9 @@ class TransferFragment : Fragment() {
|
|||||||
allAccounts: List<MibAccount>,
|
allAccounts: List<MibAccount>,
|
||||||
allContacts: List<MibBeneficiary>
|
allContacts: List<MibBeneficiary>
|
||||||
): Triple<Boolean, String, TransferReceiptData?> {
|
): Triple<Boolean, String, TransferReceiptData?> {
|
||||||
val sess = bmlSession ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
val loginId = src.loginTag.removePrefix("bml_")
|
||||||
val otp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||||
|
val otp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
||||||
?.let { Totp.generate(it) }
|
?.let { Totp.generate(it) }
|
||||||
?: return Triple(false, "OTP unavailable", null)
|
?: return Triple(false, "OTP unavailable", null)
|
||||||
val debitAccount = src.internalId.ifBlank {
|
val debitAccount = src.internalId.ifBlank {
|
||||||
@@ -698,7 +785,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine type + credit account
|
// Determine type + credit account
|
||||||
val isDestMyCard = allAccounts.any { it.profileType == "BML_PREPAID" && it.accountNumber == destAccount }
|
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
||||||
val (transferType, creditAccount, bank) = when {
|
val (transferType, creditAccount, bank) = when {
|
||||||
isSrcCard -> {
|
isSrcCard -> {
|
||||||
// CAD: card → own BML account
|
// CAD: card → own BML account
|
||||||
@@ -707,7 +794,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
isDestMyCard -> {
|
isDestMyCard -> {
|
||||||
// CPA: BML CASA → own card top-up
|
// CPA: BML CASA → own card top-up
|
||||||
val card = allAccounts.first { it.profileType == "BML_PREPAID" && it.accountNumber == destAccount }
|
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
||||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
||||||
}
|
}
|
||||||
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
||||||
@@ -730,7 +817,7 @@ class TransferFragment : Fragment() {
|
|||||||
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
||||||
|
|
||||||
// Step 2: confirm with fresh OTP
|
// Step 2: confirm with fresh OTP
|
||||||
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
||||||
?.let { Totp.generate(it) } ?: otp
|
?.let { Totp.generate(it) } ?: otp
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
@@ -880,8 +967,8 @@ class TransferFragment : Fragment() {
|
|||||||
) : BaseAdapter(), Filterable {
|
) : BaseAdapter(), Filterable {
|
||||||
|
|
||||||
private val items: List<Any> = buildList {
|
private val items: List<Any> = buildList {
|
||||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" }
|
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
|
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||||
addAll(regular)
|
addAll(regular)
|
||||||
if (cards.isNotEmpty()) {
|
if (cards.isNotEmpty()) {
|
||||||
add(getString(R.string.cards))
|
add(getString(R.string.cards))
|
||||||
@@ -890,7 +977,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
|
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
|
||||||
?.takeUnless { it.profileType == "BML_PREPAID" && !it.statusDesc.equals("Active", ignoreCase = true) }
|
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||||
|
|
||||||
override fun getCount() = items.size
|
override fun getCount() = items.size
|
||||||
override fun getItem(position: Int) = items[position]
|
override fun getItem(position: Int) = items[position]
|
||||||
@@ -921,8 +1008,10 @@ class TransferFragment : Fragment() {
|
|||||||
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
.also { it.root.tag = it }
|
.also { it.root.tag = it }
|
||||||
}
|
}
|
||||||
val inactive = acc.profileType == "BML_PREPAID" && !acc.statusDesc.equals("Active", ignoreCase = true)
|
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||||
b.tvDropdownAccountName.text = acc.accountBriefName
|
val isBmlAccount = acc.profileType.startsWith("BML")
|
||||||
|
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||||
|
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
||||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||||
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"
|
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"
|
||||||
b.root.alpha = if (inactive) 0.4f else 1f
|
b.root.alpha = if (inactive) 0.4f else 1f
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
) {
|
) {
|
||||||
fun hasMore(): Boolean = when {
|
fun hasMore(): Boolean = when {
|
||||||
account.profileType == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
account.profileType == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||||
account.profileType == "BML_PREPAID" -> cardMonthOffset < 2
|
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
|
||||||
account.profileType.startsWith("BML") -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
account.profileType.startsWith("BML") -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||||
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,6 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
|
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val mibSession = app.mibSession
|
val mibSession = app.mibSession
|
||||||
val bmlSession = app.bmlSession
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val newTransactions = withContext(Dispatchers.IO) {
|
val newTransactions = withContext(Dispatchers.IO) {
|
||||||
@@ -155,8 +154,8 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
async {
|
async {
|
||||||
try {
|
try {
|
||||||
when {
|
when {
|
||||||
state.account.profileType == "BML_PREPAID" -> {
|
state.account.profileType == "BML_PREPAID" || state.account.profileType == "BML_CREDIT" -> {
|
||||||
val session = bmlSession ?: return@async emptyList()
|
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
|
||||||
val cal = Calendar.getInstance()
|
val cal = Calendar.getInstance()
|
||||||
cal.add(Calendar.MONTH, -state.cardMonthOffset)
|
cal.add(Calendar.MONTH, -state.cardMonthOffset)
|
||||||
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
|
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
|
||||||
@@ -170,7 +169,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val session = bmlSession ?: return@async emptyList()
|
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
|
||||||
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
||||||
session = session,
|
session = session,
|
||||||
accountId = state.account.internalId,
|
accountId = state.account.internalId,
|
||||||
|
|||||||
@@ -313,6 +313,12 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
requireActivity().title = "Receipt"
|
requireActivity().title = "Receipt"
|
||||||
|
(activity as? HomeActivity)?.setBottomNavVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
(activity as? HomeActivity)?.setBottomNavVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -67,11 +67,27 @@ class CredentialsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnLogin.isEnabled = false
|
||||||
binding.btnLogin.setOnClickListener { attemptLogin() }
|
binding.btnLogin.setOnClickListener { attemptLogin() }
|
||||||
|
|
||||||
|
val loginFieldWatcher = object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) { updateLoginButtonState() }
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
}
|
||||||
|
binding.etUsername.addTextChangedListener(loginFieldWatcher)
|
||||||
|
binding.etPassword.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) { updateLoginButtonState(); updateOtpDisplay() }
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
})
|
||||||
|
|
||||||
if (bankType != "FAHIPAY") {
|
if (bankType != "FAHIPAY") {
|
||||||
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
|
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) { updateOtpDisplay() }
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
updateOtpDisplay()
|
||||||
|
updateLoginButtonState()
|
||||||
|
}
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
})
|
})
|
||||||
@@ -88,10 +104,35 @@ class CredentialsFragment : Fragment() {
|
|||||||
otpHandler.removeCallbacks(otpRunnable)
|
otpHandler.removeCallbacks(otpRunnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveOtpSeed(input: String): String {
|
||||||
|
val secret = if (input.startsWith("otpauth://totp/"))
|
||||||
|
android.net.Uri.parse(input).getQueryParameter("secret") ?: input
|
||||||
|
else
|
||||||
|
input
|
||||||
|
return secret.replace("\\s".toRegex(), "").replace("-", "").uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLoginButtonState() {
|
||||||
|
val username = binding.etUsername.text.toString().trim()
|
||||||
|
val password = binding.etPassword.text.toString()
|
||||||
|
val otpSeedRaw = binding.etOtpSeed.text.toString().trim()
|
||||||
|
val otpSeed = resolveOtpSeed(otpSeedRaw)
|
||||||
|
binding.btnLogin.isEnabled = when (bankType) {
|
||||||
|
"FAHIPAY" -> username.isNotEmpty() && password.isNotEmpty()
|
||||||
|
else -> username.isNotEmpty() && password.isNotEmpty() && otpSeed.isNotEmpty() && password != otpSeedRaw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateOtpDisplay() {
|
private fun updateOtpDisplay() {
|
||||||
val seed = binding.etOtpSeed.text.toString().trim()
|
val otpSeedRaw = binding.etOtpSeed.text.toString().trim()
|
||||||
|
val seed = resolveOtpSeed(otpSeedRaw)
|
||||||
if (seed.isEmpty()) {
|
if (seed.isEmpty()) {
|
||||||
binding.cardOtp.visibility = View.GONE
|
binding.cardOtp.visibility = View.INVISIBLE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val password = binding.etPassword.text.toString()
|
||||||
|
if (otpSeedRaw == password || seed.matches(Regex("\\d{6}"))) {
|
||||||
|
binding.cardOtp.visibility = View.INVISIBLE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -104,7 +145,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
binding.otpTimer.progress = remaining
|
binding.otpTimer.progress = remaining
|
||||||
binding.cardOtp.visibility = View.VISIBLE
|
binding.cardOtp.visibility = View.VISIBLE
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
binding.cardOtp.visibility = View.GONE
|
binding.cardOtp.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +157,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
|
|
||||||
val username = binding.etUsername.text.toString().trim()
|
val username = binding.etUsername.text.toString().trim()
|
||||||
val password = binding.etPassword.text.toString()
|
val password = binding.etPassword.text.toString()
|
||||||
val otpSeed = binding.etOtpSeed.text.toString().trim()
|
val otpSeed = resolveOtpSeed(binding.etOtpSeed.text.toString().trim())
|
||||||
|
|
||||||
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
||||||
binding.tvError.text = "Please fill in all fields"
|
binding.tvError.text = "Please fill in all fields"
|
||||||
@@ -173,7 +214,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
private fun attemptBmlLogin() {
|
private fun attemptBmlLogin() {
|
||||||
val username = binding.etUsername.text.toString().trim()
|
val username = binding.etUsername.text.toString().trim()
|
||||||
val password = binding.etPassword.text.toString()
|
val password = binding.etPassword.text.toString()
|
||||||
val otpSeed = binding.etOtpSeed.text.toString().trim()
|
val otpSeed = resolveOtpSeed(binding.etOtpSeed.text.toString().trim())
|
||||||
|
|
||||||
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
||||||
binding.tvError.text = "Please fill in all fields"
|
binding.tvError.text = "Please fill in all fields"
|
||||||
@@ -185,6 +226,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
binding.btnLogin.isEnabled = false
|
binding.btnLogin.isEnabled = false
|
||||||
|
|
||||||
|
val loginId = username
|
||||||
val flow = BmlLoginFlow()
|
val flow = BmlLoginFlow()
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -192,11 +234,12 @@ class CredentialsFragment : Fragment() {
|
|||||||
flow.login(username, password, otpSeed)
|
flow.login(username, password, otpSeed)
|
||||||
}
|
}
|
||||||
val store = CredentialStore(requireContext())
|
val store = CredentialStore(requireContext())
|
||||||
store.saveBmlCredentials(username, password, otpSeed)
|
store.saveBmlCredentials(loginId, username, password, otpSeed)
|
||||||
store.saveBmlSession(session.accessToken, session.deviceId)
|
store.saveBmlSession(loginId, session.accessToken, session.deviceId)
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val info = flow.fetchUserInfo(session)
|
val info = flow.fetchUserInfo(session)
|
||||||
if (info != null) store.saveBmlUserProfile(
|
if (info != null) store.saveBmlUserProfile(
|
||||||
|
loginId,
|
||||||
CredentialStore.BmlUserProfile(
|
CredentialStore.BmlUserProfile(
|
||||||
fullName = info.fullName,
|
fullName = info.fullName,
|
||||||
email = info.email,
|
email = info.email,
|
||||||
@@ -207,11 +250,11 @@ class CredentialsFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AccountCache.saveBml(requireContext(), accounts)
|
AccountCache.saveBml(requireContext(), loginId, accounts)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.bmlSession = session
|
app.bmlSessions[loginId] = session
|
||||||
app.bmlAccounts = accounts
|
// Merge with any existing BML accounts from other logins
|
||||||
// Merge with any existing MIB accounts already in app
|
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$loginId" } + accounts
|
||||||
app.accounts = app.accounts + accounts
|
app.accounts = app.accounts + accounts
|
||||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package sh.sar.basedbank.ui.login
|
package sh.sar.basedbank.ui.login
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import sh.sar.basedbank.databinding.ActivityLoginBinding
|
import sh.sar.basedbank.databinding.ActivityLoginBinding
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
@@ -10,7 +12,13 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
val isLight = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = isLight
|
||||||
|
isAppearanceLightNavigationBars = isLight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ package sh.sar.basedbank.ui.onboarding
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.CountDownTimer
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
@@ -17,46 +21,68 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
|||||||
|
|
||||||
private lateinit var binding: ActivityOnboardingBinding
|
private lateinit var binding: ActivityOnboardingBinding
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var prefs: SharedPreferences
|
||||||
|
private var countDownTimer: CountDownTimer? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
binding = ActivityOnboardingBinding.inflate(layoutInflater)
|
binding = ActivityOnboardingBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
val isLight = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = isLight
|
||||||
|
isAppearanceLightNavigationBars = isLight
|
||||||
|
}
|
||||||
prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||||
|
val originalBottomPadding = binding.bottomBar.paddingBottom
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets ->
|
||||||
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, originalBottomPadding + navBar.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = OnboardingPagerAdapter(this)
|
val adapter = OnboardingPagerAdapter(this)
|
||||||
binding.viewPager.adapter = adapter
|
binding.viewPager.adapter = adapter
|
||||||
|
|
||||||
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach()
|
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach()
|
||||||
|
// Disable tap-to-navigate on dots: touch listener must be on the individual
|
||||||
// Pre-select language chip without triggering the listener
|
// tab views inside SlidingTabStrip (child 0), because they consume ACTION_DOWN
|
||||||
val savedLang = prefs.getString("language", null)
|
// before the TabLayout's own touch listener ever fires.
|
||||||
binding.languageChipGroup.setOnCheckedStateChangeListener(null)
|
val tabStrip = binding.dotsIndicator.getChildAt(0) as? android.view.ViewGroup
|
||||||
when (savedLang) {
|
tabStrip?.let {
|
||||||
"en" -> binding.chipEnglish.isChecked = true
|
for (i in 0 until it.childCount) {
|
||||||
"dv" -> binding.chipDhivehi.isChecked = true
|
it.getChildAt(i).setOnTouchListener { _, _ -> true }
|
||||||
}
|
|
||||||
binding.languageChipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
|
|
||||||
if (checkedIds.isNotEmpty()) {
|
|
||||||
selectLanguage(if (checkedIds[0] == R.id.chipEnglish) "en" else "dv")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-select language button without triggering the listener
|
||||||
|
val savedLang = prefs.getString("language", null)
|
||||||
|
binding.languageToggle.clearOnButtonCheckedListeners()
|
||||||
|
when (savedLang) {
|
||||||
|
"en" -> binding.btnLangEnglish.isChecked = true
|
||||||
|
"dv" -> binding.btnLangDhivehi.isChecked = true
|
||||||
|
}
|
||||||
|
binding.languageToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||||
|
if (isChecked) selectLanguage(if (checkedId == R.id.btnLangEnglish) "en" else "dv")
|
||||||
|
}
|
||||||
|
|
||||||
|
supportFragmentManager.setFragmentResultListener(OnboardingFragment.RESULT_SCROLLED_TO_BOTTOM, this) { _, _ ->
|
||||||
|
startGetStartedCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
binding.languageChipGroup.visibility = if (position == 0) View.VISIBLE else View.GONE
|
binding.languageSection.visibility = if (position == 0) View.VISIBLE else View.GONE
|
||||||
// Block forward swipe on slide 1 until security is set up
|
binding.viewPager.isUserInputEnabled = when {
|
||||||
if (position == 1) {
|
position > 2 -> false
|
||||||
binding.viewPager.isUserInputEnabled =
|
position == 1 -> prefs.getString("security_method", null) != null
|
||||||
prefs.getString("security_method", null) != null
|
else -> true
|
||||||
} else {
|
|
||||||
binding.viewPager.isUserInputEnabled = true
|
|
||||||
}
|
}
|
||||||
updateButtons(position, adapter.itemCount)
|
updateButtons(position, adapter.itemCount)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
binding.languageChipGroup.visibility = View.VISIBLE
|
binding.languageSection.visibility = View.VISIBLE
|
||||||
updateButtons(0, adapter.itemCount)
|
updateButtons(0, adapter.itemCount)
|
||||||
|
|
||||||
binding.btnNext.setOnClickListener {
|
binding.btnNext.setOnClickListener {
|
||||||
@@ -71,10 +97,21 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// Called by SecuritySetupFragment when setup is complete
|
// Called by SecuritySetupFragment when setup is complete
|
||||||
override fun onSecuritySetupComplete() {
|
override fun onSecuritySetupComplete() {
|
||||||
binding.viewPager.isUserInputEnabled = true
|
binding.viewPager.isUserInputEnabled = true
|
||||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
|
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by SecuritySetupFragment when user resets to reconfigure
|
||||||
|
override fun onSecuritySetupReset() {
|
||||||
|
binding.viewPager.isUserInputEnabled = false
|
||||||
|
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectLanguage(lang: String) {
|
private fun selectLanguage(lang: String) {
|
||||||
@@ -83,23 +120,34 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
|||||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
|
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startGetStartedCountdown() {
|
||||||
|
binding.btnGetStarted.isEnabled = false
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
countDownTimer = object : CountDownTimer(5000, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
val seconds = (millisUntilFinished / 1000 + 1).toInt()
|
||||||
|
binding.btnGetStarted.text = "${getString(R.string.get_started)} ($seconds)"
|
||||||
|
}
|
||||||
|
override fun onFinish() {
|
||||||
|
binding.btnGetStarted.text = getString(R.string.get_started)
|
||||||
|
binding.btnGetStarted.isEnabled = true
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateButtons(position: Int, count: Int) {
|
private fun updateButtons(position: Int, count: Int) {
|
||||||
val langSelected = prefs.getString("language", null) != null
|
val langSelected = prefs.getString("language", null) != null
|
||||||
val securityDone = prefs.getString("security_method", null) != null
|
val securityDone = prefs.getString("security_method", null) != null
|
||||||
val isLast = position == count - 1
|
val isLast = position == count - 1
|
||||||
|
|
||||||
binding.btnGetStarted.visibility = if (isLast) View.VISIBLE else View.GONE
|
binding.btnGetStarted.visibility = if (isLast) View.VISIBLE else View.GONE
|
||||||
|
if (isLast) binding.btnGetStarted.isEnabled = false
|
||||||
|
|
||||||
// Hide Next on slide 1 until security is done (avoids a disabled-button-with-no-explanation)
|
binding.btnNext.visibility = if (isLast) View.GONE else View.VISIBLE
|
||||||
binding.btnNext.visibility = when {
|
|
||||||
isLast -> View.GONE
|
|
||||||
position == 1 && !securityDone -> View.GONE
|
|
||||||
else -> View.VISIBLE
|
|
||||||
}
|
|
||||||
binding.btnNext.isEnabled = when (position) {
|
binding.btnNext.isEnabled = when (position) {
|
||||||
0 -> langSelected
|
0 -> langSelected
|
||||||
1 -> securityDone
|
1 -> securityDone
|
||||||
else -> true
|
else -> true // position 2 (configure) has no gate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package sh.sar.basedbank.ui.onboarding
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.databinding.FragmentOnboardingConfigureBinding
|
||||||
|
|
||||||
|
class OnboardingConfigureFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentOnboardingConfigureBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
_binding = FragmentOnboardingConfigureBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// Navigation — default Drawer
|
||||||
|
val isBottom = prefs.getBoolean("bottom_nav", false)
|
||||||
|
binding.navModeToggle.check(if (isBottom) R.id.btnNavBottom else R.id.btnNavDrawer)
|
||||||
|
binding.navModeToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||||
|
if (!isChecked) return@addOnButtonCheckedListener
|
||||||
|
prefs.edit().putBoolean("bottom_nav", checkedId == R.id.btnNavBottom).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme — default System
|
||||||
|
val savedTheme = prefs.getString("theme", "system")
|
||||||
|
binding.themeToggle.check(when (savedTheme) {
|
||||||
|
"light" -> R.id.btnThemeLight
|
||||||
|
"dark" -> R.id.btnThemeDark
|
||||||
|
else -> R.id.btnThemeSystem
|
||||||
|
})
|
||||||
|
binding.themeToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||||
|
if (!isChecked) return@addOnButtonCheckedListener
|
||||||
|
val (key, mode) = when (checkedId) {
|
||||||
|
R.id.btnThemeLight -> "light" to AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
R.id.btnThemeDark -> "dark" to AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
else -> "system" to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
|
}
|
||||||
|
prefs.edit().putString("theme", key).apply()
|
||||||
|
AppCompatDelegate.setDefaultNightMode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Biometrics
|
||||||
|
val canUseBiometrics = BiometricManager.from(requireContext())
|
||||||
|
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
if (canUseBiometrics) {
|
||||||
|
val unlockEnabled = prefs.getBoolean("biometrics_enabled", false)
|
||||||
|
binding.switchBiometrics.isChecked = unlockEnabled
|
||||||
|
binding.switchBiometricsTransfer.isChecked = prefs.getBoolean("biometrics_transfer_confirm", false)
|
||||||
|
binding.switchBiometricsTransfer.isEnabled = unlockEnabled
|
||||||
|
|
||||||
|
binding.switchBiometrics.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
prefs.edit().putBoolean("biometrics_enabled", isChecked).apply()
|
||||||
|
binding.switchBiometricsTransfer.isEnabled = isChecked
|
||||||
|
if (!isChecked) {
|
||||||
|
binding.switchBiometricsTransfer.isChecked = false
|
||||||
|
prefs.edit().putBoolean("biometrics_transfer_confirm", false).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchBiometricsTransfer.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
prefs.edit().putBoolean("biometrics_transfer_confirm", isChecked).apply()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.tvBiometricsHint.visibility = View.VISIBLE
|
||||||
|
binding.switchBiometrics.isEnabled = false
|
||||||
|
binding.switchBiometricsTransfer.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block screenshots — default on
|
||||||
|
val blockScreenshots = prefs.getBoolean("block_screenshots", true)
|
||||||
|
binding.switchBlockScreenshots.isChecked = blockScreenshots
|
||||||
|
applyFlagSecure(blockScreenshots)
|
||||||
|
binding.switchBlockScreenshots.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
prefs.edit().putBoolean("block_screenshots", isChecked).apply()
|
||||||
|
applyFlagSecure(isChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyFlagSecure(enabled: Boolean) {
|
||||||
|
val win = activity?.window ?: return
|
||||||
|
if (enabled) win.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
else win.clearFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import android.widget.ScrollView
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import sh.sar.basedbank.databinding.FragmentOnboardingSlideBinding
|
import sh.sar.basedbank.databinding.FragmentOnboardingSlideBinding
|
||||||
@@ -12,6 +14,7 @@ class OnboardingFragment : Fragment() {
|
|||||||
|
|
||||||
private var _binding: FragmentOnboardingSlideBinding? = null
|
private var _binding: FragmentOnboardingSlideBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private var scrolledToBottom = false
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentOnboardingSlideBinding.inflate(inflater, container, false)
|
_binding = FragmentOnboardingSlideBinding.inflate(inflater, container, false)
|
||||||
@@ -19,17 +22,45 @@ class OnboardingFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val title = requireArguments().getString(ARG_TITLE, "")
|
val titleRes = requireArguments().getInt(ARG_TITLE)
|
||||||
val desc = requireArguments().getString(ARG_DESC, "")
|
val descRes = requireArguments().getInt(ARG_DESC)
|
||||||
val icon = requireArguments().getInt(ARG_ICON, 0)
|
val icon = requireArguments().getInt(ARG_ICON, 0)
|
||||||
val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false)
|
val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false)
|
||||||
|
val isLast = requireArguments().getBoolean(ARG_IS_LAST, false)
|
||||||
|
|
||||||
|
binding.icon.visibility = if (isLast) View.GONE else View.VISIBLE
|
||||||
binding.icon.setImageResource(icon)
|
binding.icon.setImageResource(icon)
|
||||||
binding.title.text = title
|
binding.title.text = getString(titleRes)
|
||||||
binding.description.text = desc
|
binding.description.text = getString(descRes)
|
||||||
|
binding.description.gravity = if (isLast) android.view.Gravity.START else android.view.Gravity.CENTER
|
||||||
|
|
||||||
// On the first slide, show the two placeholder cards for upcoming banks
|
// On the first slide, show the two placeholder cards for upcoming banks
|
||||||
binding.placeholderCards.visibility = if (isFirst) View.VISIBLE else View.GONE
|
binding.placeholderCards.visibility = if (isFirst) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
if (isLast) setupScrollToBottomDetection()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupScrollToBottomDetection() {
|
||||||
|
val scrollView = binding.scrollView
|
||||||
|
// If content fits without scrolling, fire immediately after layout
|
||||||
|
scrollView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
override fun onGlobalLayout() {
|
||||||
|
scrollView.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||||
|
val child = scrollView.getChildAt(0) ?: return
|
||||||
|
if (child.height <= scrollView.height) notifyScrolledToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
scrollView.setOnScrollChangeListener { v, _, scrollY, _, _ ->
|
||||||
|
val sv = v as ScrollView
|
||||||
|
val child = sv.getChildAt(0) ?: return@setOnScrollChangeListener
|
||||||
|
if (scrollY + sv.height >= child.height) notifyScrolledToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyScrolledToBottom() {
|
||||||
|
if (scrolledToBottom) return
|
||||||
|
scrolledToBottom = true
|
||||||
|
parentFragmentManager.setFragmentResult(RESULT_SCROLLED_TO_BOTTOM, Bundle.EMPTY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -38,17 +69,20 @@ class OnboardingFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val RESULT_SCROLLED_TO_BOTTOM = "scroll_to_bottom"
|
||||||
private const val ARG_TITLE = "title"
|
private const val ARG_TITLE = "title"
|
||||||
private const val ARG_DESC = "desc"
|
private const val ARG_DESC = "desc"
|
||||||
private const val ARG_ICON = "icon"
|
private const val ARG_ICON = "icon"
|
||||||
private const val ARG_IS_FIRST = "is_first"
|
private const val ARG_IS_FIRST = "is_first"
|
||||||
|
private const val ARG_IS_LAST = "is_last"
|
||||||
|
|
||||||
fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply {
|
fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply {
|
||||||
arguments = bundleOf(
|
arguments = bundleOf(
|
||||||
ARG_TITLE to slide.title,
|
ARG_TITLE to slide.titleRes,
|
||||||
ARG_DESC to slide.description,
|
ARG_DESC to slide.descRes,
|
||||||
ARG_ICON to slide.iconRes,
|
ARG_ICON to slide.iconRes,
|
||||||
ARG_IS_FIRST to slide.isFirst
|
ARG_IS_FIRST to slide.isFirst,
|
||||||
|
ARG_IS_LAST to slide.isLast
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,36 +9,39 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
|
|||||||
|
|
||||||
private val slides = listOf(
|
private val slides = listOf(
|
||||||
OnboardingSlide(
|
OnboardingSlide(
|
||||||
title = activity.getString(R.string.onboarding_title_1),
|
titleRes = R.string.onboarding_title_1,
|
||||||
description = activity.getString(R.string.onboarding_desc_1),
|
descRes = R.string.onboarding_desc_1,
|
||||||
iconRes = R.drawable.ic_launcher_foreground,
|
iconRes = R.drawable.ic_launcher_foreground,
|
||||||
isFirst = true
|
isFirst = true
|
||||||
),
|
),
|
||||||
OnboardingSlide(
|
OnboardingSlide(
|
||||||
title = activity.getString(R.string.onboarding_title_2),
|
titleRes = R.string.onboarding_title_2,
|
||||||
description = activity.getString(R.string.onboarding_desc_2),
|
descRes = R.string.onboarding_desc_2,
|
||||||
iconRes = R.drawable.ic_launcher_foreground,
|
iconRes = R.drawable.ic_launcher_foreground,
|
||||||
isFirst = false
|
isFirst = false
|
||||||
),
|
),
|
||||||
OnboardingSlide(
|
OnboardingSlide(
|
||||||
title = activity.getString(R.string.onboarding_title_3),
|
titleRes = R.string.onboarding_title_3,
|
||||||
description = activity.getString(R.string.onboarding_desc_3),
|
descRes = R.string.onboarding_desc_3,
|
||||||
iconRes = R.drawable.ic_launcher_foreground,
|
iconRes = R.drawable.ic_launcher_foreground,
|
||||||
isFirst = false
|
isFirst = false,
|
||||||
|
isLast = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getItemCount() = slides.size
|
override fun getItemCount() = slides.size + 1 // +1 for OnboardingConfigureFragment at position 2
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = when (position) {
|
override fun createFragment(position: Int): Fragment = when (position) {
|
||||||
1 -> SecuritySetupFragment()
|
1 -> SecuritySetupFragment()
|
||||||
else -> OnboardingFragment.newInstance(slides[position])
|
2 -> OnboardingConfigureFragment()
|
||||||
|
else -> OnboardingFragment.newInstance(slides[position - if (position > 2) 1 else 0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class OnboardingSlide(
|
data class OnboardingSlide(
|
||||||
val title: String,
|
val titleRes: Int,
|
||||||
val description: String,
|
val descRes: Int,
|
||||||
val iconRes: Int,
|
val iconRes: Int,
|
||||||
val isFirst: Boolean
|
val isFirst: Boolean,
|
||||||
|
val isLast: Boolean = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,17 +86,21 @@ class PatternView @JvmOverloads constructor(
|
|||||||
if (errorState) return false
|
if (errorState) return false
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
recording = true; selected.clear()
|
recording = true; selected.clear()
|
||||||
hit(event.x, event.y)
|
hit(event.x, event.y)
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
touchX = event.x; touchY = event.y
|
touchX = event.x; touchY = event.y
|
||||||
hit(event.x, event.y)
|
hit(event.x, event.y)
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
recording = false
|
recording = false
|
||||||
invalidate()
|
invalidate()
|
||||||
onPatternComplete?.invoke(selected.map { it.index })
|
if (event.action == MotionEvent.ACTION_UP)
|
||||||
|
onPatternComplete?.invoke(selected.map { it.index })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
@@ -21,6 +20,7 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onSecuritySetupComplete()
|
fun onSecuritySetupComplete()
|
||||||
|
fun onSecuritySetupReset()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -33,7 +33,7 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
private var _b: FragmentSecuritySetupBinding? = null
|
private var _b: FragmentSecuritySetupBinding? = null
|
||||||
private val b get() = _b!!
|
private val b get() = _b!!
|
||||||
|
|
||||||
private enum class Step { CHOOSE, PIN_ENTER, PIN_CONFIRM, PATTERN_ENTER, PATTERN_CONFIRM, BIOMETRIC }
|
private enum class Step { CONFIGURED, CHOOSE, PIN_ENTER, PIN_CONFIRM, PATTERN_ENTER, PATTERN_CONFIRM }
|
||||||
|
|
||||||
private var step = Step.CHOOSE
|
private var step = Step.CHOOSE
|
||||||
private val pinDigits = mutableListOf<Int>()
|
private val pinDigits = mutableListOf<Int>()
|
||||||
@@ -48,9 +48,6 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
val changeMode = arguments?.getBoolean(ARG_CHANGE_MODE, false) ?: false
|
val changeMode = arguments?.getBoolean(ARG_CHANGE_MODE, false) ?: false
|
||||||
if (!changeMode && prefs.getString("security_method", null) != null) {
|
|
||||||
(activity as? Callback)?.onSecuritySetupComplete()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.cardPin.setOnClickListener { goTo(Step.PIN_ENTER) }
|
b.cardPin.setOnClickListener { goTo(Step.PIN_ENTER) }
|
||||||
b.cardPattern.setOnClickListener { goTo(Step.PATTERN_ENTER) }
|
b.cardPattern.setOnClickListener { goTo(Step.PATTERN_ENTER) }
|
||||||
@@ -62,20 +59,21 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.btnPatternBack.setOnClickListener { goTo(Step.CHOOSE) }
|
b.btnPatternBack.setOnClickListener { goTo(Step.CHOOSE) }
|
||||||
|
b.btnChangeLock.setOnClickListener {
|
||||||
|
prefs.edit().remove("security_method").apply()
|
||||||
|
(activity as? Callback)?.onSecuritySetupReset()
|
||||||
|
goTo(Step.CHOOSE)
|
||||||
|
}
|
||||||
|
|
||||||
b.patternView.onPatternComplete = { pattern -> handlePattern(pattern) }
|
b.patternView.onPatternComplete = { pattern -> handlePattern(pattern) }
|
||||||
|
|
||||||
b.btnEnableBiometrics.setOnClickListener {
|
|
||||||
prefs.edit().putBoolean("biometrics_enabled", true).apply()
|
|
||||||
finishSetup()
|
|
||||||
}
|
|
||||||
b.btnSkipBiometrics.setOnClickListener {
|
|
||||||
prefs.edit().putBoolean("biometrics_enabled", false).apply()
|
|
||||||
finishSetup()
|
|
||||||
}
|
|
||||||
|
|
||||||
buildNumpad()
|
buildNumpad()
|
||||||
goTo(Step.CHOOSE)
|
|
||||||
|
if (!changeMode && prefs.getString("security_method", null) != null) {
|
||||||
|
goTo(Step.CONFIGURED)
|
||||||
|
} else {
|
||||||
|
goTo(Step.CHOOSE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNumpad() {
|
private fun buildNumpad() {
|
||||||
@@ -144,7 +142,7 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
Step.PIN_CONFIRM -> {
|
Step.PIN_CONFIRM -> {
|
||||||
if (entered == firstPin) {
|
if (entered == firstPin) {
|
||||||
saveCredential("pin", entered)
|
saveCredential("pin", entered)
|
||||||
goToBiometricOrFinish()
|
finishSetup()
|
||||||
} else {
|
} else {
|
||||||
b.tvPinDots.text = getString(R.string.pin_no_match)
|
b.tvPinDots.text = getString(R.string.pin_no_match)
|
||||||
pinDigits.clear()
|
pinDigits.clear()
|
||||||
@@ -172,7 +170,7 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
Step.PATTERN_CONFIRM -> {
|
Step.PATTERN_CONFIRM -> {
|
||||||
if (pattern == firstPattern) {
|
if (pattern == firstPattern) {
|
||||||
saveCredential("pattern", pattern.joinToString(""))
|
saveCredential("pattern", pattern.joinToString(""))
|
||||||
goToBiometricOrFinish()
|
finishSetup()
|
||||||
} else {
|
} else {
|
||||||
b.patternView.showError()
|
b.patternView.showError()
|
||||||
b.tvPatternStatus.text = getString(R.string.pattern_no_match)
|
b.tvPatternStatus.text = getString(R.string.pattern_no_match)
|
||||||
@@ -187,29 +185,12 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goToBiometricOrFinish() {
|
|
||||||
// In change mode, biometrics is managed from Settings — skip this step
|
|
||||||
if (arguments?.getBoolean(ARG_CHANGE_MODE, false) == true) {
|
|
||||||
finishSetup()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val canAuth = BiometricManager.from(requireContext())
|
|
||||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
|
|
||||||
if (canAuth == BiometricManager.BIOMETRIC_SUCCESS) {
|
|
||||||
goTo(Step.BIOMETRIC)
|
|
||||||
} else {
|
|
||||||
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
.edit().putBoolean("biometrics_enabled", false).apply()
|
|
||||||
finishSetup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goTo(s: Step) {
|
private fun goTo(s: Step) {
|
||||||
step = s
|
step = s
|
||||||
|
b.viewConfigured.visibility = if (s == Step.CONFIGURED) View.VISIBLE else View.GONE
|
||||||
b.viewChooseMethod.visibility = if (s == Step.CHOOSE) View.VISIBLE else View.GONE
|
b.viewChooseMethod.visibility = if (s == Step.CHOOSE) View.VISIBLE else View.GONE
|
||||||
b.viewPinSetup.visibility = if (s == Step.PIN_ENTER || s == Step.PIN_CONFIRM) View.VISIBLE else View.GONE
|
b.viewPinSetup.visibility = if (s == Step.PIN_ENTER || s == Step.PIN_CONFIRM) View.VISIBLE else View.GONE
|
||||||
b.viewPatternSetup.visibility = if (s == Step.PATTERN_ENTER || s == Step.PATTERN_CONFIRM) View.VISIBLE else View.GONE
|
b.viewPatternSetup.visibility = if (s == Step.PATTERN_ENTER || s == Step.PATTERN_CONFIRM) View.VISIBLE else View.GONE
|
||||||
b.viewBiometric.visibility = if (s == Step.BIOMETRIC) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
when (s) {
|
when (s) {
|
||||||
Step.PIN_ENTER -> {
|
Step.PIN_ENTER -> {
|
||||||
@@ -236,9 +217,6 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
val hash = pbkdf2(input, salt)
|
val hash = pbkdf2(input, salt)
|
||||||
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
|
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
|
||||||
.putString("security_method", method)
|
.putString("security_method", method)
|
||||||
// Remove legacy plaintext fields if they exist from an old install
|
|
||||||
.remove("security_salt")
|
|
||||||
.remove("security_hash")
|
|
||||||
.apply()
|
.apply()
|
||||||
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
|
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
|
||||||
}
|
}
|
||||||
@@ -256,6 +234,7 @@ class SecuritySetupFragment : Fragment() {
|
|||||||
private fun finishSetup() {
|
private fun finishSetup() {
|
||||||
val cb = activity as? Callback
|
val cb = activity as? Callback
|
||||||
if (cb != null) {
|
if (cb != null) {
|
||||||
|
goTo(Step.CONFIGURED)
|
||||||
cb.onSecuritySetupComplete()
|
cb.onSecuritySetupComplete()
|
||||||
} else {
|
} else {
|
||||||
parentFragmentManager.popBackStack()
|
parentFragmentManager.popBackStack()
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ object AccountCache {
|
|||||||
|
|
||||||
private const val PREFS = "account_cache"
|
private const val PREFS = "account_cache"
|
||||||
private const val KEY_MIB = "mib_accounts"
|
private const val KEY_MIB = "mib_accounts"
|
||||||
private const val KEY_BML = "bml_accounts"
|
|
||||||
private const val KEY_FAHIPAY = "fahipay_accounts"
|
private const val KEY_FAHIPAY = "fahipay_accounts"
|
||||||
|
|
||||||
|
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
||||||
|
|
||||||
fun save(context: Context, accounts: List<MibAccount>) {
|
fun save(context: Context, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
@@ -36,7 +37,7 @@ object AccountCache {
|
|||||||
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveBml(context: Context, accounts: List<MibAccount>) {
|
fun saveBml(context: Context, loginId: String, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -56,15 +57,14 @@ object AccountCache {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.edit().putString(KEY_BML, CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBml(context: Context): List<MibAccount> {
|
fun loadBml(context: Context, loginId: String): List<MibAccount> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_BML, null) ?: return emptyList()
|
.getString(bmlKey(loginId), null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val json = CacheEncryption.decrypt(raw)
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
val arr = JSONArray(json)
|
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibAccount(
|
MibAccount(
|
||||||
@@ -84,9 +84,12 @@ object AccountCache {
|
|||||||
internalId = o.optString("internalId", "")
|
internalId = o.optString("internalId", "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
|
||||||
|
loginIds.flatMap { loadBml(context, it) }
|
||||||
|
|
||||||
fun saveFahipay(context: Context, accounts: List<MibAccount>) {
|
fun saveFahipay(context: Context, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ object AccountInputParser {
|
|||||||
UNKNOWN
|
UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip spaces and remove a 960/+960 country code prefix, but only when
|
||||||
|
* the stripped result is exactly 7 digits (so "9603456" is left intact).
|
||||||
|
*/
|
||||||
|
fun normalize(input: String): String {
|
||||||
|
var s = input.replace(" ", "")
|
||||||
|
val stripped = when {
|
||||||
|
s.startsWith("+960") -> s.removePrefix("+960")
|
||||||
|
s.startsWith("960") -> s.removePrefix("960")
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (stripped != null && stripped.matches(Regex("^\\d{7}$"))) s = stripped
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
fun detect(input: String): InputType {
|
fun detect(input: String): InputType {
|
||||||
val s = input.trim()
|
val s = input.trim()
|
||||||
return when {
|
return when {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
object BmlDashboardParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a display-ready product label for a BML dashboard account or card.
|
||||||
|
* Known BML product names are mapped to short friendly labels.
|
||||||
|
* Everything else is title-cased (first letter of each word capitalised).
|
||||||
|
*/
|
||||||
|
fun productLabel(raw: String): String {
|
||||||
|
val u = raw.trim().uppercase()
|
||||||
|
return when {
|
||||||
|
u == "SAVINGS ACCOUNT" -> "Savings"
|
||||||
|
u == "CURRENT ACCOUNT" ||
|
||||||
|
u == "CURRENT ACCOUNT(PERSONAL)" ||
|
||||||
|
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
|
||||||
|
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
|
||||||
|
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
|
||||||
|
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
|
||||||
|
else -> toTitleCase(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toTitleCase(input: String): String =
|
||||||
|
input.trim().lowercase().split(" ").joinToString(" ") { word ->
|
||||||
|
word.replaceFirstChar { it.uppercaseChar() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,7 +84,9 @@ object ContactsCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveBml(context: Context, contacts: List<MibBeneficiary>) {
|
private fun bmlKey(loginId: String) = "bml_contacts_$loginId"
|
||||||
|
|
||||||
|
fun saveBml(context: Context, loginId: String, contacts: List<MibBeneficiary>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (c in contacts) {
|
for (c in contacts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -99,18 +101,18 @@ object ContactsCache {
|
|||||||
put("benefStatus", c.benefStatus)
|
put("benefStatus", c.benefStatus)
|
||||||
put("transferCyDesc", c.transferCyDesc)
|
put("transferCyDesc", c.transferCyDesc)
|
||||||
put("benefCategoryId", c.benefCategoryId)
|
put("benefCategoryId", c.benefCategoryId)
|
||||||
|
put("profileId", c.profileId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.edit().putString("bml_contacts", CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBml(context: Context): List<MibBeneficiary> {
|
fun loadBml(context: Context, loginId: String): List<MibBeneficiary> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString("bml_contacts", null) ?: return emptyList()
|
.getString(bmlKey(loginId), null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val json = CacheEncryption.decrypt(raw)
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
val arr = JSONArray(json)
|
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibBeneficiary(
|
MibBeneficiary(
|
||||||
@@ -125,12 +127,16 @@ object ContactsCache {
|
|||||||
benefStatus = o.optString("benefStatus"),
|
benefStatus = o.optString("benefStatus"),
|
||||||
transferCyDesc = o.optString("transferCyDesc", "MVR"),
|
transferCyDesc = o.optString("transferCyDesc", "MVR"),
|
||||||
customerImgHash = null,
|
customerImgHash = null,
|
||||||
benefCategoryId = o.optString("benefCategoryId", "BML")
|
benefCategoryId = o.optString("benefCategoryId", "BML"),
|
||||||
|
profileId = o.optString("profileId", "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadBml(context: Context, loginIds: List<String>): List<MibBeneficiary> =
|
||||||
|
loginIds.flatMap { loadBml(context, it) }
|
||||||
|
|
||||||
fun saveFahipay(context: Context, contacts: List<MibBeneficiary>, categories: List<MibBeneficiaryCategory>) {
|
fun saveFahipay(context: Context, contacts: List<MibBeneficiary>, categories: List<MibBeneficiaryCategory>) {
|
||||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class CredentialStore(context: Context) {
|
|||||||
// ── MIB login credentials ─────────────────────────────────────────────────
|
// ── MIB login credentials ─────────────────────────────────────────────────
|
||||||
|
|
||||||
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
|
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
|
||||||
fun hasBmlCredentials(): Boolean = prefs.contains("bml_enc_username")
|
|
||||||
fun hasFahipayCredentials(): Boolean = prefs.contains("fahipay_enc_id_card")
|
fun hasFahipayCredentials(): Boolean = prefs.contains("fahipay_enc_id_card")
|
||||||
|
|
||||||
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
|
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
|
||||||
@@ -91,58 +90,84 @@ class CredentialStore(context: Context) {
|
|||||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BML login credentials ─────────────────────────────────────────────────
|
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
|
||||||
|
|
||||||
fun saveBmlCredentials(username: String, password: String, otpSeed: String) {
|
fun getBmlLoginIds(): List<String> {
|
||||||
|
val json = prefs.getString("bml_login_ids", null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = org.json.JSONArray(json)
|
||||||
|
(0 until arr.length()).map { arr.getString(it) }
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasBmlCredentials(): Boolean = getBmlLoginIds().isNotEmpty()
|
||||||
|
|
||||||
|
private fun addBmlLoginId(loginId: String) {
|
||||||
|
val ids = getBmlLoginIds().toMutableList()
|
||||||
|
if (loginId !in ids) {
|
||||||
|
ids.add(loginId)
|
||||||
|
prefs.edit().putString("bml_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeBmlLoginId(loginId: String) {
|
||||||
|
val ids = getBmlLoginIds().toMutableList()
|
||||||
|
if (ids.remove(loginId))
|
||||||
|
prefs.edit().putString("bml_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveBmlCredentials(loginId: String, username: String, password: String, otpSeed: String) {
|
||||||
|
addBmlLoginId(loginId)
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("bml_enc_username", encrypt(username, key))
|
.putString("bml_${loginId}_enc_username", encrypt(username, key))
|
||||||
.putString("bml_enc_password", encrypt(password, key))
|
.putString("bml_${loginId}_enc_password", encrypt(password, key))
|
||||||
.putString("bml_enc_otp_seed", encrypt(otpSeed, key))
|
.putString("bml_${loginId}_enc_otp_seed", encrypt(otpSeed, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBmlCredentials(): BmlCredentials? {
|
fun loadBmlCredentials(loginId: String): BmlCredentials? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encUsername = prefs.getString("bml_enc_username", null) ?: return null
|
val encUsername = prefs.getString("bml_${loginId}_enc_username", null) ?: return null
|
||||||
val encPassword = prefs.getString("bml_enc_password", null) ?: return null
|
val encPassword = prefs.getString("bml_${loginId}_enc_password", null) ?: return null
|
||||||
val encSeed = prefs.getString("bml_enc_otp_seed", null) ?: return null
|
val encSeed = prefs.getString("bml_${loginId}_enc_otp_seed", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
BmlCredentials(decrypt(encUsername, key), decrypt(encPassword, key), decrypt(encSeed, key))
|
BmlCredentials(decrypt(encUsername, key), decrypt(encPassword, key), decrypt(encSeed, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearBmlCredentials() {
|
fun clearBmlCredentials(loginId: String) {
|
||||||
|
removeBmlLoginId(loginId)
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove("bml_enc_username")
|
.remove("bml_${loginId}_enc_username")
|
||||||
.remove("bml_enc_password")
|
.remove("bml_${loginId}_enc_password")
|
||||||
.remove("bml_enc_otp_seed")
|
.remove("bml_${loginId}_enc_otp_seed")
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BML session token ─────────────────────────────────────────────────────
|
// ── BML session token (per loginId) ───────────────────────────────────────
|
||||||
|
|
||||||
fun saveBmlSession(accessToken: String, deviceId: String) {
|
fun saveBmlSession(loginId: String, accessToken: String, deviceId: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("bml_enc_token", encrypt(accessToken, key))
|
.putString("bml_${loginId}_enc_token", encrypt(accessToken, key))
|
||||||
.putString("bml_enc_device_id", encrypt(deviceId, key))
|
.putString("bml_${loginId}_enc_device_id", encrypt(deviceId, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBmlSession(): Pair<String, String>? {
|
fun loadBmlSession(loginId: String): Pair<String, String>? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encToken = prefs.getString("bml_enc_token", null) ?: return null
|
val encToken = prefs.getString("bml_${loginId}_enc_token", null) ?: return null
|
||||||
val encDeviceId = prefs.getString("bml_enc_device_id", null) ?: return null
|
val encDeviceId = prefs.getString("bml_${loginId}_enc_device_id", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
Pair(decrypt(encToken, key), decrypt(encDeviceId, key))
|
Pair(decrypt(encToken, key), decrypt(encDeviceId, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearBmlSession() {
|
fun clearBmlSession(loginId: String) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove("bml_enc_token")
|
.remove("bml_${loginId}_enc_token")
|
||||||
.remove("bml_enc_device_id")
|
.remove("bml_${loginId}_enc_device_id")
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,16 +340,6 @@ class CredentialStore(context: Context) {
|
|||||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveBmlFullName(name: String) {
|
|
||||||
val key = getOrCreateKey()
|
|
||||||
prefs.edit().putString("bml_enc_full_name", encrypt(name, key)).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadBmlFullName(): String? {
|
|
||||||
val key = getOrCreateKey()
|
|
||||||
val enc = prefs.getString("bml_enc_full_name", null) ?: return null
|
|
||||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveMibUserProfile(p: MibUserProfile) {
|
fun saveMibUserProfile(p: MibUserProfile) {
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
@@ -355,7 +370,7 @@ class CredentialStore(context: Context) {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveBmlUserProfile(p: BmlUserProfile) {
|
fun saveBmlUserProfile(loginId: String, p: BmlUserProfile) {
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("fullName", p.fullName)
|
put("fullName", p.fullName)
|
||||||
put("email", p.email)
|
put("email", p.email)
|
||||||
@@ -365,13 +380,12 @@ class CredentialStore(context: Context) {
|
|||||||
put("birthdate", p.birthdate)
|
put("birthdate", p.birthdate)
|
||||||
}.toString()
|
}.toString()
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("bml_enc_profile", encrypt(json, key)).apply()
|
prefs.edit().putString("bml_${loginId}_enc_profile", encrypt(json, key)).apply()
|
||||||
prefs.edit().putString("bml_enc_full_name", encrypt(p.fullName, key)).apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBmlUserProfile(): BmlUserProfile? {
|
fun loadBmlUserProfile(loginId: String): BmlUserProfile? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("bml_enc_profile", null) ?: return null
|
val enc = prefs.getString("bml_${loginId}_enc_profile", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
val o = JSONObject(decrypt(enc, key))
|
val o = JSONObject(decrypt(enc, key))
|
||||||
BmlUserProfile(
|
BmlUserProfile(
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
object MibAccountParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a display-ready product label for a MIB (Faisanet) account type name.
|
||||||
|
* Known MIB accountTypeName values are mapped to short friendly labels.
|
||||||
|
* Everything else is returned trimmed as-is.
|
||||||
|
*/
|
||||||
|
fun productLabel(raw: String): String {
|
||||||
|
val u = raw.trim().uppercase()
|
||||||
|
return when {
|
||||||
|
u == "SAVING ACCOUNT" -> "Savings"
|
||||||
|
u == "CURRENT ACCOUNT" -> "Current"
|
||||||
|
else -> raw.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#016FD0"
|
||||||
|
android:pathData="M16.015 14.378c0-.32-.135-.496-.344-.622-.21-.12-.464-.135-.81-.135h-1.543v2.82h.675v-1.027h.72c.24 0 .39.024.478.125.12.13.104.38.104.55v.35h.66v-.555c-.002-.25-.017-.376-.108-.516-.06-.08-.18-.18-.33-.234l.02-.008c.18-.072.48-.297.48-.747zm-.87.407l-.028-.002c-.09.053-.195.058-.33.058h-.81v-.63h.824c.12 0 .24 0 .33.05.098.048.156.147.15.255 0 .12-.045.215-.134.27zM20.297 15.837H19v.6h1.304c.676 0 1.05-.278 1.05-.884 0-.28-.066-.448-.187-.582-.153-.133-.392-.193-.73-.207l-.376-.015c-.104 0-.18 0-.255-.03-.09-.03-.15-.105-.15-.21 0-.09.017-.166.09-.21.083-.046.177-.066.272-.06h1.23v-.602h-1.35c-.704 0-.958.437-.958.84 0 .9.776.855 1.407.87.104 0 .18.015.225.06.046.03.082.106.082.18 0 .077-.035.15-.08.18-.06.053-.15.07-.277.07zM0 0v10.096L.81 8.22h1.75l.225.464V8.22h2.043l.45 1.02.437-1.013h6.502c.295 0 .56.057.756.236v-.23h1.787v.23c.307-.17.686-.23 1.12-.23h2.606l.24.466v-.466h1.918l.254.465v-.466h1.858v3.948H20.87l-.36-.6v.585h-2.353l-.256-.63h-.583l-.27.614h-1.213c-.48 0-.84-.104-1.08-.24v.24h-2.89v-.884c0-.12-.03-.12-.105-.135h-.105v1.036H6.067v-.48l-.21.48H4.69l-.202-.48v.465H2.235l-.256-.624H1.4l-.256.624H0V24h23.786v-7.108c-.27.135-.613.18-.973.18H21.09v-.255c-.21.165-.57.255-.914.255H14.71v-.9c0-.12-.018-.12-.12-.12h-.075v1.022h-1.8v-1.066c-.298.136-.643.15-.928.136h-.214v.915h-2.18l-.54-.617-.57.6H4.742v-3.93h3.61l.518.602.554-.6h2.412c.28 0 .74.03.942.225v-.24h2.177c.202 0 .644.045.903.225v-.24h3.265v.24c.163-.164.508-.24.803-.24h1.89v.24c.194-.15.464-.24.84-.24h1.176V0H0zM21.156 14.955c.004.005.006.012.01.016.01.01.024.01.032.02l-.042-.035zM23.828 13.082h.065v.555h-.065zM23.865 15.03v-.005c-.03-.025-.046-.048-.075-.07-.15-.153-.39-.215-.764-.225l-.36-.012c-.12 0-.194-.007-.27-.03-.09-.03-.15-.105-.15-.21 0-.09.03-.16.09-.204.076-.045.15-.05.27-.05h1.223v-.588h-1.283c-.69 0-.96.437-.96.84 0 .9.78.855 1.41.87.104 0 .18.015.224.06.046.03.076.106.076.18 0 .07-.034.138-.09.18-.045.056-.136.07-.27.07h-1.288v.605h1.287c.42 0 .734-.118.9-.36h.03c.09-.134.135-.3.135-.523 0-.24-.045-.39-.135-.526zM18.597 14.208v-.583h-2.235V16.458h2.235v-.585h-1.57v-.57h1.533v-.584h-1.532v-.51M13.51 8.787h.685V11.6h-.684zM13.126 9.543l-.007.006c0-.314-.13-.5-.34-.624-.217-.125-.47-.135-.81-.135H10.43v2.82h.674v-1.034h.72c.24 0 .39.03.487.12.122.136.107.378.107.548v.354h.677v-.553c0-.25-.016-.375-.11-.516-.09-.107-.202-.19-.33-.237.172-.07.472-.3.472-.75zm-.855.396h-.015c-.09.054-.195.056-.33.056H11.1v-.623h.825c.12 0 .24.004.33.05.09.04.15.128.15.25s-.047.22-.134.266zM15.92 9.373h.632v-.6h-.644c-.464 0-.804.105-1.02.33-.286.3-.362.69-.362 1.11 0 .512.123.833.36 1.074.232.238.645.31.97.31h.78l.255-.627h1.39l.262.627h1.36v-2.11l1.272 2.11h.95l.002.002V8.786h-.684v1.963l-1.18-1.96h-1.02V11.4L18.11 8.744h-1.004l-.943 2.22h-.3c-.177 0-.362-.03-.468-.134-.125-.15-.186-.36-.186-.662 0-.285.08-.51.194-.63.133-.135.272-.165.516-.165zm1.668-.108l.464 1.118v.002h-.93l.466-1.12zM2.38 10.97l.254.628H4V9.393l.972 2.205h.584l.973-2.202.015 2.202h.69v-2.81H6.118l-.807 1.904-.876-1.905H3.343v2.663L2.205 8.787h-.997L.01 11.597h.72l.26-.626h1.39zm-.688-1.705l.46 1.118-.003.002h-.915l.457-1.12zM11.856 13.62H9.714l-.85.923-.825-.922H5.346v2.82H8l.855-.932.824.93h1.302v-.94h.838c.6 0 1.17-.164 1.17-.945l-.006-.003c0-.78-.598-.93-1.128-.93zM7.67 15.853l-.014-.002H6.02v-.557h1.47v-.574H6.02v-.51H7.7l.733.82-.764.824zm2.642.33l-1.03-1.147 1.03-1.108v2.253zm1.553-1.258h-.885v-.717h.885c.24 0 .42.098.42.344 0 .243-.15.372-.42.372zM9.967 9.373v-.586H7.73V11.6h2.237v-.58H8.4v-.564h1.527V9.88H8.4v-.507" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF5F00"
|
||||||
|
android:pathData="M11.343 18.031c.058.049.12.098.181.146-1.177.783-2.59 1.238-4.107 1.238C3.32 19.416 0 16.096 0 12c0-4.095 3.32-7.416 7.416-7.416 1.518 0 2.931.456 4.105 1.238-.06.051-.12.098-.165.15C9.6 7.489 8.595 9.688 8.595 12c0 2.311 1.001 4.51 2.748 6.031zm5.241-13.447c-1.52 0-2.931.456-4.105 1.238.06.051.12.098.165.15C14.4 7.489 15.405 9.688 15.405 12c0 2.31-1.001 4.507-2.748 6.031-.058.049-.12.098-.181.146 1.177.783 2.588 1.238 4.107 1.238C20.68 19.416 24 16.096 24 12c0-4.094-3.32-7.416-7.416-7.416zM12 6.174c-.096.075-.189.15-.28.231C10.156 7.764 9.169 9.765 9.169 12c0 2.236.987 4.236 2.551 5.595.09.08.185.158.28.232.096-.074.189-.152.28-.232 1.563-1.359 2.551-3.359 2.551-5.595 0-2.235-.987-4.236-2.551-5.595-.09-.08-.184-.156-.28-.231z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A1F71"
|
||||||
|
android:pathData="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-0.094,-0.368,-0.175,-0.503,-0.461,-0.658C1.447 8.864 0.677 8.627 0 8.479l0.046,-0.217h3.3a0.904,0.904,0,0,1,0.894,0.764l0.817 4.338 2.018,-5.102zM17.145 13.311c0.008,-1.979,-2.736,-2.088,-2.717,-2.972 0.006,-0.269 0.262,-0.555 0.822,-0.628a3.66,3.66,0,0,1,1.913,0.336l0.34,-1.59a5.207,5.207,0,0,0,-1.814,-0.333c-1.917 0,-3.266 1.02,-3.278 2.479,-0.012 1.079 0.963 1.68 1.698 2.04 0.756 0.367 1.01 0.603 1.006 0.931,-0.005 0.504,-0.602 0.725,-1.16 0.734,-0.975 0.015,-1.54,-0.263,-1.992,-0.473l-0.351 1.642c0.453 0.208 1.289 0.39 2.156 0.398 2.037 0 3.37,-1.006 3.377,-2.564zM22.206 15.758H24l-1.565,-7.496h-1.656a0.883,0.883,0,0,0,-0.826,0.55l-2.909 6.946h2.036l0.405,-1.12h2.488zM20.043 13.102l1.02,-2.815 0.588 2.815zM11.883 8.262l-1.603 7.496H8.34l1.605,-7.496z" />
|
||||||
|
</vector>
|
||||||
@@ -4,17 +4,20 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/drawerLayout"
|
android:id="@+id/drawerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface">
|
android:background="?attr/colorSurface"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
@@ -49,6 +52,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
app:menu="@menu/bottom_nav_menu" />
|
app:menu="@menu/bottom_nav_menu" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -61,6 +65,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
app:menu="@menu/drawer_menu" />
|
app:menu="@menu/drawer_menu" />
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface">
|
android:background="?attr/colorSurface"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/navHostFragment"
|
android:id="@+id/navHostFragment"
|
||||||
|
|||||||
@@ -27,31 +27,49 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
<LinearLayout
|
||||||
android:id="@+id/languageChipGroup"
|
android:id="@+id/languageSection"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:orientation="vertical"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone">
|
||||||
app:singleSelection="true"
|
|
||||||
app:selectionRequired="false">
|
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<TextView
|
||||||
android:id="@+id/chipEnglish"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="English"
|
android:layout_gravity="center_horizontal"
|
||||||
style="@style/Widget.Material3.Chip.Filter" />
|
android:text="@string/select_language"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
android:id="@+id/chipDhivehi"
|
android:id="@+id/languageToggle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="ދިވެހި"
|
app:singleSelection="true"
|
||||||
style="@style/Widget.Material3.Chip.Filter" />
|
app:selectionRequired="true">
|
||||||
|
|
||||||
</com.google.android.material.chip.ChipGroup>
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnLangEnglish"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="English" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnLangDhivehi"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="ދިވެހި" />
|
||||||
|
|
||||||
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/dotsIndicator"
|
android:id="@+id/dotsIndicator"
|
||||||
@@ -61,6 +79,8 @@
|
|||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginBottom="24dp"
|
android:layout_marginBottom="24dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
app:tabBackground="@drawable/tab_indicator_selector"
|
app:tabBackground="@drawable/tab_indicator_selector"
|
||||||
app:tabGravity="center"
|
app:tabGravity="center"
|
||||||
app:tabIndicatorHeight="0dp"
|
app:tabIndicatorHeight="0dp"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/btnContainer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
android:layout_gravity="bottom|center_horizontal"
|
||||||
|
|||||||
@@ -80,7 +80,6 @@
|
|||||||
android:hint="@string/otp_seed"
|
android:hint="@string/otp_seed"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
app:endIconMode="password_toggle"
|
app:endIconMode="password_toggle"
|
||||||
app:helperText="@string/otp_seed_hint"
|
|
||||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/etOtpSeed"
|
android:id="@+id/etOtpSeed"
|
||||||
@@ -116,7 +115,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:visibility="gone"
|
android:visibility="invisible"
|
||||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||||
app:cardCornerRadius="12dp"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardElevation="0dp">
|
app:cardElevation="0dp">
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:paddingTop="40dp">
|
||||||
|
|
||||||
|
<!-- Appearance -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_appearance"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_navigation"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
|
android:id="@+id/navModeToggle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:singleSelection="true"
|
||||||
|
app:selectionRequired="true">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnNavDrawer"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_nav_drawer" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnNavBottom"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_nav_bottom" />
|
||||||
|
|
||||||
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/theme"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
|
android:id="@+id/themeToggle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:singleSelection="true"
|
||||||
|
app:selectionRequired="true">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnThemeSystem"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/theme_system" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnThemeLight"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/theme_light" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnThemeDark"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/theme_dark" />
|
||||||
|
|
||||||
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
|
<!-- Privacy & Security -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_privacy_security"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Biometrics -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rowBiometrics"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_biometrics"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvBiometricsHint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_biometrics_unavailable"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_biometrics_unlock"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switchBiometrics"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_biometrics_transfer"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switchBiometricsTransfer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Block screenshots -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rowBlockScreenshots"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_block_screenshots"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_block_screenshots_desc"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switchBlockScreenshots"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
@@ -1,94 +1,96 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ScrollView
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fillViewport="true">
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:paddingHorizontal="32dp"
|
||||||
|
android:paddingTop="64dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
<LinearLayout
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:layout_marginBottom="40dp"
|
||||||
|
android:contentDescription="@string/app_name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
||||||
android:gravity="center_horizontal"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:paddingHorizontal="32dp"
|
android:gravity="center"
|
||||||
android:paddingTop="64dp"
|
android:layout_marginBottom="16dp" />
|
||||||
android:paddingBottom="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
<ScrollView
|
||||||
android:id="@+id/icon"
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="120dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="120dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginBottom="40dp"
|
android:layout_weight="1"
|
||||||
android:contentDescription="@string/app_name" />
|
android:fillViewport="true">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
|
||||||
android:textColor="?attr/colorOnSurface"
|
|
||||||
android:gravity="center"
|
|
||||||
android:layout_marginBottom="16dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/description"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
|
||||||
android:textColor="?attr/colorOnSurfaceVariant"
|
|
||||||
android:gravity="center"
|
|
||||||
android:lineSpacingMultiplier="1.4" />
|
|
||||||
|
|
||||||
<!-- Bank logo cards shown only on first slide -->
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/placeholderCards"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="vertical">
|
||||||
android:layout_marginTop="40dp"
|
|
||||||
android:weightSum="2"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:id="@+id/description"
|
||||||
android:layout_height="80dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
app:cardCornerRadius="12dp"
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
app:strokeWidth="1dp"
|
android:gravity="center"
|
||||||
app:strokeColor="?attr/colorOutline">
|
android:lineSpacingMultiplier="1.4" />
|
||||||
|
|
||||||
|
<!-- Supported services shown only on first slide -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/placeholderCards"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:text="@string/onboarding_supported_services"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="40dp"
|
||||||
android:src="@drawable/mib_faisanet_logo"
|
android:src="@drawable/mib_faisanet_logo"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:padding="12dp"
|
|
||||||
android:contentDescription="@string/mib_name" />
|
android:contentDescription="@string/mib_name" />
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="80dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
app:cardCornerRadius="12dp"
|
|
||||||
app:strokeWidth="1dp"
|
|
||||||
app:strokeColor="?attr/colorOutline">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
android:src="@drawable/bml_logo_vector"
|
android:src="@drawable/bml_logo_vector"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:padding="8dp"
|
|
||||||
android:contentDescription="@string/bml_name" />
|
android:contentDescription="@string/bml_name" />
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
<ImageView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:src="@drawable/fahipay_logo_long"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:contentDescription="@string/fahipay_name" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:background="#FFFFFF">
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
<!-- Renderable receipt card -->
|
<!-- Renderable receipt card -->
|
||||||
@@ -206,12 +206,6 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Pushes buttons to bottom of screen -->
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
<!-- Action buttons — outside renderable area -->
|
<!-- Action buttons — outside renderable area -->
|
||||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
@@ -219,7 +213,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:background="#FFFFFF"
|
android:background="?attr/colorSurface"
|
||||||
android:paddingHorizontal="12dp"
|
android:paddingHorizontal="12dp"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingBottom="12dp">
|
android:paddingBottom="12dp">
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
android:paddingBottom="24dp"
|
android:paddingBottom="24dp"
|
||||||
android:gravity="center_horizontal">
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="80dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="80dp"
|
||||||
android:text="🔒"
|
android:src="@drawable/ic_lock"
|
||||||
android:textSize="56sp"
|
android:tint="?attr/colorPrimary"
|
||||||
|
android:contentDescription="@null"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -255,73 +256,49 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Step: Biometric prompt -->
|
|
||||||
|
<!-- Step: Already configured -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/viewBiometric"
|
android:id="@+id/viewConfigured"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
android:paddingHorizontal="24dp"
|
android:paddingHorizontal="24dp"
|
||||||
android:paddingTop="40dp"
|
|
||||||
android:paddingBottom="24dp"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:src="@drawable/ic_check_circle"
|
||||||
|
android:tint="?attr/colorPrimary"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="🔐"
|
android:text="@string/security_already_configured"
|
||||||
android:textSize="72sp"
|
|
||||||
android:layout_marginBottom="16dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/biometric_title"
|
|
||||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||||
android:textColor="?attr/colorOnSurface"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginBottom="12dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/biometric_desc"
|
android:text="@string/security_already_configured_desc"
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
android:textColor="?attr/colorOnSurfaceVariant"
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="32dp"
|
|
||||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
|
||||||
app:cardCornerRadius="12dp"
|
|
||||||
app:cardElevation="0dp">
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="14dp"
|
|
||||||
android:text="@string/biometric_security_note"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnSecondaryContainer"
|
|
||||||
android:gravity="center" />
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnEnableBiometrics"
|
android:id="@+id/btnChangeLock"
|
||||||
android:layout_width="match_parent"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_height="56dp"
|
|
||||||
android:text="@string/enable_biometrics"
|
|
||||||
android:layout_marginBottom="8dp" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnSkipBiometrics"
|
|
||||||
style="@style/Widget.Material3.Button.TextButton"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/skip_biometrics" />
|
android:text="@string/settings_change_lock" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -30,23 +30,69 @@
|
|||||||
android:id="@+id/rowBiometrics"
|
android:id="@+id/rowBiometrics"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="vertical"
|
||||||
android:gravity="center_vertical"
|
android:layout_marginTop="16dp">
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/settings_biometrics"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
|
||||||
android:textColor="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
|
||||||
android:id="@+id/switchBiometrics"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_biometrics"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvBiometricsHint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_biometrics_unavailable"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_biometrics_unlock"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switchBiometrics"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_biometrics_transfer"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switchBiometricsTransfer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,6 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:cardCornerRadius="16dp"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="1dp"
|
|
||||||
app:strokeColor="?attr/colorOutlineVariant">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="20dp">
|
|
||||||
|
|
||||||
<!-- From label + account dropdown -->
|
<!-- From label + account dropdown -->
|
||||||
<TextView
|
<TextView
|
||||||
@@ -355,10 +342,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/transfer" />
|
android:text="@string/transfer" />
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.card.MaterialCardView
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="12dp"
|
android:orientation="vertical">
|
||||||
app:cardCornerRadius="16dp"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="1dp"
|
|
||||||
app:strokeColor="?attr/colorOutlineVariant">
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="20dp"
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingVertical="14dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:foreground="?attr/selectableItemBackground">
|
android:foreground="?attr/selectableItemBackground">
|
||||||
|
|
||||||
@@ -43,7 +39,7 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Right: segmented pill (bank | type | profile) + balance -->
|
<!-- Right: segmented pill (bank | type) + balance -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -58,41 +54,11 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:background="@drawable/pill_segment_bg">
|
android:background="@drawable/pill_segment_bg">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvPillBank"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="12dp"
|
|
||||||
android:paddingEnd="10dp"
|
|
||||||
android:paddingVertical="6dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
|
||||||
android:textColor="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:background="?attr/colorOutline" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvPillType"
|
android:id="@+id/tvPillType"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingHorizontal="10dp"
|
android:paddingHorizontal="12dp"
|
||||||
android:paddingVertical="6dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
|
||||||
android:textColor="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:background="?attr/colorOutline" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvPillProfile"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="10dp"
|
|
||||||
android:paddingEnd="12dp"
|
|
||||||
android:paddingVertical="6dp"
|
android:paddingVertical="6dp"
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
android:textColor="?attr/colorOnSurface" />
|
android:textColor="?attr/colorOnSurface" />
|
||||||
@@ -111,4 +77,10 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:background="?attr/colorOutlineVariant" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:layout_marginTop="16dp">
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -165,6 +166,20 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnHeaderTransfer"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:insetTop="0dp"
|
||||||
|
android:insetBottom="0dp"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:minHeight="0dp"
|
||||||
|
app:icon="@drawable/ic_send"
|
||||||
|
app:iconSize="20dp"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconPadding="0dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,35 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.card.MaterialCardView
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="12dp"
|
android:orientation="vertical">
|
||||||
app:cardCornerRadius="16dp"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="1dp"
|
|
||||||
app:strokeColor="?attr/colorOutlineVariant">
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="20dp"
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingVertical="14dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:foreground="?attr/selectableItemBackground">
|
android:foreground="?attr/selectableItemBackground">
|
||||||
|
|
||||||
<!-- Brand chip -->
|
<!-- Brand logo -->
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/tvCardBrand"
|
android:id="@+id/ivCardBrand"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="48dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="32dp"
|
||||||
android:paddingHorizontal="10dp"
|
|
||||||
android:paddingVertical="6dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
android:scaleType="fitCenter"
|
||||||
android:textColor="@android:color/white"
|
android:contentDescription="Card brand" />
|
||||||
android:fontFamily="monospace"
|
|
||||||
android:background="@drawable/chip_background" />
|
|
||||||
|
|
||||||
<!-- Card name + number -->
|
<!-- Card name + number -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -54,6 +46,14 @@
|
|||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
android:fontFamily="monospace" />
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCardProduct"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Status pill + balance (prepaid only) -->
|
<!-- Status pill + balance (prepaid only) -->
|
||||||
@@ -89,4 +89,10 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:background="?attr/colorOutlineVariant" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|||||||
@@ -5,12 +5,18 @@
|
|||||||
<item android:id="@+id/nav_dashboard"
|
<item android:id="@+id/nav_dashboard"
|
||||||
android:icon="@drawable/ic_nav_dashboard"
|
android:icon="@drawable/ic_nav_dashboard"
|
||||||
android:title="@string/nav_dashboard" />
|
android:title="@string/nav_dashboard" />
|
||||||
<item android:id="@+id/nav_accounts"
|
|
||||||
android:icon="@drawable/ic_nav_accounts"
|
|
||||||
android:title="@string/nav_accounts" />
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group android:id="@+id/group_finance" android:checkableBehavior="single">
|
<group android:id="@+id/group_finance" android:checkableBehavior="single">
|
||||||
|
<item android:id="@+id/nav_accounts"
|
||||||
|
android:icon="@drawable/ic_nav_accounts"
|
||||||
|
android:title="@string/nav_accounts" />
|
||||||
|
<item android:id="@+id/nav_transfer"
|
||||||
|
android:icon="@drawable/ic_send"
|
||||||
|
android:title="@string/transfer" />
|
||||||
|
<item android:id="@+id/nav_pay_mv_qr"
|
||||||
|
android:icon="@drawable/ic_qr_scan"
|
||||||
|
android:title="@string/pay_mv_qr" />
|
||||||
<item android:id="@+id/nav_contacts"
|
<item android:id="@+id/nav_contacts"
|
||||||
android:icon="@drawable/ic_contacts"
|
android:icon="@drawable/ic_contacts"
|
||||||
android:title="@string/nav_contacts" />
|
android:title="@string/nav_contacts" />
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@+id/nav_pay_mv_qr"
|
||||||
|
android:icon="@drawable/ic_qr_scan"
|
||||||
|
android:title="@string/pay_mv_qr" />
|
||||||
<item android:id="@+id/nav_activities"
|
<item android:id="@+id/nav_activities"
|
||||||
android:icon="@drawable/ic_nav_activities"
|
android:icon="@drawable/ic_nav_activities"
|
||||||
android:title="@string/nav_activities" />
|
android:title="@string/nav_activities" />
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
<string name="app_name">BasedBank</string>
|
<string name="app_name">BasedBank</string>
|
||||||
|
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
|
<string name="onboarding_supported_services">ހިދުމަތްތައް</string>
|
||||||
|
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</string>
|
||||||
<string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string>
|
<string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string>
|
||||||
<string name="onboarding_desc_1">BasedBank ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
|
<string name="onboarding_desc_1">BasedBank ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
|
||||||
<string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string>
|
<string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string>
|
||||||
<string name="onboarding_desc_2">އިތުރު ބޭންކްތަކަށް ސަޕޯޓް ލިބޭ ގޮތަށް ތައްޔާރުވަމުން ދަނީ. ދިވެހިރާއްޖޭގެ ބޭންކްތަކަށް ސަޕޯޓް ފަހި ވަމުން ދިޔަ ވަރަކަށް ހިމަނެމުން ދޭ.</string>
|
<string name="onboarding_desc_2">އިތުރު ބޭންކްތަކަށް ސަޕޯޓް ލިބޭ ގޮތަށް ތައްޔާރުވަމުން ދަނީ. ދިވެހިރާއްޖޭގެ ބޭންކްތަކަށް ސަޕޯޓް ފަހި ވަމުން ދިޔަ ވަރަކަށް ހިމަނެމުން ދޭ.</string>
|
||||||
<string name="onboarding_title_3">ފެށޭ ގޮތަށް ތައްޔާރު</string>
|
<string name="onboarding_title_3">ފެށޭ ގޮތަށް ތައްޔާރު</string>
|
||||||
<string name="onboarding_desc_3">ތިޔަ ބޭންކު ކްރެޑެންޝަލް ޖެހި، ތިޔަ އެކައުންޓްތައް ބަލާ. ތިޔަ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.</string>
|
<string name="onboarding_desc_3">ތިޔަ ބޭންކު ކްރެޑެންޝަލް ޖެހި، ތިޔަ އެކައުންޓްތައް ބަލާ. ތިޔަ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.\n\nDhiraagu އާއި Ooredoo ގެ API ބޭނުންކޮށްގެން ފޯން ނަންބަރުގެ ތަފްސީލު ބެލޭ.\n\nމި އެޕް ތިޔައާ ބެހޭ ތަފްސީލެއް ނެހެދޭ، ޑިވެލޮޕަރަށް ވެސް ނުފޮނުވާ. ހުރިހާ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.</string>
|
||||||
<string name="coming_soon">ފަހުން ލިބޭ</string>
|
<string name="coming_soon">ފަހުން ލިބޭ</string>
|
||||||
<string name="next">ދެން</string>
|
<string name="next">ދެން</string>
|
||||||
<string name="get_started">ފަށާ</string>
|
<string name="get_started">ފަށާ</string>
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
<string name="app_name">BasedBank</string>
|
<string name="app_name">BasedBank</string>
|
||||||
|
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
|
<string name="onboarding_supported_services">Supported services</string>
|
||||||
|
<string name="select_language">Select Language</string>
|
||||||
<string name="onboarding_title_1">Your Banks, One App</string>
|
<string name="onboarding_title_1">Your Banks, One App</string>
|
||||||
<string name="onboarding_desc_1">BasedBank brings all your Maldivian bank accounts together in one place. Check balances, view accounts, and more — without switching between apps.</string>
|
<string name="onboarding_desc_1">BasedBank brings all your Maldivian bank accounts together in one place. Check balances, view accounts, and more — without switching between apps.</string>
|
||||||
<string name="onboarding_title_2">More Banks Coming</string>
|
<string name="onboarding_title_2">More Banks Coming</string>
|
||||||
<string name="onboarding_desc_2">Support for additional banks is on the way. Stay tuned as we expand coverage across the Maldives.</string>
|
<string name="onboarding_desc_2">Support for additional banks is on the way. Stay tuned as we expand coverage across the Maldives.</string>
|
||||||
<string name="onboarding_title_3">Get Started</string>
|
<string name="onboarding_title_3">Before You Begin</string>
|
||||||
<string name="onboarding_desc_3">Add your bank credentials and start viewing your accounts. Your data stays on your device.</string>
|
<string name="onboarding_desc_3">BasedBank is an independent, third-party app. It is not affiliated with, endorsed by, or officially supported by any bank or financial institution.\n\nThis app works by logging into your internet banking services directly using your credentials and communicating with bank APIs. It is built using techniques derived from reverse-engineered official bank apps. Behaviour may change or break without notice if banks update their systems.\n\nDhiraagu and Ooredoo APIs are used to look up details about phone numbers.\n\nThis app does not collect any analytics, telemetry, or personal data, and does not transmit any information about you or your usage to the developer. Everything stays entirely on your device.\n\nBy tapping Get Started, you acknowledge and accept that:\n\n• Errors, failures, or service interruptions may occur at any time\n• Your bank may detect third-party access and apply restrictions or take other actions against your account\n• The developer of this app is not liable for any loss, damage, or consequences arising from your use of this app\n• You use this app entirely at your own risk</string>
|
||||||
<string name="coming_soon">Coming Soon</string>
|
<string name="coming_soon">Coming Soon</string>
|
||||||
<string name="next">Next</string>
|
<string name="next">Next</string>
|
||||||
<string name="get_started">Get Started</string>
|
<string name="get_started">Get Started</string>
|
||||||
@@ -49,6 +51,9 @@
|
|||||||
<!-- Security setup -->
|
<!-- Security setup -->
|
||||||
<string name="security_setup">Secure Your App</string>
|
<string name="security_setup">Secure Your App</string>
|
||||||
<string name="security_setup_desc">Choose how you want to lock BasedBank when you\'re away.</string>
|
<string name="security_setup_desc">Choose how you want to lock BasedBank when you\'re away.</string>
|
||||||
|
<string name="security_already_configured">App Lock Configured</string>
|
||||||
|
<string name="security_already_configured_desc">Your app lock is set up.</string>
|
||||||
|
|
||||||
<string name="method_pin">PIN Code</string>
|
<string name="method_pin">PIN Code</string>
|
||||||
<string name="method_pin_desc">4–8 digit numeric PIN</string>
|
<string name="method_pin_desc">4–8 digit numeric PIN</string>
|
||||||
<string name="method_pattern">Draw Pattern</string>
|
<string name="method_pattern">Draw Pattern</string>
|
||||||
@@ -103,7 +108,11 @@
|
|||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="settings_security">Security</string>
|
<string name="settings_security">Security</string>
|
||||||
<string name="settings_change_lock">Change PIN / Pattern</string>
|
<string name="settings_change_lock">Change PIN / Pattern</string>
|
||||||
<string name="settings_biometrics">Use Biometrics</string>
|
<string name="settings_biometrics">Use biometrics</string>
|
||||||
|
<string name="settings_biometrics_unavailable">No biometrics enrolled on this device</string>
|
||||||
|
<string name="settings_biometrics_unlock">To unlock app</string>
|
||||||
|
<string name="settings_biometrics_transfer">Confirm transfer</string>
|
||||||
|
<string name="biometric_transfer_title">Confirm Transfer</string>
|
||||||
<string name="settings_autolock">Auto-lock</string>
|
<string name="settings_autolock">Auto-lock</string>
|
||||||
<string name="autolock_off">Off</string>
|
<string name="autolock_off">Off</string>
|
||||||
<string name="autolock_30s">30s</string>
|
<string name="autolock_30s">30s</string>
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
||||||
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
||||||
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
||||||
<exclude domain="cache" path="."/>
|
<exclude domain="root" path="cache/"/>
|
||||||
</full-backup-content>
|
</full-backup-content>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
||||||
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
||||||
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
||||||
<exclude domain="cache" path="."/>
|
<exclude domain="root" path="cache/"/>
|
||||||
</cloud-backup>
|
</cloud-backup>
|
||||||
<device-transfer>
|
<device-transfer>
|
||||||
<exclude domain="sharedpref" path="credential_store.xml"/>
|
<exclude domain="sharedpref" path="credential_store.xml"/>
|
||||||
@@ -26,6 +26,6 @@
|
|||||||
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
||||||
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
||||||
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
||||||
<exclude domain="cache" path="."/>
|
<exclude domain="root" path="cache/"/>
|
||||||
</device-transfer>
|
</device-transfer>
|
||||||
</data-extraction-rules>
|
</data-extraction-rules>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.1"
|
agp = "8.7.3"
|
||||||
kotlin = "2.1.21"
|
kotlin = "2.1.21"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user