19 Commits
v1.0.6 ... main

Author SHA1 Message Date
e0a554c769 fix useragents to give out actual device model os version and etc
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 6s
2026-05-22 06:53:07 +05:00
94b280a177 version 1.0.7
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
Build and Release APK / build (push) Successful in 4m55s
2026-05-22 06:43:36 +05:00
88c9f153e5 rm temp file 2026-05-22 06:43:11 +05:00
eb7da01b2e auto and lazy load cards to dashbaord
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 06:42:43 +05:00
27270f1b7a auto unlock on correct pin
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
2026-05-22 06:39:59 +05:00
fd7fcb41a6 added transfer support for bml business profiles
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
2026-05-22 06:31:21 +05:00
c9ae614fc7 prep support for transfers for bml business accounts)
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 5s
2026-05-22 06:21:20 +05:00
b784085605 optimize bml refresh flow
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 06:01:13 +05:00
01e5c17284 move refresh indicator to action bar to fix ui shifting
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 05:14:46 +05:00
6d3c7036b5 rebranding
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 05:05:57 +05:00
804712d22d cards on dashboard now
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 04:28:51 +05:00
f208ee6ad1 optimze mib cards loading
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 5s
2026-05-22 03:55:59 +05:00
51dbed94d4 bug fix: paymv qr page emptu space
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 03:40:14 +05:00
0b5a452046 exclude bml loans from dashboard total, transfer from and paymvQR
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 03:22:50 +05:00
00297da71e Revert "fix bug that allowed to skip password setup during inital setup"
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
This reverts commit 1602d061c1.
2026-05-22 03:07:34 +05:00
1602d061c1 fix bug that allowed to skip password setup during inital setup
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 03:01:21 +05:00
ddd64e8624 descriptive menus
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 02:03:20 +05:00
77f367844d rework back butotn 2026-05-22 01:50:12 +05:00
e2729b1d1a add support for fetching mib cards
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 7s
2026-05-22 01:40:14 +05:00
56 changed files with 1898 additions and 369 deletions

View File

@@ -3,11 +3,11 @@
<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-18T20:24:18.550107339Z"> <DropdownSelection timestamp="2026-05-22T00:11:32.873305232Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=683a9830" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>
@@ -15,7 +15,7 @@
<targets> <targets>
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" /> <DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=67d022c2" />
</handle> </handle>
</Target> </Target>
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">

View File

@@ -11,8 +11,8 @@ android {
applicationId = "sh.sar.basedbank" applicationId = "sh.sar.basedbank"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 5 versionCode = 6
versionName = "1.0.6" versionName = "1.0.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

View File

@@ -32,6 +32,8 @@ 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 autoUnlockPin = false
private var pinLength = 4
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)
@@ -61,6 +63,8 @@ class LockActivity : AppCompatActivity() {
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)
autoUnlockPin = prefs.getBoolean("auto_unlock_pin", false)
pinLength = prefs.getInt("pin_length", 4)
val stored = CredentialStore(this).loadSecurityHash() ?: run { finish(); return } val stored = CredentialStore(this).loadSecurityHash() ?: run { finish(); return }
salt = stored.first salt = stored.first
@@ -134,13 +138,18 @@ class LockActivity : AppCompatActivity() {
when (key) { when (key) {
"" -> if (pinDigits.isNotEmpty()) { pinDigits.removeLast(); updateDots() } "" -> if (pinDigits.isNotEmpty()) { pinDigits.removeLast(); updateDots() }
"" -> if (pinDigits.size >= 4) verifyPin() "" -> if (pinDigits.size >= 4) verifyPin()
else -> if (pinDigits.size < 8) { pinDigits.add(key.toInt()); updateDots() } else -> if (pinDigits.size < 8) {
pinDigits.add(key.toInt())
updateDots()
if (autoUnlockPin && pinDigits.size == pinLength) verifyPin()
}
} }
} }
private fun updateDots() { private fun updateDots() {
val n = pinDigits.size val n = pinDigits.size
binding.tvLockPinDots.text = "".repeat(n) + "".repeat(maxOf(4 - n, 0)) val total = if (autoUnlockPin) pinLength else maxOf(n, 4)
binding.tvLockPinDots.text = "".repeat(n) + "".repeat(maxOf(total - n, 0))
} }
private fun verifyPin() { private fun verifyPin() {

View File

@@ -30,6 +30,14 @@ class BmlAccountClient {
return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId) return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId)
} }
/** Lightweight call to verify the session is alive. Throws [AuthExpiredException] on 401/419. */
fun checkProfile(session: BmlSession) {
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/profile")).execute()
val code = resp.code
resp.close()
if (code == 401 || code == 419) throw AuthExpiredException()
}
fun fetchUserInfo(session: BmlSession): BmlUserInfo? { fun fetchUserInfo(session: BmlSession): BmlUserInfo? {
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/userinfo")).execute() val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/userinfo")).execute()
val json = resp.body?.string() ?: return null val json = resp.body?.string() ?: return null
@@ -73,6 +81,27 @@ class BmlAccountClient {
} catch (_: Exception) { null } } catch (_: Exception) { null }
} }
fun fetchTransferChannels(session: BmlSession): List<BmlOtpChannel> {
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/transfer")).execute()
val json = resp.body?.string() ?: run { resp.close(); return emptyList() }
resp.close()
return try {
val root = JSONObject(json)
if (!root.optBoolean("success")) return emptyList()
val arr = root.optJSONObject("payload")
?.optJSONObject("transfer")
?.optJSONArray("otpChannel") ?: return emptyList()
(0 until arr.length()).map { i ->
val ch = arr.getJSONObject(i)
BmlOtpChannel(
channel = ch.optString("channel"),
description = ch.optString("description"),
masked = ch.optString("masked")
)
}
} catch (_: Exception) { emptyList() }
}
private fun parseDashboard( private fun parseDashboard(
json: String, json: String,
loginTag: String, loginTag: String,

View File

@@ -1,11 +1,12 @@
package sh.sar.basedbank.api.bml package sh.sar.basedbank.api.bml
import android.os.Build
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
internal const val BML_BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking" internal const val BML_BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
internal const val BML_USER_AGENT = "bml-mobile-banking/348 (POCO; Android 14; 22101320I)" internal val BML_USER_AGENT = "bml-mobile-banking/348 (${Build.MANUFACTURER}; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
internal const val BML_APP_VERSION = "2.1.44.348" internal const val BML_APP_VERSION = "2.1.44.348"
internal fun newBmlApiClient(): OkHttpClient = OkHttpClient.Builder() internal fun newBmlApiClient(): OkHttpClient = OkHttpClient.Builder()

View File

@@ -25,7 +25,7 @@ class BmlLoginFlow {
private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking" private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
private val CLIENT_ID = "98C83590-513F-4716-B02B-EC68B7D9E7E7" private val CLIENT_ID = "98C83590-513F-4716-B02B-EC68B7D9E7E7"
private val REDIRECT_URI = "https://app.bankofmaldives.com.mv/oauth/mobile-callback" private val REDIRECT_URI = "https://app.bankofmaldives.com.mv/oauth/mobile-callback"
private val APP_USER_AGENT = "bml-mobile-banking/348 (POCO; Android 14; 22101320I)" private val APP_USER_AGENT = "bml-mobile-banking/348 (${android.os.Build.MANUFACTURER}; Android ${android.os.Build.VERSION.RELEASE}; ${android.os.Build.MODEL})"
private val APP_VERSION = "2.1.44.348" private val APP_VERSION = "2.1.44.348"
private val WEB_USER_AGENT = "Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0" private val WEB_USER_AGENT = "Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0"
@@ -310,13 +310,47 @@ class BmlLoginFlow {
val tokenJson = tokenResp.body?.string() ?: throw Exception("Empty token response") val tokenJson = tokenResp.body?.string() ?: throw Exception("Empty token response")
tokenResp.close() tokenResp.close()
val accessToken = JSONObject(tokenJson).optString("access_token") val tokenObj = JSONObject(tokenJson)
val accessToken = tokenObj.optString("access_token")
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed") .takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
val refreshToken = tokenObj.optString("refresh_token", "")
val expiresIn = tokenObj.optLong("expires_in", 0L)
val expiresAt = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000L else 0L
val session = BmlSession(accessToken = accessToken, deviceId = deviceId) val session = BmlSession(accessToken = accessToken, deviceId = deviceId, refreshToken = refreshToken, expiresAt = expiresAt)
val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profileName, profileId) val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profileName, profileId)
return Pair(session, accounts) return Pair(session, accounts)
} }
// ─── Token refresh ───────────────────────────────────────────────────────
/**
* Uses the saved refresh token to obtain a new access token without re-login.
* Returns a new [BmlSession] with updated tokens.
*/
fun refreshSession(session: BmlSession): BmlSession {
val body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("refresh_token", session.refreshToken)
.add("client_id", CLIENT_ID)
.add("Device-ID", session.deviceId)
.add("User-Agent", APP_USER_AGENT)
.add("x-app-version", APP_VERSION)
.build()
val resp = newBmlApiClient().newCall(
Request.Builder().url("$BASE_URL/oauth/token").post(body)
.header("User-Agent", WEB_USER_AGENT).build()
).execute()
val json = resp.body?.string() ?: throw Exception("Empty refresh response")
resp.close()
val obj = JSONObject(json)
val newAccess = obj.optString("access_token").takeIf { it.isNotBlank() }
?: throw Exception("Token refresh failed")
val newRefresh = obj.optString("refresh_token", "").ifBlank { session.refreshToken }
val expiresIn = obj.optLong("expires_in", 0L)
val expiresAt = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000L else 0L
return BmlSession(accessToken = newAccess, deviceId = session.deviceId, refreshToken = newRefresh, expiresAt = expiresAt)
}
// ─── Parsing ────────────────────────────────────────────────────────────── // ─── Parsing ──────────────────────────────────────────────────────────────
/** /**

View File

@@ -4,8 +4,12 @@ import sh.sar.basedbank.api.models.BankAccount
data class BmlSession( data class BmlSession(
val accessToken: String, val accessToken: String,
val deviceId: String val deviceId: String,
) val refreshToken: String = "",
val expiresAt: Long = 0L // Unix millis; 0 = unknown
) {
fun isExpired() = expiresAt > 0L && System.currentTimeMillis() >= expiresAt
}
data class BmlProfile( data class BmlProfile(
val profileId: String, val profileId: String,

View File

@@ -17,7 +17,8 @@ class BmlTransferClient {
amount: Double, amount: Double,
transferType: String, transferType: String,
currency: String, currency: String,
bank: String? = null bank: String? = null,
channel: String = "token"
): Boolean { ): Boolean {
val jo = JSONObject().apply { val jo = JSONObject().apply {
put("debitAccount", debitAccount) put("debitAccount", debitAccount)
@@ -25,7 +26,7 @@ class BmlTransferClient {
put("debitAmount", amount) put("debitAmount", amount)
put("transfertype", transferType) put("transfertype", transferType)
put("currency", currency) put("currency", currency)
put("channel", "token") put("channel", channel)
if (bank != null) put("bank", bank) if (bank != null) put("bank", bank)
} }
val request = Request.Builder() val request = Request.Builder()
@@ -55,7 +56,8 @@ class BmlTransferClient {
currency: String, currency: String,
otp: String, otp: String,
remarks: String = "", remarks: String = "",
bank: String? = null bank: String? = null,
channel: String = "token"
): BmlTransferResult { ): BmlTransferResult {
val jo = JSONObject().apply { val jo = JSONObject().apply {
put("debitAccount", debitAccount) put("debitAccount", debitAccount)
@@ -63,7 +65,7 @@ class BmlTransferClient {
put("debitAmount", amount) put("debitAmount", amount)
put("transfertype", transferType) put("transfertype", transferType)
put("currency", currency) put("currency", currency)
put("channel", "token") put("channel", channel)
put("otp", otp) put("otp", otp)
if (remarks.isNotBlank()) put("remarks", remarks) if (remarks.isNotBlank()) put("remarks", remarks)
if (bank != null) put("bank", bank) if (bank != null) put("bank", bank)

View File

@@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit
class FahipayLoginFlow { class FahipayLoginFlow {
private val BASE_URL = "https://fahipay.mv" private val BASE_URL = "https://fahipay.mv"
private val UA_WEBVIEW = "Mozilla/5.0 (Linux; Android 14; ${Build.MODEL} Build/AP2A.240905.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" private val UA_WEBVIEW = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL} Build/AP2A.240905.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>() private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
private val cookieJar = object : CookieJar { private val cookieJar = object : CookieJar {

View File

@@ -0,0 +1,63 @@
package sh.sar.basedbank.api.mib
import android.os.Build
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class MibCardsClient {
private val BASE_WV_URL = "https://faisamobilex-wv.mib.com.mv"
private val client = OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build()
private fun cookieHeader(session: MibSession) =
"mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " +
"mbnonce=${session.nonceGenerator}; time-tracker=597"
fun fetchCards(session: MibSession, loginTag: String): List<MibCard> {
val body = FormBody.Builder()
.add("name", "")
.add("start", "1")
.add("end", "50")
.add("includeCount", "1")
.build()
val request = Request.Builder()
.url("$BASE_WV_URL/ajaxDebitCard/fetchCardInfos")
.post(body)
.header("Cookie", cookieHeader(session))
.header("User-Agent", "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36")
.header("X-Requested-With", "XMLHttpRequest")
.header("Accept", "*/*")
.header("Origin", BASE_WV_URL)
.header("Referer", "$BASE_WV_URL//debitCards?dashurl=1")
.build()
return client.newCall(request).execute().use { response ->
val bodyStr = response.body?.string() ?: return emptyList()
val json = try { JSONObject(bodyStr) } catch (_: Exception) { return emptyList() }
if (!json.optBoolean("success")) return emptyList()
val data = json.optJSONArray("data") ?: return emptyList()
(0 until data.length()).map { i ->
val item = data.getJSONObject(i)
MibCard(
cardId = item.optString("cardId"),
maskedCardNumber = item.optString("maskedCardNumber"),
cardStatus = item.optString("cardStatus"),
cardType = item.optString("cardType"),
cardTypeDesc = item.optString("cardTypeDesc"),
customerId = item.optString("customerId"),
phoneNumber = item.optString("phoneNumber"),
cardHolderName = item.optString("cardHolderName"),
loginTag = loginTag
)
}
}
}
}

View File

@@ -1,5 +1,6 @@
package sh.sar.basedbank.api.mib package sh.sar.basedbank.api.mib
import android.os.Build
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -24,7 +25,7 @@ class MibContactsClient {
.header("Cookie", cookieHeader(session)) .header("Cookie", cookieHeader(session))
.header( .header(
"User-Agent", "User-Agent",
"Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
) )
.header("X-Requested-With", "XMLHttpRequest") .header("X-Requested-With", "XMLHttpRequest")
.header("Accept", "*/*") .header("Accept", "*/*")

View File

@@ -1,5 +1,6 @@
package sh.sar.basedbank.api.mib package sh.sar.basedbank.api.mib
import android.os.Build
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -27,7 +28,7 @@ class MibFinancingClient {
.header("Cookie", cookieHeader) .header("Cookie", cookieHeader)
.header( .header(
"User-Agent", "User-Agent",
"Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
) )
.header("X-Requested-With", "mv.com.mib.faisamobilex") .header("X-Requested-With", "mv.com.mib.faisamobilex")
.get() .get()

View File

@@ -1,5 +1,6 @@
package sh.sar.basedbank.api.mib package sh.sar.basedbank.api.mib
import android.os.Build
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -50,7 +51,7 @@ class MibHistoryClient {
.header("Cookie", cookieHeader(session)) .header("Cookie", cookieHeader(session))
.header( .header(
"User-Agent", "User-Agent",
"Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
) )
.header("X-Requested-With", "XMLHttpRequest") .header("X-Requested-With", "XMLHttpRequest")
.header("Accept", "*/*") .header("Accept", "*/*")

View File

@@ -46,6 +46,18 @@ data class MibIpsAccountInfo(
) )
data class MibCard(
val cardId: String,
val maskedCardNumber: String,
val cardStatus: String,
val cardType: String,
val cardTypeDesc: String,
val customerId: String,
val phoneNumber: String,
val cardHolderName: String,
val loginTag: String
)
data class MibFinanceDeal( data class MibFinanceDeal(
val dealNo: String, val dealNo: String,
val productDesc: String, val productDesc: String,

View File

@@ -1,5 +1,6 @@
package sh.sar.basedbank.api.mib package sh.sar.basedbank.api.mib
import android.os.Build
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -26,7 +27,7 @@ class MibTransferClient {
.header("Cookie", cookieHeader(session)) .header("Cookie", cookieHeader(session))
.header( .header(
"User-Agent", "User-Agent",
"Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
) )
.header("X-Requested-With", "XMLHttpRequest") .header("X-Requested-With", "XMLHttpRequest")
.header("Accept", "*/*") .header("Accept", "*/*")

View File

@@ -0,0 +1,116 @@
package sh.sar.basedbank.ui.home
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.databinding.FragmentCardSettingsBinding
class CardSettingsFragment : Fragment() {
private var _binding: FragmentCardSettingsBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCardSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = CardSettingsAdapter(emptyList(), requireContext())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val extraBottom = if (isBottomNav) 0 else navBar.bottom
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
insets
}
binding.swipeRefresh.setOnRefreshListener {
(activity as? HomeActivity)?.triggerRefreshCards()
}
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
if (cards == null) return@observe
adapter.update(cards)
binding.loadingView.visibility = View.GONE
binding.swipeRefresh.isRefreshing = false
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
}
if (viewModel.mibCards.value == null) {
binding.loadingView.visibility = View.VISIBLE
(activity as? HomeActivity)?.triggerRefreshCards()
}
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_card_settings)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private inner class CardSettingsAdapter(
private var cards: List<MibCard>,
private val context: Context
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
fun update(newCards: List<MibCard>) {
cards = newCards
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
VH(LayoutInflater.from(context).inflate(R.layout.item_card_settings_entry, parent, false))
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
override fun getItemCount() = cards.size
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
private val btnChangePin: View = view.findViewById(R.id.btnChangePin)
private val btnFreeze: View = view.findViewById(R.id.btnFreeze)
private val btnBlock: View = view.findViewById(R.id.btnBlock)
fun bind(card: MibCard) {
tvCardOwner.text = card.cardHolderName
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
tvCardType.text = card.cardTypeDesc
val assetPath = PayWithCardFragment.cardImageAsset(card)
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
else ivCardImage.setImageDrawable(null)
val wip = View.OnClickListener {
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
btnChangePin.setOnClickListener(wip)
btnFreeze.setOnClickListener(wip)
btnBlock.setOnClickListener(wip)
}
}
}
}

View File

@@ -5,14 +5,21 @@ 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.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlForeignLimit import sh.sar.basedbank.api.bml.BmlForeignLimit
import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.api.mib.MibFinanceDeal
import kotlin.math.abs import kotlin.math.abs
import sh.sar.basedbank.databinding.FragmentDashboardBinding import sh.sar.basedbank.databinding.FragmentDashboardBinding
@@ -45,6 +52,17 @@ class DashboardFragment : Fragment() {
binding.swipeRefresh.isRefreshing = false binding.swipeRefresh.isRefreshing = false
} }
val cardAdapter = DashboardCardAdapter()
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.rvCards.adapter = cardAdapter
LinearSnapHelper().attachToRecyclerView(binding.rvCards)
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
if (cards.isNullOrEmpty()) return@observe
cardAdapter.update(cards)
binding.sectionCards.visibility = View.VISIBLE
}
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt() val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(binding.buttonBar) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.buttonBar) { v, insets ->
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false) val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
@@ -78,7 +96,7 @@ class DashboardFragment : Fragment() {
private fun updateBalances(accounts: List<BankAccount>) { private fun updateBalances(accounts: List<BankAccount>) {
val hide = viewModel.hideAmounts.value ?: false val hide = viewModel.hideAmounts.value ?: false
val nonCreditAccounts = accounts.filter { it.profileType != "BML_CREDIT" } val nonCreditAccounts = accounts.filter { it.profileType != "BML_CREDIT" && it.profileType != "BML_LOAN" }
val creditAccounts = accounts.filter { it.profileType == "BML_CREDIT" } val creditAccounts = accounts.filter { it.profileType == "BML_CREDIT" }
if (hide) { if (hide) {
@@ -209,4 +227,51 @@ class DashboardFragment : Fragment() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
} }
private inner class DashboardCardAdapter : RecyclerView.Adapter<DashboardCardAdapter.VH>() {
private var cards: List<MibCard> = emptyList()
fun update(newCards: List<MibCard>) {
cards = newCards
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_card_dashboard, parent, false)
return VH(view)
}
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
override fun getItemCount() = cards.size
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
fun bind(card: MibCard) {
tvCardOwner.text = card.cardHolderName
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
val assetPath = PayWithCardFragment.cardImageAsset(card)
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
else ivCardImage.setImageDrawable(null)
btnPayQr.setOnClickListener {
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
val nfcAdapter = android.nfc.NfcAdapter.getDefaultAdapter(requireContext())
val nfcSupported = nfcAdapter != null
btnPayNfc.isEnabled = nfcSupported
if (nfcSupported) {
btnPayNfc.setOnClickListener {
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
} else {
btnPayNfc.setOnClickListener(null)
}
}
}
}
} }

View File

@@ -14,8 +14,10 @@ import android.widget.Toast
import sh.sar.basedbank.ui.home.NavCustomization import sh.sar.basedbank.ui.home.NavCustomization
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.GravityCompat
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@@ -37,7 +39,6 @@ import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.AuthExpiredException import sh.sar.basedbank.api.bml.AuthExpiredException
import sh.sar.basedbank.api.bml.BmlAccountClient import sh.sar.basedbank.api.bml.BmlAccountClient
import sh.sar.basedbank.api.bml.BmlActivationResult
import sh.sar.basedbank.api.bml.BmlContactsClient import sh.sar.basedbank.api.bml.BmlContactsClient
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
import sh.sar.basedbank.api.bml.BmlLoanDetail import sh.sar.basedbank.api.bml.BmlLoanDetail
@@ -54,6 +55,7 @@ import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibCardsClient
import sh.sar.basedbank.api.mib.MibFinancingClient import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.api.mib.MibProfile import sh.sar.basedbank.api.mib.MibProfile
import sh.sar.basedbank.api.mib.MibSession import sh.sar.basedbank.api.mib.MibSession
@@ -61,6 +63,7 @@ import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.util.AccountCache import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.ContactsCache import sh.sar.basedbank.util.ContactsCache
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.CardsCache
import sh.sar.basedbank.util.FinancingCache import sh.sar.basedbank.util.FinancingCache
import sh.sar.basedbank.util.ForeignLimitsCache import sh.sar.basedbank.util.ForeignLimitsCache
@@ -71,6 +74,10 @@ class HomeActivity : AppCompatActivity() {
private lateinit var toggle: ActionBarDrawerToggle private lateinit var toggle: ActionBarDrawerToggle
private var suppressBottomNavCallback = false private var suppressBottomNavCallback = false
private var backPressedOnce = false
private val backPressHandler = Handler(Looper.getMainLooper())
private val resetBackPress = Runnable { backPressedOnce = false }
private val autolockHandler = Handler(Looper.getMainLooper()) private val autolockHandler = Handler(Looper.getMainLooper())
private var warningDialog: AlertDialog? = null private var warningDialog: AlertDialog? = null
private var countdownTimer: CountDownTimer? = null private var countdownTimer: CountDownTimer? = null
@@ -138,6 +145,8 @@ class HomeActivity : AppCompatActivity() {
R.id.nav_finances -> FinancingFragment() R.id.nav_finances -> FinancingFragment()
R.id.nav_otp -> OtpFragment() R.id.nav_otp -> OtpFragment()
R.id.nav_settings -> SettingsFragment() R.id.nav_settings -> SettingsFragment()
R.id.nav_pay_with_card -> PayWithCardFragment()
R.id.nav_card_settings -> CardSettingsFragment()
else -> null else -> null
} }
if (frag != null) show(frag) if (frag != null) show(frag)
@@ -169,6 +178,8 @@ class HomeActivity : AppCompatActivity() {
byLoginId.forEach { (loginId, accs) -> AccountCache.saveFahipay(this, loginId, accs) } byLoginId.forEach { (loginId, accs) -> AccountCache.saveFahipay(this, loginId, accs) }
} }
val cachedCards = CardsCache.load(this)
if (cachedCards.isNotEmpty()) viewModel.mibCards.value = cachedCards
val cachedFinancing = FinancingCache.load(this) val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedBmlLoans = FinancingCache.loadBmlLoans(this) val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
@@ -182,6 +193,7 @@ class HomeActivity : AppCompatActivity() {
} }
for ((_, session) in app.bmlSessions) refreshBmlLimits(session) for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
refreshBmlLoanDetails() refreshBmlLoanDetails()
triggerRefreshCards()
} 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 store = CredentialStore(this)
@@ -190,6 +202,8 @@ class HomeActivity : AppCompatActivity() {
val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds()) val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds())
val merged = cachedMib + cachedBml + cachedFahipay val merged = cachedMib + cachedBml + cachedFahipay
if (merged.isNotEmpty()) viewModel.accounts.value = merged if (merged.isNotEmpty()) viewModel.accounts.value = merged
val cachedCards = CardsCache.load(this)
if (cachedCards.isNotEmpty()) viewModel.mibCards.value = cachedCards
val cachedFinancing = FinancingCache.load(this) val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedBmlLoans = FinancingCache.loadBmlLoans(this) val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
@@ -208,6 +222,42 @@ class HomeActivity : AppCompatActivity() {
binding.navigationView.setCheckedItem(R.id.nav_dashboard) binding.navigationView.setCheckedItem(R.id.nav_dashboard)
} }
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Close drawer if open (drawer-nav mode)
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawers()
return
}
// Pop fragment back stack if there's anything on it (e.g. showWithBackStack)
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
return
}
// In bottom nav mode, pressing back navigates up the hierarchy
val isBottomNav = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
if (isBottomNav && binding.bottomNavigation.selectedItemId != R.id.nav_dashboard) {
val currentFrag = supportFragmentManager.findFragmentById(R.id.contentFrame)
// Sub-page reached via More (e.g. Settings, Activities) — go back to More
if (binding.bottomNavigation.selectedItemId == R.id.nav_more && currentFrag !is MoreFragment) {
show(MoreFragment())
return
}
binding.bottomNavigation.selectedItemId = R.id.nav_dashboard
return
}
// At top level — require double-tap to exit
if (backPressedOnce) {
backPressHandler.removeCallbacks(resetBackPress)
finish()
} else {
backPressedOnce = true
Toast.makeText(this@HomeActivity, R.string.press_back_to_exit, Toast.LENGTH_SHORT).show()
backPressHandler.postDelayed(resetBackPress, 2000)
}
}
})
// Keep all MIB sessions alive every 25 seconds while the app is in the foreground // Keep all MIB sessions alive every 25 seconds while the app is in the foreground
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -307,6 +357,8 @@ fun applyNavLabelVisibility() {
R.id.nav_finances -> FinancingFragment() R.id.nav_finances -> FinancingFragment()
R.id.nav_otp -> OtpFragment() R.id.nav_otp -> OtpFragment()
R.id.nav_settings -> SettingsFragment() R.id.nav_settings -> SettingsFragment()
R.id.nav_pay_with_card -> PayWithCardFragment()
R.id.nav_card_settings -> CardSettingsFragment()
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return } else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
} }
show(dest) show(dest)
@@ -521,34 +573,63 @@ fun applyNavLabelVisibility() {
} }
// One async job per BML login, all run in parallel // One async job per BML login, all run in parallel
val bmlJobs = bmlLoginIds.mapNotNull { loginId -> val bmlJobs = bmlLoginIds.map { loginId ->
val creds = store.loadBmlCredentials(loginId) ?: return@mapNotNull null
loginId to async(Dispatchers.IO) { loginId to async(Dispatchers.IO) {
val loginTag = "bml_$loginId" val loginTag = "bml_$loginId"
val app = application as BasedBankApp val app = application as BasedBankApp
val savedProfiles = store.loadBmlProfiles(loginId) val savedProfiles = store.loadBmlProfiles(loginId)
val allAccounts = mutableListOf<BankAccount>() val allAccounts = mutableListOf<BankAccount>()
var anyExpired = savedProfiles.isEmpty()
// Try each saved profile's cached session
for (profile in savedProfiles) {
val saved = store.loadBmlProfileSession(profile.profileId)
if (saved != null) {
try {
val session = BmlSession(saved.first, saved.second)
val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profile.name, profile.profileId)
app.bmlSessions[profile.profileId] = session
allAccounts += accounts
} catch (_: AuthExpiredException) { anyExpired = true
} catch (_: Exception) { anyExpired = true }
} else {
anyExpired = true
}
}
if (savedProfiles.isNotEmpty()) app.bmlProfilesMap[loginId] = savedProfiles if (savedProfiles.isNotEmpty()) app.bmlProfilesMap[loginId] = savedProfiles
// Also try legacy single-profile session token (pre-multi-profile installs) val bmlClient = BmlAccountClient()
for (profile in savedProfiles) {
val saved = store.loadBmlProfileSession(profile.profileId)
val refreshToken = store.loadBmlProfileRefreshToken(profile.profileId)
if (saved == null) {
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
.filter { it.profileId == profile.profileId }
continue
}
val expiresAt = store.loadBmlProfileExpiresAt(profile.profileId)
val tokenKnownExpired = expiresAt > 0L && System.currentTimeMillis() >= expiresAt
suspend fun fetchWithSession(session: BmlSession) {
bmlClient.checkProfile(session)
val accounts = bmlClient.fetchAccounts(session, loginTag, profile.name, profile.profileId)
app.bmlSessions[profile.profileId] = session
allAccounts += accounts
}
suspend fun tryRefresh() {
if (refreshToken == null) throw Exception("No refresh token")
val oldSession = BmlSession(saved.first, saved.second, refreshToken)
val newSession = app.bmlFlowFor(loginId).refreshSession(oldSession)
store.saveBmlProfileSession(profile.profileId, newSession.accessToken, newSession.deviceId)
if (newSession.refreshToken.isNotBlank())
store.saveBmlProfileRefreshToken(profile.profileId, newSession.refreshToken)
if (newSession.expiresAt > 0)
store.saveBmlProfileExpiresAt(profile.profileId, newSession.expiresAt)
fetchWithSession(newSession)
}
try {
if (tokenKnownExpired) {
tryRefresh()
} else {
try {
fetchWithSession(BmlSession(saved.first, saved.second))
} catch (_: AuthExpiredException) {
tryRefresh()
}
}
} catch (_: Exception) {
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
.filter { it.profileId == profile.profileId }
}
}
// Legacy single-profile session (pre-multi-profile installs)
if (savedProfiles.isEmpty()) { if (savedProfiles.isEmpty()) {
val legacyToken = store.loadBmlSession(loginId) val legacyToken = store.loadBmlSession(loginId)
if (legacyToken != null) { if (legacyToken != null) {
@@ -557,47 +638,11 @@ fun applyNavLabelVisibility() {
val accounts = BmlAccountClient().fetchAccounts(session, loginTag) val accounts = BmlAccountClient().fetchAccounts(session, loginTag)
app.bmlSessions[loginId] = session app.bmlSessions[loginId] = session
allAccounts += accounts allAccounts += accounts
anyExpired = false } catch (_: Exception) {
} catch (_: AuthExpiredException) { anyExpired = true
} catch (_: Exception) { anyExpired = true }
}
}
if (anyExpired || allAccounts.isEmpty()) {
// Re-authenticate to refresh personal profile sessions
try {
val flow = app.bmlFlowFor(loginId)
val profiles = flow.login(creds.username, creds.password, creds.otpSeed)
store.saveBmlProfiles(loginId, profiles)
app.bmlProfilesMap[loginId] = profiles
for (profile in profiles) {
if (profile.profileType == "business") {
// Can't activate business profiles without user OTP — use cached
val cached = AccountCache.loadBml(this@HomeActivity, loginId)
.filter { it.profileId == profile.profileId }
if (allAccounts.none { it.profileId == profile.profileId })
allAccounts += cached
continue
}
try {
val result = flow.activateProfile(profile, loginTag)
if (result is BmlActivationResult.Success) {
store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId)
app.bmlSessions[profile.profileId] = result.session
allAccounts.removeAll { it.profileId == profile.profileId }
allAccounts += result.accounts
}
} catch (_: Exception) {
if (allAccounts.none { it.profileId == profile.profileId }) {
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
.filter { it.profileId == profile.profileId }
}
}
}
} catch (_: Exception) {
if (allAccounts.isEmpty())
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId) allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
}
} else {
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
} }
} }
@@ -669,6 +714,10 @@ fun applyNavLabelVisibility() {
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId)) refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
} }
refreshBmlLoanDetails() refreshBmlLoanDetails()
for ((loginId, session) in app.mibSessions) {
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
refreshMibCards(loginId, session, profiles)
}
} }
} }
@@ -932,6 +981,44 @@ fun applyNavLabelVisibility() {
} }
} }
fun triggerRefreshCards() {
val app = application as BasedBankApp
for ((loginId, session) in app.mibSessions) {
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
refreshMibCards(loginId, session, profiles)
}
}
private fun refreshMibCards(loginId: String, session: MibSession, profiles: List<MibProfile>) {
if (profiles.isEmpty()) return
val flow = (application as BasedBankApp).mibFlowFor(loginId)
val client = MibCardsClient()
lifecycleScope.launch {
try {
val cards = withContext(Dispatchers.IO) {
val result = mutableListOf<sh.sar.basedbank.api.mib.MibCard>()
val seen = mutableSetOf<String>()
for (profile in profiles) {
try {
flow.switchProfile(session, profile)
for (card in client.fetchCards(session, "mib_$loginId")) {
if (seen.add(card.cardId)) result += card
}
} catch (_: Exception) { }
}
result
}
if (cards.isNotEmpty()) {
val existing = viewModel.mibCards.value?.toMutableList() ?: mutableListOf()
existing.removeAll { it.loginTag == "mib_$loginId" }
existing += cards
viewModel.mibCards.postValue(existing)
CardsCache.save(this@HomeActivity, existing)
}
} catch (_: Exception) { }
}
}
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) { private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
if (profiles.isEmpty()) return if (profiles.isEmpty()) return
val flow = (application as BasedBankApp).mibFlowFor(loginId) val flow = (application as BasedBankApp).mibFlowFor(loginId)

View File

@@ -7,6 +7,7 @@ import sh.sar.basedbank.api.bml.BmlLoanDetail
import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.models.BankContact import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.api.mib.MibFinanceDeal
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
@@ -20,5 +21,7 @@ class HomeViewModel : ViewModel() {
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>) data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList()) val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
val mibCards = MutableLiveData<List<MibCard>?>(null)
val hideAmounts = MutableLiveData<Boolean>(false) val hideAmounts = MutableLiveData<Boolean>(false)
} }

View File

@@ -25,6 +25,7 @@ class MoreFragment : Fragment() {
val row = inflater.inflate(R.layout.item_more_nav, list, false) val row = inflater.inflate(R.layout.item_more_nav, list, false)
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes) row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes) row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
row.findViewById<TextView>(R.id.tvDescription).setText(item.descriptionRes)
row.setOnClickListener { (requireActivity() as HomeActivity).navigateTo(item.id) } row.setOnClickListener { (requireActivity() as HomeActivity).navigateTo(item.id) }
list.addView(row) list.addView(row)
} }

View File

@@ -10,21 +10,23 @@ object NavCustomization {
data class NavItemDef( data class NavItemDef(
val id: Int, val id: Int,
@DrawableRes val iconRes: Int, @DrawableRes val iconRes: Int,
@StringRes val titleRes: Int @StringRes val titleRes: Int,
@StringRes val descriptionRes: Int
) )
/** All items that can occupy either a bottom nav slot or the "More" screen. */ /** All items that can occupy either a bottom nav slot or the "More" screen. */
val ALL_SWAPPABLE = listOf( val ALL_SWAPPABLE = listOf(
NavItemDef(R.id.nav_accounts, R.drawable.ic_nav_accounts, R.string.nav_accounts), NavItemDef(R.id.nav_accounts, R.drawable.ic_nav_accounts, R.string.nav_accounts, R.string.nav_desc_accounts),
NavItemDef(R.id.nav_contacts, R.drawable.ic_contacts, R.string.nav_contacts), NavItemDef(R.id.nav_contacts, R.drawable.ic_contacts, R.string.nav_contacts, R.string.nav_desc_contacts),
NavItemDef(R.id.nav_transfer, R.drawable.ic_send, R.string.transfer), NavItemDef(R.id.nav_transfer, R.drawable.ic_send, R.string.transfer, R.string.nav_desc_transfer),
NavItemDef(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr), NavItemDef(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr, R.string.nav_desc_pay_mv_qr),
NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities), NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities, R.string.nav_desc_activities),
NavItemDef(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history), NavItemDef(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history, R.string.nav_desc_transfer_history),
NavItemDef(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances), NavItemDef(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances, R.string.nav_desc_finances),
NavItemDef(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings), NavItemDef(R.id.nav_pay_with_card, R.drawable.ic_nav_card, R.string.nav_pay_with_card, R.string.nav_desc_pay_with_card),
NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp), NavItemDef(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings, R.string.nav_desc_card_settings),
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings), NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp, R.string.nav_desc_otp),
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings, R.string.nav_desc_settings),
) )
fun getSlots(prefs: SharedPreferences): List<Int> = listOf( fun getSlots(prefs: SharedPreferences): List<Int> = listOf(

View File

@@ -77,8 +77,10 @@ class PayMvQrFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val basePaddingBottom = view.paddingBottom val basePaddingBottom = view.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val bottomNav = activity?.findViewById<View>(R.id.bottomNavigation)
v.updatePadding(bottom = basePaddingBottom + navBar.bottom) val navBarBottom = if (bottomNav?.visibility == View.VISIBLE) 0
else insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
v.updatePadding(bottom = basePaddingBottom + navBarBottom)
insets insets
} }
setupDropdown() setupDropdown()
@@ -95,7 +97,7 @@ class PayMvQrFragment : Fragment() {
private fun setupDropdown() { private fun setupDropdown() {
viewModel.accounts.observe(viewLifecycleOwner) { accounts -> viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
val eligible = accounts.filter { val eligible = accounts.filter {
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_LOAN"
} }
val adapter = QrAccountAdapter(requireContext(), eligible) val adapter = QrAccountAdapter(requireContext(), eligible)
binding.actvAccount.setAdapter(adapter) binding.actvAccount.setAdapter(adapter)

View File

@@ -0,0 +1,152 @@
package sh.sar.basedbank.ui.home
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding
import sh.sar.basedbank.util.CardsCache
class PayWithCardFragment : Fragment() {
private var _binding: FragmentPayWithCardBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentPayWithCardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = CardWalletAdapter(emptyList(), requireContext())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val extraBottom = if (isBottomNav) 0 else navBar.bottom
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
insets
}
binding.swipeRefresh.setOnRefreshListener {
(activity as? HomeActivity)?.triggerRefreshCards()
}
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
if (cards == null) return@observe
adapter.update(cards)
binding.loadingView.visibility = View.GONE
binding.swipeRefresh.isRefreshing = false
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
}
val cached = CardsCache.load(requireContext())
if (cached.isNotEmpty()) {
viewModel.mibCards.value = cached
} else {
binding.loadingView.visibility = View.VISIBLE
}
(activity as? HomeActivity)?.triggerRefreshCards()
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_pay_with_card)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private inner class CardWalletAdapter(
private var cards: List<MibCard>,
private val context: Context
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
fun update(newCards: List<MibCard>) {
cards = newCards
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
VH(LayoutInflater.from(context).inflate(R.layout.item_card_wallet, parent, false))
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
override fun getItemCount() = cards.size
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
fun bind(card: MibCard) {
tvCardOwner.text = card.cardHolderName
tvCardNumber.text = formatMasked(card.maskedCardNumber)
tvCardType.text = card.cardTypeDesc
val assetPath = cardImageAsset(card)
if (assetPath != null) loadCardImage(ivCardImage, assetPath)
else ivCardImage.setImageDrawable(null)
btnPayQr.setOnClickListener {
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
val nfcAdapter = android.nfc.NfcAdapter.getDefaultAdapter(context)
val nfcSupported = nfcAdapter != null
btnPayNfc.isEnabled = nfcSupported
if (nfcSupported) {
btnPayNfc.setOnClickListener {
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
} else {
btnPayNfc.setOnClickListener(null)
}
}
}
}
companion object {
fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
"53" -> "cards/mib/visa_black_platinum.jpg"
"57" -> "cards/mib/visa_blue_everyday.jpg"
"70" -> "cards/mib/visa_business.jpg"
else -> null
}
fun loadCardImage(imageView: ImageView, assetPath: String) {
try {
val bitmap = imageView.context.assets.open(assetPath).use {
BitmapFactory.decodeStream(it)
}
imageView.setImageBitmap(bitmap)
} catch (_: Exception) {
imageView.setImageDrawable(null)
}
}
fun formatMasked(masked: String): String {
if (masked.length < 4) return masked
return "\u2022\u2022\u2022\u2022 ${masked.takeLast(4)}"
}
}
}

View File

@@ -17,14 +17,15 @@ class SettingsFragment : Fragment() {
private data class SettingsItem( private data class SettingsItem(
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
@StringRes val title: Int, @StringRes val title: Int,
@StringRes val description: Int,
val dest: () -> Fragment val dest: () -> Fragment
) )
private val items = listOf( private val items = listOf(
SettingsItem(R.drawable.ic_contacts, R.string.settings_logins) { SettingsLoginsFragment() }, SettingsItem(R.drawable.ic_contacts, R.string.settings_logins, R.string.settings_desc_logins) { SettingsLoginsFragment() },
SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance) { SettingsAppearanceFragment() }, SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance, R.string.settings_desc_appearance) { SettingsAppearanceFragment() },
SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security) { SettingsSecurityFragment() }, SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security, R.string.settings_desc_privacy_security) { SettingsSecurityFragment() },
SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage) { SettingsStorageFragment() }, SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage, R.string.settings_desc_storage) { SettingsStorageFragment() },
) )
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
@@ -37,6 +38,7 @@ class SettingsFragment : Fragment() {
val row = inflater.inflate(R.layout.item_more_nav, list, false) val row = inflater.inflate(R.layout.item_more_nav, list, false)
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.icon) row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.icon)
row.findViewById<TextView>(R.id.tvLabel).setText(item.title) row.findViewById<TextView>(R.id.tvLabel).setText(item.title)
row.findViewById<TextView>(R.id.tvDescription).setText(item.description)
row.setOnClickListener { row.setOnClickListener {
(requireActivity() as HomeActivity).showWithBackStack(item.dest()) (requireActivity() as HomeActivity).showWithBackStack(item.dest())
} }

View File

@@ -433,6 +433,10 @@ class SettingsLoginsFragment : Fragment() {
return when (activationResult) { return when (activationResult) {
is BmlActivationResult.Success -> { is BmlActivationResult.Success -> {
store.saveBmlProfileSession(profile.profileId, activationResult.session.accessToken, activationResult.session.deviceId) store.saveBmlProfileSession(profile.profileId, activationResult.session.accessToken, activationResult.session.deviceId)
if (activationResult.session.refreshToken.isNotBlank())
store.saveBmlProfileRefreshToken(profile.profileId, activationResult.session.refreshToken)
if (activationResult.session.expiresAt > 0)
store.saveBmlProfileExpiresAt(profile.profileId, activationResult.session.expiresAt)
true true
} }
is BmlActivationResult.NeedsBusinessOtp -> is BmlActivationResult.NeedsBusinessOtp ->
@@ -475,6 +479,10 @@ class SettingsLoginsFragment : Fragment() {
} }
verifyProgress.dismiss() verifyProgress.dismiss()
store.saveBmlProfileSession(profile.profileId, session.accessToken, session.deviceId) store.saveBmlProfileSession(profile.profileId, session.accessToken, session.deviceId)
if (session.refreshToken.isNotBlank())
store.saveBmlProfileRefreshToken(profile.profileId, session.refreshToken)
if (session.expiresAt > 0)
store.saveBmlProfileExpiresAt(profile.profileId, session.expiresAt)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
verifyProgress.dismiss() verifyProgress.dismiss()

View File

@@ -31,6 +31,15 @@ class SettingsSecurityFragment : Fragment() {
) )
} }
// Auto unlock on correct PIN (only for pin method)
if (prefs.getString("security_method", null) == "pin") {
binding.rowAutoUnlockPin.visibility = View.VISIBLE
binding.switchAutoUnlockPin.isChecked = prefs.getBoolean("auto_unlock_pin", false)
binding.switchAutoUnlockPin.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("auto_unlock_pin", isChecked).apply()
}
}
// Biometrics // Biometrics
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

View File

@@ -15,6 +15,8 @@ import android.widget.BaseAdapter
import android.widget.Filter import android.widget.Filter
import android.widget.Filterable import android.widget.Filterable
import android.graphics.Typeface import android.graphics.Typeface
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@@ -36,6 +38,8 @@ import kotlinx.coroutines.launch
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.R
import sh.sar.basedbank.api.bml.BmlAccountClient
import sh.sar.basedbank.api.bml.BmlOtpChannel
import sh.sar.basedbank.api.bml.BmlTransferClient import sh.sar.basedbank.api.bml.BmlTransferClient
import sh.sar.basedbank.api.bml.BmlTransferResult import sh.sar.basedbank.api.bml.BmlTransferResult
import sh.sar.basedbank.api.bml.BmlValidateClient import sh.sar.basedbank.api.bml.BmlValidateClient
@@ -83,6 +87,28 @@ class TransferFragment : Fragment() {
// Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL" // Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL"
private var selectedFahipayService: String? = null private var selectedFahipayService: String? = null
// BML business profile OTP flow state
private enum class BmlOtpState { NONE, SELECTING_CHANNEL, AWAITING_OTP }
private var bmlOtpState = BmlOtpState.NONE
private var bmlOtpChannel: String? = null
private data class PendingBmlTransfer(
val src: BankAccount,
val debitAccount: String,
val creditAccount: String,
val amount: Double,
val amountStr: String,
val remarks: String,
val transferType: String,
val currency: String,
val bank: String?,
val destDisplay: String,
val destAccount: String,
val toBank: String,
val toAvatar: Bitmap?
)
private var pendingBmlTransfer: PendingBmlTransfer? = null
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
@@ -171,7 +197,10 @@ class TransferFragment : Fragment() {
} }
binding.btnTransfer.isEnabled = false binding.btnTransfer.isEnabled = false
binding.btnTransfer.setOnClickListener { initiateTransfer() } binding.btnTransfer.setOnClickListener {
if (bmlOtpState == BmlOtpState.AWAITING_OTP) verifyBmlOtp()
else initiateTransfer()
}
binding.etAmount.addTextChangedListener { updateTransferButton() } binding.etAmount.addTextChangedListener { updateTransferButton() }
@@ -602,6 +631,7 @@ class TransferFragment : Fragment() {
val remarks = binding.etRemarks.text?.toString()?.trim() ?: "" val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
val isSrcBml = src.bank == "BML" val isSrcBml = src.bank == "BML"
val isBmlBusiness = isSrcBml && isBusinessProfile(src) // to test on personal accounts: use `isSrcBml`
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT" 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" }
@@ -636,26 +666,34 @@ class TransferFragment : Fragment() {
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 doTransfer: () -> Unit = { val doTransfer: () -> Unit = {
binding.btnTransfer.isEnabled = false if (isBmlBusiness) {
(activity as? HomeActivity)?.setRefreshing(true) // Business profile: async OTP channel selection flow
viewLifecycleOwner.lifecycleScope.launch { startBmlBusinessOtpFlow(
val (ok, msg, receipt) = withContext(Dispatchers.IO) { src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks,
if (!isSrcBml) { isSrcCard, isDestMib, currency, allAccounts, allContacts, capturedToAvatar
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture) )
} else { } else {
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts) binding.btnTransfer.isEnabled = false
(activity as? HomeActivity)?.setRefreshing(true)
viewLifecycleOwner.lifecycleScope.launch {
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
if (!isSrcBml) {
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
} else {
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
}
}
binding.btnTransfer.isEnabled = true
(activity as? HomeActivity)?.setRefreshing(false)
if (ok && receipt != null) {
ReceiptStore.save(requireContext(), receipt)
clearForm()
val activity = requireActivity() as HomeActivity
activity.triggerRefresh()
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) {
ReceiptStore.save(requireContext(), receipt)
clearForm()
val activity = requireActivity() as HomeActivity
activity.triggerRefresh()
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
} else if (!ok) {
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
} }
} }
} }
@@ -884,12 +922,303 @@ class TransferFragment : Fragment() {
} }
} }
// ── BML business profile OTP flow ─────────────────────────────────────────
private fun isBusinessProfile(account: BankAccount): Boolean {
val app = requireActivity().application as BasedBankApp
val loginId = account.loginTag.removePrefix("bml_")
val profiles = app.bmlProfilesMap[loginId] ?: return false
return profiles.firstOrNull { it.profileId == account.profileId }?.profileType == "business"
}
private fun startBmlBusinessOtpFlow(
src: BankAccount,
destAccount: String,
destDisplay: String,
amount: Double,
amountStr: String,
remarks: String,
isSrcCard: Boolean,
isDestMib: Boolean,
currency: String,
allAccounts: List<BankAccount>,
allContacts: List<BankContact>,
toAvatar: Bitmap?
) {
val debitAccount = src.internalId.ifBlank {
Toast.makeText(requireContext(), getString(R.string.transfer_missing_internal_id), Toast.LENGTH_SHORT).show()
return
}
val isDestMyCard = allAccounts.any {
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount
}
val (transferType, creditAccount, bank) = when {
isSrcCard -> {
val destBml = allAccounts.firstOrNull { it.accountNumber == destAccount && it.profileType == "BML" }
Triple("CAD", destBml?.internalId?.ifBlank { destAccount } ?: destAccount, null as String?)
}
isDestMyCard -> {
val card = allAccounts.first {
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount
}
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
}
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
isDestMib -> {
val contact = allContacts.firstOrNull { it.benefCategoryId == "BML" && it.benefAccount == destAccount }
if (contact == null) {
Toast.makeText(requireContext(), "BML contact not found for this account", Toast.LENGTH_SHORT).show()
return
}
Triple("DOT", contact.benefNo.removePrefix("bml_"), null as String?)
}
else -> Triple("IAT", destAccount, null as String?)
}
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
pendingBmlTransfer = PendingBmlTransfer(
src = src,
debitAccount = debitAccount,
creditAccount = creditAccount,
amount = amount,
amountStr = amountStr,
remarks = remarks,
transferType = transferType,
currency = currency,
bank = bank,
destDisplay = destDisplay,
destAccount = destAccount,
toBank = toBank,
toAvatar = toAvatar
)
bmlOtpState = BmlOtpState.SELECTING_CHANNEL
binding.btnTransfer.isEnabled = false
(activity as? HomeActivity)?.setRefreshing(true)
viewLifecycleOwner.lifecycleScope.launch {
val sess = bmlSessionFor(src)
val channels = if (sess != null) {
withContext(Dispatchers.IO) {
try { BmlAccountClient().fetchTransferChannels(sess) }
catch (_: Exception) { emptyList() }
}
} else emptyList<BmlOtpChannel>()
(activity as? HomeActivity)?.setRefreshing(false)
if (channels.isEmpty()) {
Toast.makeText(requireContext(), "Could not load OTP channels", Toast.LENGTH_SHORT).show()
resetBmlOtpState()
updateTransferButton()
return@launch
}
showBmlChannelSelection(channels)
}
}
private fun showBmlChannelSelection(channels: List<BmlOtpChannel>) {
val ctx = requireContext()
val dp = ctx.resources.displayMetrics.density
binding.containerBmlChannels.removeAllViews()
for (channel in channels) {
val iconRes = when (channel.channel) {
"email" -> R.drawable.ic_channel_email
"mobile" -> R.drawable.ic_channel_sms
else -> R.drawable.ic_channel_sms
}
val iconSize = (24 * dp).toInt()
val textCol = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER_VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
marginStart = (12 * dp).toInt()
}
}
textCol.addView(TextView(ctx).apply {
text = channel.description
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge)
})
textCol.addView(TextView(ctx).apply {
text = channel.masked
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall)
alpha = 0.6f
})
val row = LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
background = ta.getDrawable(0); ta.recycle()
isClickable = true; isFocusable = true
val hp = (16 * dp).toInt(); val vp = (12 * dp).toInt()
setPadding(hp, vp, hp, vp)
}
row.addView(ImageView(ctx).apply { setImageResource(iconRes) },
LinearLayout.LayoutParams(iconSize, iconSize))
row.addView(textCol)
row.setOnClickListener { selectBmlOtpChannel(channel) }
binding.containerBmlChannels.addView(row)
}
disableTransferFields()
binding.layoutBmlChannelSelection.visibility = View.VISIBLE
}
private fun selectBmlOtpChannel(channel: BmlOtpChannel) {
bmlOtpChannel = channel.channel
binding.layoutBmlChannelSelection.visibility = View.GONE
val pending = pendingBmlTransfer ?: return
val sess = bmlSessionFor(pending.src) ?: run {
Toast.makeText(requireContext(), getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
resetBmlOtpState()
updateTransferButton()
return
}
binding.btnTransfer.isEnabled = false
(activity as? HomeActivity)?.setRefreshing(true)
viewLifecycleOwner.lifecycleScope.launch {
val initiated = withContext(Dispatchers.IO) {
try {
BmlTransferClient().initiateTransfer(
sess, pending.debitAccount, pending.creditAccount,
pending.amount, pending.transferType, pending.currency,
pending.bank, channel.channel
)
} catch (_: Exception) { false }
}
(activity as? HomeActivity)?.setRefreshing(false)
if (!initiated) {
Toast.makeText(requireContext(), "Failed to initiate transfer — check your session", Toast.LENGTH_SHORT).show()
resetBmlOtpState()
updateTransferButton()
return@launch
}
bmlOtpState = BmlOtpState.AWAITING_OTP
binding.tvBmlOtpSentVia.text = "OTP code sent via: ${channel.description} (${channel.masked})"
binding.tvBmlOtpSentVia.visibility = View.VISIBLE
binding.tilBmlOtp.visibility = View.VISIBLE
binding.etBmlOtp.requestFocus()
binding.btnTransfer.text = getString(R.string.transfer_verify_payment)
binding.btnTransfer.isEnabled = true
}
}
private fun verifyBmlOtp() {
val otp = binding.etBmlOtp.text?.toString()?.trim() ?: ""
if (otp.isEmpty()) {
binding.tilBmlOtp.error = "Enter the verification code"
return
}
binding.tilBmlOtp.error = null
val pending = pendingBmlTransfer ?: return
val channel = bmlOtpChannel ?: return
val sess = bmlSessionFor(pending.src) ?: run {
Toast.makeText(requireContext(), getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
return
}
binding.btnTransfer.isEnabled = false
(activity as? HomeActivity)?.setRefreshing(true)
val capturedToAvatar = pending.toAvatar
viewLifecycleOwner.lifecycleScope.launch {
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
try {
val result = BmlTransferClient().confirmTransfer(
sess, pending.debitAccount, pending.creditAccount,
pending.amount, pending.transferType, pending.currency,
otp, pending.remarks, pending.bank, channel
)
if (result.success) {
val r = TransferReceiptData(
bank = "BML",
amount = "%.2f".format(pending.amount),
currency = pending.currency,
fromLabel = pending.src.accountBriefName,
fromColorHex = "#0066A1",
toLabel = pending.destDisplay.ifBlank { pending.destAccount },
toAccount = pending.destAccount,
toBank = pending.toBank,
remarks = pending.remarks,
bmlFromName = pending.src.accountBriefName,
bmlReference = result.reference,
bmlTimestamp = result.timestamp,
bmlMessage = result.message
)
Triple(true, "", r)
} else {
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null as TransferReceiptData?)
}
} catch (e: Exception) {
Triple(false, e.message ?: "Transfer failed", null as TransferReceiptData?)
}
}
(activity as? HomeActivity)?.setRefreshing(false)
if (ok && receipt != null) {
ReceiptStore.save(requireContext(), receipt)
resetBmlOtpState()
clearForm()
val activity = requireActivity() as HomeActivity
activity.triggerRefresh()
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
} else {
binding.btnTransfer.isEnabled = true
binding.tilBmlOtp.error = msg
}
}
}
private fun disableTransferFields() {
binding.tilAmount.isEnabled = false
binding.tilRemarks.isEnabled = false
binding.cardFromInfo.alpha = 0.5f
binding.btnClearFromInfo.isEnabled = false
binding.cardToInfo.alpha = 0.5f
binding.btnClearToInfo.isEnabled = false
}
private fun enableTransferFields() {
binding.tilAmount.isEnabled = true
binding.tilRemarks.isEnabled = true
binding.cardFromInfo.alpha = 1f
binding.btnClearFromInfo.isEnabled = true
binding.cardToInfo.alpha = 1f
binding.btnClearToInfo.isEnabled = true
}
private fun resetBmlOtpState() {
bmlOtpState = BmlOtpState.NONE
bmlOtpChannel = null
pendingBmlTransfer = null
val b = _binding ?: return
b.layoutBmlChannelSelection.visibility = View.GONE
b.tvBmlOtpSentVia.visibility = View.GONE
b.tilBmlOtp.visibility = View.GONE
b.etBmlOtp.setText("")
b.tilBmlOtp.error = null
enableTransferFields()
b.btnTransfer.text = getString(R.string.transfer)
}
private fun updateTransferButton() { private fun updateTransferButton() {
if (bmlOtpState != BmlOtpState.NONE) return
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0 val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0 binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
} }
private fun clearForm() { private fun clearForm() {
resetBmlOtpState()
selectedAccount = null selectedAccount = null
binding.actvFrom.setText("", false) binding.actvFrom.setText("", false)
binding.cardFromInfo.visibility = View.GONE binding.cardFromInfo.visibility = View.GONE
@@ -1008,7 +1337,7 @@ 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" && it.profileType != "BML_CREDIT" } val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_LOAN" }
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" } val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
addAll(regular) addAll(regular)
if (cards.isNotEmpty()) { if (cards.isNotEmpty()) {

View File

@@ -267,6 +267,10 @@ class CredentialsFragment : Fragment() {
bmlAccumulatedAccounts += result.accounts bmlAccumulatedAccounts += result.accounts
val store = CredentialStore(requireContext()) val store = CredentialStore(requireContext())
store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId) store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId)
if (result.session.refreshToken.isNotBlank())
store.saveBmlProfileRefreshToken(profile.profileId, result.session.refreshToken)
if (result.session.expiresAt > 0)
store.saveBmlProfileExpiresAt(profile.profileId, result.session.expiresAt)
val app = requireActivity().application as BasedBankApp val app = requireActivity().application as BasedBankApp
app.bmlSessions[profile.profileId] = result.session app.bmlSessions[profile.profileId] = result.session
} }
@@ -326,8 +330,16 @@ class CredentialsFragment : Fragment() {
val session = app.bmlSessions.remove(oldId) val session = app.bmlSessions.remove(oldId)
if (session != null) { if (session != null) {
app.bmlSessions[customerId] = session app.bmlSessions[customerId] = session
val savedRefresh = store.loadBmlProfileRefreshToken(oldId)
val savedExpiry = store.loadBmlProfileExpiresAt(oldId)
store.clearBmlProfileSession(oldId) store.clearBmlProfileSession(oldId)
store.saveBmlProfileSession(customerId, session.accessToken, session.deviceId) store.saveBmlProfileSession(customerId, session.accessToken, session.deviceId)
if (session.refreshToken.isNotBlank())
store.saveBmlProfileRefreshToken(customerId, session.refreshToken)
else if (savedRefresh != null)
store.saveBmlProfileRefreshToken(customerId, savedRefresh)
val expiryToSave = if (session.expiresAt > 0) session.expiresAt else savedExpiry
if (expiryToSave > 0) store.saveBmlProfileExpiresAt(customerId, expiryToSave)
} }
// Update stored profile list with the real ID // Update stored profile list with the real ID
val updatedProfiles = profiles.map { val updatedProfiles = profiles.map {

View File

@@ -11,19 +11,19 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
OnboardingSlide( OnboardingSlide(
titleRes = R.string.onboarding_title_1, titleRes = R.string.onboarding_title_1,
descRes = R.string.onboarding_desc_1, descRes = R.string.onboarding_desc_1,
iconRes = R.drawable.ic_launcher_foreground, iconRes = R.drawable.ic_logo,
isFirst = true isFirst = true
), ),
OnboardingSlide( OnboardingSlide(
titleRes = R.string.onboarding_title_2, titleRes = R.string.onboarding_title_2,
descRes = R.string.onboarding_desc_2, descRes = R.string.onboarding_desc_2,
iconRes = R.drawable.ic_launcher_foreground, iconRes = R.drawable.ic_logo,
isFirst = false isFirst = false
), ),
OnboardingSlide( OnboardingSlide(
titleRes = R.string.onboarding_title_3, titleRes = R.string.onboarding_title_3,
descRes = R.string.onboarding_desc_3, descRes = R.string.onboarding_desc_3,
iconRes = R.drawable.ic_launcher_foreground, iconRes = R.drawable.ic_logo,
isFirst = false, isFirst = false,
isLast = true isLast = true
) )

View File

@@ -215,9 +215,10 @@ class SecuritySetupFragment : Fragment() {
val salt = ByteArray(16).also { SecureRandom().nextBytes(it) } val salt = ByteArray(16).also { SecureRandom().nextBytes(it) }
val saltB64 = Base64.encodeToString(salt, Base64.NO_WRAP) val saltB64 = Base64.encodeToString(salt, Base64.NO_WRAP)
val hash = pbkdf2(input, salt) val hash = pbkdf2(input, salt)
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit() val edit = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
.putString("security_method", method) .putString("security_method", method)
.apply() if (method == "pin") edit.putInt("pin_length", input.length)
edit.apply()
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash) CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
} }

View File

@@ -0,0 +1,57 @@
package sh.sar.basedbank.util
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import sh.sar.basedbank.api.mib.MibCard
object CardsCache {
private const val PREFS = "cards_cache"
private const val KEY_MIB_CARDS = "mib_cards"
fun save(context: Context, cards: List<MibCard>) {
val arr = JSONArray()
for (c in cards) {
arr.put(JSONObject().apply {
put("cardId", c.cardId)
put("maskedCardNumber", c.maskedCardNumber)
put("cardStatus", c.cardStatus)
put("cardType", c.cardType)
put("cardTypeDesc", c.cardTypeDesc)
put("customerId", c.customerId)
put("phoneNumber", c.phoneNumber)
put("cardHolderName", c.cardHolderName)
put("loginTag", c.loginTag)
})
}
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putString(KEY_MIB_CARDS, CacheEncryption.encrypt(arr.toString())).apply()
}
fun load(context: Context): List<MibCard> {
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_MIB_CARDS, null) ?: return emptyList()
return try {
val arr = JSONArray(CacheEncryption.decrypt(raw))
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
MibCard(
cardId = o.optString("cardId"),
maskedCardNumber = o.optString("maskedCardNumber"),
cardStatus = o.optString("cardStatus"),
cardType = o.optString("cardType"),
cardTypeDesc = o.optString("cardTypeDesc"),
customerId = o.optString("customerId"),
phoneNumber = o.optString("phoneNumber"),
cardHolderName = o.optString("cardHolderName"),
loginTag = o.optString("loginTag")
)
}
} catch (_: Exception) { emptyList() }
}
fun clear(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
}
}

View File

@@ -264,10 +264,30 @@ class CredentialStore(context: Context) {
} catch (_: Exception) { null } } catch (_: Exception) { null }
} }
fun saveBmlProfileExpiresAt(profileId: String, expiresAt: Long) {
prefs.edit().putLong("bml_profile_${profileId}_expires_at", expiresAt).apply()
}
fun loadBmlProfileExpiresAt(profileId: String): Long =
prefs.getLong("bml_profile_${profileId}_expires_at", 0L)
fun saveBmlProfileRefreshToken(profileId: String, refreshToken: String) {
val key = getOrCreateKey()
prefs.edit().putString("bml_profile_${profileId}_enc_refresh_token", encrypt(refreshToken, key)).apply()
}
fun loadBmlProfileRefreshToken(profileId: String): String? {
val key = getOrCreateKey()
val enc = prefs.getString("bml_profile_${profileId}_enc_refresh_token", null) ?: return null
return try { decrypt(enc, key) } catch (_: Exception) { null }
}
fun clearBmlProfileSession(profileId: String) { fun clearBmlProfileSession(profileId: String) {
prefs.edit() prefs.edit()
.remove("bml_profile_${profileId}_enc_token") .remove("bml_profile_${profileId}_enc_token")
.remove("bml_profile_${profileId}_enc_device_id") .remove("bml_profile_${profileId}_enc_device_id")
.remove("bml_profile_${profileId}_enc_refresh_token")
.remove("bml_profile_${profileId}_expires_at")
.apply() .apply()
} }

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#00000000"
android:endColor="#CC000000"
android:angle="270"/>
</shape>

View File

@@ -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="?attr/colorOnSurface"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.68L5.68,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.68L18.32,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/>
</vector>

View File

@@ -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="?attr/colorOnSurface"
android:pathData="M22,11h-4.17l3.24,-3.24 -1.41,-1.42L15,11h-2V9l4.66,-4.66 -1.42,-1.41L13,6.17V2h-2v4.17L7.76,2.93 6.34,4.34 11,9v2H9L4.34,6.34 2.93,7.76 6.17,11H2v2h4.17l-3.24,3.24 1.41,1.42L9,13h2v2l-4.66,4.66 1.42,1.41L11,17.83V22h2v-4.17l3.24,3.24 1.42,-1.41L13,15v-2h2l4.66,4.66 1.41,-1.42L17.83,13H22z"/>
</vector>

View File

@@ -5,166 +5,6 @@
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<path <path
android:fillColor="#3DDC84" android:fillColor="#E8B547"
android:pathData="M0,0h108v108h-108z" /> android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector> </vector>

View File

@@ -1,30 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="432"
android:viewportHeight="108"> android:viewportHeight="432">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor"> <!--
<gradient Rufiyaa symbol centered on canvas.
android:endX="85.84757" Original SVG bounding box: 276.85 × 175.60
android:endY="92.4963" Scale 0.85 → 235.3 × 149.3, centered at (216, 216):
android:startX="42.9492" translateX = 216 235.3/2 = 98
android:startY="49.59793" translateY = 216 149.3/2 = 141
android:type="linear"> -->
<item <group
android:color="#44000000" android:scaleX="0.85"
android:offset="0.0" /> android:scaleY="0.85"
<item android:translateX="98"
android:color="#00000000" android:translateY="141">
android:offset="1.0" /> <path
</gradient> android:fillColor="#000000"
</aapt:attr> android:pathData="m 0.01444349,146.48136 c 0.292039,-6.44722 3.89308401,-9.12968 9.18909001,-9.48871 1.2523995,-0.0849 2.7807995,-0.0849 4.8186995,0 7.677319,0.0719 15.575535,2.04677 23.196,0.32719 3.328697,-0.83215 38.545925,-17.3522 71.890297,-40.525298 -6.38484,-3.92558 -9.558207,-9.227296 -9.06617,-16.8414 0.1618,-1.45617 2.26605,-16.30132 20.6841,-14.74007 2.82949,0.2398 11.06017,2.89394 24.6766,7.89527 4.8434,-4.011342 9.86993,-7.792775 14.86,-11.6179 -8.69961,-3.530822 -11.46541,-11.291021 -10.04177,-20.1135 1.42405,-8.825035 8.66169,-13.705029 19.0329,-11.88805 9.23153,1.617223 19.00984,5.215984 22.8004,6.7845 20.70075,-11.189327 54.77683,-28.8770828 65.3541,-34.0729998 5.70626,-3.35254 12.71464,-3.19857098 16.5862,2.73179 4.21557,6.6454798 3.9892,13.2813598 -2.19151,17.9821998 -8.64301,6.573585 -19.44036,9.920412 -29.2398,14.5974 -6.93317,3.490262 -13.93101,6.864024 -20.6089,10.8298 21.25669,6.267982 27.73865,10.456642 28.27166,22.5229 0.0826,1.868798 -0.20263,3.745 -0.64544,5.3661 -1.21907,4.462958 -5.06847,10.963331 -17.1941,9.7415 -5.09154,-0.513098 -25.58805,-6.353594 -42.9433,-12.0531 -4.15228,3.73688 -8.78178,6.907135 -13.2389,10.267 9.60154,3.066049 21.58993,6.56711 24.4216,17.486898 0.24324,1.08936 0.96989,5.15913 -0.58539,9.4564 -2.62403,7.6132 -9.24819,10.55389 -16.7664,10.1919 -3.54102,-0.12568 -18.38167,-3.16212 -42.9144,-12.18824 -26.82393,18.68149 -64.679357,43.44745 -92.643497,57.009 -5.389878,2.61389 -21.064916,10.09114 -30.0955,9.4114 -5.029255,-0.37856 -10.4541355,-5.68673 -13.6177995,-11.67795 -4.32270801,-8.18616 -4.01566901,-16.80012 -3.98877001,-17.39403 z" />
</path> </group>
<path
android:fillColor="#FFFFFF" </vector>
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="#E8B547" />
</shape>
</item>
<item android:drawable="@drawable/ic_launcher_foreground" />
</layer-list>

View File

@@ -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="?attr/colorOnSurface"
android:pathData="M20,2L4,2C2.9,2 2,2.9 2,4l0,16c0,1.1 0.9,2 2,2l16,0c1.1,0 2,-0.9 2,-2L22,4C22,2.9 21.1,2 20,2zM13,18l-2,0 0,-1c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5l-2,0c0,-1.65 -1.35,-3 -3,-3s-3,1.35 -3,3 1.35,3 3,3l0,-2 2,3zM19,12l-2,0c0,-2.76 -2.24,-5 -5,-5l0,-2C15.87,5 19,8.13 19,12z"/>
</vector>

View File

@@ -19,19 +19,26 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar <FrameLayout
android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="wrap_content">
app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/refreshIndicator" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="?attr/actionBarSize"
android:indeterminate="true" app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
android:visibility="gone"
app:trackCornerRadius="0dp" /> <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/refreshIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:indeterminate="true"
android:visibility="gone"
app:trackCornerRadius="0dp" />
</FrameLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false"/>
<LinearLayout
android:id="@+id/loadingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/cards_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone"/>
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -216,41 +216,31 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" /> android:orientation="vertical" />
<!-- Card support WIP --> <!-- Card carousel (hidden when no cards loaded) -->
<com.google.android.material.card.MaterialCardView <LinearLayout
android:id="@+id/sectionCards"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
app:cardElevation="1dp" android:visibility="gone">
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutlineVariant">
<LinearLayout <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_pay_with_card"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCards"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:clipToPadding="false"
android:gravity="center" android:paddingEnd="4dp"/>
android:padding="24dp">
<TextView </LinearLayout>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/card_support_wip"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/coming_soon"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOutline" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false"/>
<LinearLayout
android:id="@+id/loadingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/cards_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone"/>
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -96,6 +96,46 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/rowAutoUnlockPin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:visibility="gone">
<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_auto_unlock_pin"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_auto_unlock_pin_desc"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchAutoUnlockPin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp" />
</LinearLayout>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -336,6 +336,61 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<!-- BML business OTP: channel selection (shown after confirmation, before OTP entry) -->
<LinearLayout
android:id="@+id/layoutBmlChannelSelection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/transfer_send_otp_via"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
<LinearLayout
android:id="@+id/containerBmlChannels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
<!-- BML business OTP: sent-via label (shown after channel selection) -->
<TextView
android:id="@+id/tvBmlOtpSentVia"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
<!-- BML business OTP: verification code input (shown after channel selection) -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBmlOtp"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/transfer_otp_code_hint"
android:layout_marginBottom="16dp"
android:visibility="gone">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etBmlOtp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLines="1"
android:maxLength="6" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnTransfer" android:id="@+id/btnTransfer"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivCardImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:contentDescription="@string/nav_card_settings"/>
<View
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_gravity="bottom"
android:background="@drawable/bg_card_overlay_gradient"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tvCardOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="3"/>
<TextView
android:id="@+id/tvCardNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="#CCFFFFFF"
android:fontFamily="monospace"/>
</LinearLayout>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:paddingTop="8dp"
android:paddingBottom="10dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPayQr"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_pay_qr"
android:textSize="11sp"
app:icon="@drawable/ic_qr_scan"
app:iconSize="16dp"
app:iconPadding="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPayNfc"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_pay_nfc"
android:textSize="11sp"
app:icon="@drawable/ic_nfc"
app:iconSize="16dp"
app:iconPadding="4dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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="wrap_content"
android:layout_marginBottom="20dp"
app:cardCornerRadius="20dp"
app:cardElevation="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivCardImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:contentDescription="@string/nav_card_settings"/>
<!-- Bottom gradient for text legibility -->
<View
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_gravity="bottom"
android:background="@drawable/bg_card_overlay_gradient"/>
<!-- Bottom-left: card owner name + masked number -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvCardOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="3"/>
<TextView
android:id="@+id/tvCardNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="#CCFFFFFF"
android:fontFamily="monospace"/>
</LinearLayout>
</FrameLayout>
<!-- Card type label -->
<TextView
android:id="@+id/tvCardType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="10dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"/>
<!-- Action buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="12dp"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnChangePin"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_change_pin"
android:textSize="11sp"
app:icon="@drawable/ic_edit"
app:iconSize="16dp"
app:iconPadding="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnFreeze"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_freeze"
android:textSize="11sp"
app:icon="@drawable/ic_freeze"
app:iconSize="16dp"
app:iconPadding="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnBlock"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_block"
android:textSize="11sp"
app:icon="@drawable/ic_block"
app:iconSize="16dp"
app:iconPadding="4dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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="wrap_content"
android:layout_marginBottom="20dp"
app:cardCornerRadius="20dp"
app:cardElevation="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivCardImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:contentDescription="@string/nav_card_settings"/>
<!-- Bottom gradient for text legibility -->
<View
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_gravity="bottom"
android:background="@drawable/bg_card_overlay_gradient"/>
<!-- Bottom-left: card owner name + masked number -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvCardOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="3"/>
<TextView
android:id="@+id/tvCardNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="#CCFFFFFF"
android:fontFamily="monospace"/>
</LinearLayout>
</FrameLayout>
<!-- Card type label -->
<TextView
android:id="@+id/tvCardType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="10dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"/>
<!-- Action buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="12dp"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPayQr"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_pay_qr"
android:textSize="11sp"
app:icon="@drawable/ic_qr_scan"
app:iconSize="16dp"
app:iconPadding="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPayNfc"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_pay_nfc"
android:textSize="11sp"
app:icon="@drawable/ic_nfc"
app:iconSize="16dp"
app:iconPadding="4dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -15,13 +15,26 @@
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginEnd="16dp" /> android:layout_marginEnd="16dp" />
<TextView <LinearLayout
android:id="@+id/tvLabel"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyLarge" android:orientation="vertical">
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -29,6 +29,9 @@
<item android:id="@+id/nav_finances" <item android:id="@+id/nav_finances"
android:icon="@drawable/ic_nav_finances" android:icon="@drawable/ic_nav_finances"
android:title="@string/nav_finances" /> android:title="@string/nav_finances" />
<item android:id="@+id/nav_pay_with_card"
android:icon="@drawable/ic_nav_card"
android:title="@string/nav_pay_with_card" />
<item android:id="@+id/nav_card_settings" <item android:id="@+id/nav_card_settings"
android:icon="@drawable/ic_nav_card" android:icon="@drawable/ic_nav_card"
android:title="@string/nav_card_settings" /> android:title="@string/nav_card_settings" />

View File

@@ -12,6 +12,9 @@
<item android:id="@+id/nav_finances" <item android:id="@+id/nav_finances"
android:icon="@drawable/ic_nav_finances" android:icon="@drawable/ic_nav_finances"
android:title="@string/nav_finances" /> android:title="@string/nav_finances" />
<item android:id="@+id/nav_pay_with_card"
android:icon="@drawable/ic_nav_card"
android:title="@string/nav_pay_with_card" />
<item android:id="@+id/nav_card_settings" <item android:id="@+id/nav_card_settings"
android:icon="@drawable/ic_nav_card" android:icon="@drawable/ic_nav_card"
android:title="@string/nav_card_settings" /> android:title="@string/nav_card_settings" />

View File

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">BasedBank</string> <string name="app_name">ތިޖޫރީ</string>
<!-- Onboarding --> <!-- Onboarding -->
<string name="onboarding_supported_services">ހިދުމަތްތައް</string> <string name="onboarding_supported_services">ހިދުމަތްތައް</string>
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</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">ތިޖޫރީ ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</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>
@@ -31,7 +31,7 @@
<string name="login">ލޮގިން</string> <string name="login">ލޮގިން</string>
<!-- Lock screen --> <!-- Lock screen -->
<string name="unlock_app">BasedBank ހުޅުވާ</string> <string name="unlock_app">ތިޖޫރީ ހުޅުވާ</string>
<string name="unlock_pin_subtitle">PIN ޖަހާ</string> <string name="unlock_pin_subtitle">PIN ޖަހާ</string>
<string name="unlock_pattern_subtitle">ހުޅުވާ ޕެޓަން ކަހާ</string> <string name="unlock_pattern_subtitle">ހުޅުވާ ޕެޓަން ކަހާ</string>
<string name="use_biometrics">ބަޔޮމެޓްރިކް ބޭނުން ކުރޭ</string> <string name="use_biometrics">ބަޔޮމެޓްރިކް ބޭނުން ކުރޭ</string>
@@ -43,7 +43,7 @@
<!-- Security setup --> <!-- Security setup -->
<string name="security_setup">އެޕް ރައްކާތެރި ކުރޭ</string> <string name="security_setup">އެޕް ރައްކާތެރި ކުރޭ</string>
<string name="security_setup_desc">BasedBank ހުޅުވަން ބޭނުންވާ ގޮތެއް ހިޔާރު ކުރޭ.</string> <string name="security_setup_desc">ތިޖޫރީ ހުޅުވަން ބޭނުންވާ ގޮތެއް ހިޔާރު ކުރޭ.</string>
<string name="method_pin">PIN ކޯޑް</string> <string name="method_pin">PIN ކޯޑް</string>
<string name="method_pin_desc">48 ރިޔަލެއްގެ ނަންބަރު PIN</string> <string name="method_pin_desc">48 ރިޔަލެއްގެ ނަންބަރު PIN</string>
<string name="method_pattern">ޕެޓަން ކަހާ</string> <string name="method_pattern">ޕެޓަން ކަހާ</string>
@@ -74,6 +74,17 @@
<string name="nav_finances">ފައިނޭންސް</string> <string name="nav_finances">ފައިނޭންސް</string>
<string name="nav_card_settings">ކާޑް ސެޓިންގ</string> <string name="nav_card_settings">ކާޑް ސެޓިންގ</string>
<string name="nav_settings">ސެޓިންގ</string> <string name="nav_settings">ސެޓިންގ</string>
<string name="nav_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string>
<string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string>
<string name="nav_desc_transfer">ކޮންޓެކްޓަކަށް ފައިސާ ފޮނުވާ</string>
<string name="nav_desc_pay_mv_qr">PayMV QR ކޯޑް ސްކޭން ނުވަތަ ތައްޔާރު ކުރޭ</string>
<string name="nav_desc_activities">ފަހުގެ ޓްރާންސްފަތައް ބަލާ</string>
<string name="nav_desc_transfer_history">އެކައުންޓް ތަކުގެ ޓްރާންސެކްޝަން ތާރީހް</string>
<string name="nav_desc_finances">ލޯން އަދި ފައިނޭންސިންގ</string>
<string name="nav_desc_pay_with_card">ކާޑް ބޭނުންކޮށް ފައިސާ ދައްކާ</string>
<string name="nav_desc_card_settings">ކާޑް ސެޓިންގ މެނޭޖް ކުރޭ</string>
<string name="nav_desc_otp">OTP ކޯޑް ތައްޔާރު ކުރޭ</string>
<string name="nav_desc_settings">އެޕްލިކޭޝަންގެ ތަރުތީބު</string>
<string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string> <string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string>
<string name="nav_close_drawer">ނެވިގޭޝަން ލައްޕާ</string> <string name="nav_close_drawer">ނެވިގޭޝަން ލައްޕާ</string>
<string name="work_in_progress">ތައްޔާރުވަމުން ދަނީ</string> <string name="work_in_progress">ތައްޔާރުވަމުން ދަނީ</string>
@@ -100,12 +111,18 @@
<string name="lang_english">English</string> <string name="lang_english">English</string>
<string name="lang_dhivehi">ދިވެހި</string> <string name="lang_dhivehi">ދިވެހި</string>
<string name="settings_privacy">ޕްރައިވެސީ</string> <string name="settings_privacy">ޕްރައިވެސީ</string>
<string name="settings_auto_unlock_pin">ރަނގަޅު ޕިން އެޅުމުން ހުޅުވޭ</string>
<string name="settings_auto_unlock_pin_desc">ޕިންގެ ދިގުމިނާ އެއްވަރަށް ޑިޖިޓް ލިޔުމުން ހުޅުވިދާ</string>
<string name="settings_block_screenshots">ސްކްރީންޝޮޓް ބްލޮކްކުރޭ</string> <string name="settings_block_screenshots">ސްކްރީންޝޮޓް ބްލޮކްކުރޭ</string>
<string name="settings_block_screenshots_desc">ރިސެންޓްސް ސްކްރީނުންނާއި ސްކްރީން ކެޕްޗާ ހުއްޓުވައިދޭ</string> <string name="settings_block_screenshots_desc">ރިސެންޓްސް ސްކްރީނުންނާއި ސްކްރީން ކެޕްޗާ ހުއްޓުވައިދޭ</string>
<string name="settings_cache">ކޭޝް</string> <string name="settings_cache">ކޭޝް</string>
<string name="settings_clear_cache">ކޭޝް ސާފުކުރޭ</string> <string name="settings_clear_cache">ކޭޝް ސާފުކުރޭ</string>
<string name="settings_cache_cleared">ކޭޝް ސާފުކުރެވިއްޖެ</string> <string name="settings_cache_cleared">ކޭޝް ސާފުކުރެވިއްޖެ</string>
<string name="settings_logins">ލޮގިންތައް</string> <string name="settings_logins">ލޮގިންތައް</string>
<string name="settings_desc_logins">ބޭންކް ލޮގިންތައް މެނޭޖް ކުރޭ</string>
<string name="settings_desc_appearance">ތީމް، ބަސް، އަދި ދައްކުވާ ގޮތް</string>
<string name="settings_desc_privacy_security">އެޕް ލޮކް، ޕިން، އަދި ސަލާމަތީ ސެޓިންގ</string>
<string name="settings_desc_storage">ކޭޝް ޑޭޓާ އަދި ސްޓޯރޭޖް</string>
<string name="settings_logout">ލޮގްއައުޓް</string> <string name="settings_logout">ލޮގްއައުޓް</string>
<string name="settings_logout_confirm_title">%s އިން ލޮގްއައުޓް ވަންތަ؟</string> <string name="settings_logout_confirm_title">%s އިން ލޮގްއައުޓް ވަންތަ؟</string>
<string name="settings_logout_confirm_message">ހުރިހާ ކޭޝް ޑޭޓާ ސާފުވެ، ބާކީ ހުރި އެކައުންޓްތައް އަލުން ލޯޑްވާނެ.</string> <string name="settings_logout_confirm_message">ހުރިހާ ކޭޝް ޑޭޓާ ސާފުވެ، ބާކީ ހުރި އެކައުންޓްތައް އަލުން ލޯޑްވާނެ.</string>

View File

@@ -1,15 +1,15 @@
<resources> <resources>
<string name="app_name">BasedBank</string> <string name="app_name">Thijooree</string>
<!-- Onboarding --> <!-- Onboarding -->
<string name="onboarding_supported_services">Supported services</string> <string name="onboarding_supported_services">Supported services</string>
<string name="select_language">Select Language</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">Thijooree 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">Before You Begin</string> <string name="onboarding_title_3">Before You Begin</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="onboarding_desc_3">Thijooree 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>
@@ -38,7 +38,7 @@
<string name="login">Login</string> <string name="login">Login</string>
<!-- Lock screen --> <!-- Lock screen -->
<string name="unlock_app">Unlock BasedBank</string> <string name="unlock_app">Unlock Thijooree</string>
<string name="unlock_pin_subtitle">Enter your PIN</string> <string name="unlock_pin_subtitle">Enter your PIN</string>
<string name="unlock_pattern_subtitle">Draw your unlock pattern</string> <string name="unlock_pattern_subtitle">Draw your unlock pattern</string>
<string name="use_biometrics">Use Biometrics</string> <string name="use_biometrics">Use Biometrics</string>
@@ -50,7 +50,7 @@
<!-- 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 Thijooree when you\'re away.</string>
<string name="security_already_configured">App Lock Configured</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="security_already_configured_desc">Your app lock is set up.</string>
@@ -86,9 +86,21 @@
<string name="nav_otp">OTP Codes</string> <string name="nav_otp">OTP Codes</string>
<string name="nav_settings">Settings</string> <string name="nav_settings">Settings</string>
<string name="nav_more">More</string> <string name="nav_more">More</string>
<string name="nav_desc_accounts">View all your bank accounts</string>
<string name="nav_desc_contacts">Manage your transfer contacts</string>
<string name="nav_desc_transfer">Send money to a contact</string>
<string name="nav_desc_pay_mv_qr">Scan or generate a PayMV QR code</string>
<string name="nav_desc_activities">View your recent transfers</string>
<string name="nav_desc_transfer_history">Full transaction history by account</string>
<string name="nav_desc_finances">Loans and financing overview</string>
<string name="nav_desc_pay_with_card">Make a payment using your card</string>
<string name="nav_desc_card_settings">Manage your card preferences</string>
<string name="nav_desc_otp">Generate OTP codes for authentication</string>
<string name="nav_desc_settings">App preferences and configuration</string>
<string name="nav_open_drawer">Open navigation</string> <string name="nav_open_drawer">Open navigation</string>
<string name="nav_close_drawer">Close navigation</string> <string name="nav_close_drawer">Close navigation</string>
<string name="work_in_progress">Work in progress</string> <string name="work_in_progress">Work in progress</string>
<string name="press_back_to_exit">Press back again to exit</string>
<!-- Dashboard --> <!-- Dashboard -->
<string name="dashboard_pending_finances">Pending Finances</string> <string name="dashboard_pending_finances">Pending Finances</string>
@@ -143,6 +155,8 @@
<string name="settings_privacy">Privacy</string> <string name="settings_privacy">Privacy</string>
<string name="settings_hide_amounts">Hide sensitive information</string> <string name="settings_hide_amounts">Hide sensitive information</string>
<string name="settings_hide_amounts_desc">Masks account balances and financial figures across the app</string> <string name="settings_hide_amounts_desc">Masks account balances and financial figures across the app</string>
<string name="settings_auto_unlock_pin">Auto unlock on correct PIN</string>
<string name="settings_auto_unlock_pin_desc">Unlock automatically when the entered digits match your PIN length</string>
<string name="settings_block_screenshots">Block Screenshots</string> <string name="settings_block_screenshots">Block Screenshots</string>
<string name="settings_block_screenshots_desc">Prevents the app from appearing in the recents screen and blocks screen capture</string> <string name="settings_block_screenshots_desc">Prevents the app from appearing in the recents screen and blocks screen capture</string>
<string name="settings_cache">Cache</string> <string name="settings_cache">Cache</string>
@@ -161,6 +175,10 @@
<string name="settings_privacy_security">Privacy &amp; Security</string> <string name="settings_privacy_security">Privacy &amp; Security</string>
<string name="settings_storage">Storage</string> <string name="settings_storage">Storage</string>
<string name="settings_logins">Logins</string> <string name="settings_logins">Logins</string>
<string name="settings_desc_logins">Manage your bank account logins</string>
<string name="settings_desc_appearance">Theme, language, and display options</string>
<string name="settings_desc_privacy_security">App lock, PIN, and security preferences</string>
<string name="settings_desc_storage">Manage cached data and storage usage</string>
<string name="settings_logout">Log out</string> <string name="settings_logout">Log out</string>
<string name="settings_logout_confirm_title">Log out of %s?</string> <string name="settings_logout_confirm_title">Log out of %s?</string>
<string name="settings_logout_confirm_message">All cached data will be cleared and remaining accounts will be refreshed.</string> <string name="settings_logout_confirm_message">All cached data will be cleared and remaining accounts will be refreshed.</string>
@@ -216,6 +234,9 @@
<string name="transfer_bml_contact_required_title">Contact Required</string> <string name="transfer_bml_contact_required_title">Contact Required</string>
<string name="transfer_bml_contact_required_msg">To send USD to a MIB account from BML, the recipient must be saved as a BML contact first. This is required by BML\'s API.\n\nPlease add this account as a BML contact, then try again.</string> <string name="transfer_bml_contact_required_msg">To send USD to a MIB account from BML, the recipient must be saved as a BML contact first. This is required by BML\'s API.\n\nPlease add this account as a BML contact, then try again.</string>
<string name="transfer_missing_internal_id">Account data is incomplete — please re-login to refresh.</string> <string name="transfer_missing_internal_id">Account data is incomplete — please re-login to refresh.</string>
<string name="transfer_verify_payment">Verify Payment</string>
<string name="transfer_send_otp_via">Send verification code via</string>
<string name="transfer_otp_code_hint">Verification code</string>
<!-- Contacts --> <!-- Contacts -->
<string name="contacts_empty">No contacts found</string> <string name="contacts_empty">No contacts found</string>
@@ -272,4 +293,13 @@
<string name="loan_end_date">End Date</string> <string name="loan_end_date">End Date</string>
<string name="loan_overdue_payments">Overdue Payments</string> <string name="loan_overdue_payments">Overdue Payments</string>
<string name="loan_rate_fmt">%.2f%%</string> <string name="loan_rate_fmt">%.2f%%</string>
<!-- Cards -->
<string name="nav_pay_with_card">Pay with Card</string>
<string name="card_pay_qr">QR Pay</string>
<string name="card_pay_nfc">NFC Pay</string>
<string name="card_action_change_pin">Change PIN</string>
<string name="card_action_freeze">Freeze</string>
<string name="card_action_block">Block</string>
<string name="cards_empty">No cards found</string>
</resources> </resources>