15 Commits
Author SHA1 Message Date
shihaam 94b280a177 version 1.0.7
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
shihaam 88c9f153e5 rm temp file 2026-05-22 06:43:11 +05:00
shihaam eb7da01b2e auto and lazy load cards to dashbaord
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 06:42:43 +05:00
shihaam 27270f1b7a auto unlock on correct pin
Auto Tag on Version Change / check-version (push) Successful in 4s
2026-05-22 06:39:59 +05:00
shihaam fd7fcb41a6 added transfer support for bml business profiles
Auto Tag on Version Change / check-version (push) Successful in 4s
2026-05-22 06:31:21 +05:00
shihaam c9ae614fc7 prep support for transfers for bml business accounts)
Auto Tag on Version Change / check-version (push) Successful in 5s
2026-05-22 06:21:20 +05:00
shihaam b784085605 optimize bml refresh flow
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 06:01:13 +05:00
shihaam 01e5c17284 move refresh indicator to action bar to fix ui shifting
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 05:14:46 +05:00
shihaam 6d3c7036b5 rebranding
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 05:05:57 +05:00
shihaam 804712d22d cards on dashboard now
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 04:28:51 +05:00
shihaam f208ee6ad1 optimze mib cards loading
Auto Tag on Version Change / check-version (push) Successful in 5s
2026-05-22 03:55:59 +05:00
shihaam 51dbed94d4 bug fix: paymv qr page emptu space
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 03:40:14 +05:00
shihaam 0b5a452046 exclude bml loans from dashboard total, transfer from and paymvQR
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 03:22:50 +05:00
shihaam 00297da71e Revert "fix bug that allowed to skip password setup during inital setup"
Auto Tag on Version Change / check-version (push) Successful in 3s
This reverts commit 1602d061c1.
2026-05-22 03:07:34 +05:00
shihaam 1602d061c1 fix bug that allowed to skip password setup during inital setup
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 03:01:21 +05:00
32 changed files with 1005 additions and 357 deletions
+4 -4
View File
@@ -3,11 +3,11 @@
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-05-18T20:24:18.550107339Z">
<option name="selectionMode" value="DIALOG" />
<DropdownSelection timestamp="2026-05-22T00:11:32.873305232Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=683a9830" />
</handle>
</Target>
</DropdownSelection>
@@ -15,7 +15,7 @@
<targets>
<Target type="DEFAULT_BOOT">
<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>
</Target>
<Target type="DEFAULT_BOOT">
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "sh.sar.basedbank"
minSdk = 26
targetSdk = 36
versionCode = 5
versionName = "1.0.6"
versionCode = 6
versionName = "1.0.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 242 KiB

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 378 KiB

@@ -32,6 +32,8 @@ class LockActivity : AppCompatActivity() {
private lateinit var salt: String
private lateinit var storedHash: String
private var biometricsEnabled = false
private var autoUnlockPin = false
private var pinLength = 4
private var isVerifying = false
private val lockPrefs get() = getSharedPreferences("lock_attempts", MODE_PRIVATE)
@@ -61,6 +63,8 @@ class LockActivity : AppCompatActivity() {
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
method = prefs.getString("security_method", "pin") ?: "pin"
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 }
salt = stored.first
@@ -134,13 +138,18 @@ class LockActivity : AppCompatActivity() {
when (key) {
"" -> if (pinDigits.isNotEmpty()) { pinDigits.removeLast(); updateDots() }
"" -> 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() {
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() {
@@ -30,6 +30,14 @@ class BmlAccountClient {
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? {
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/userinfo")).execute()
val json = resp.body?.string() ?: return null
@@ -73,6 +81,27 @@ class BmlAccountClient {
} 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(
json: String,
loginTag: String,
@@ -310,13 +310,47 @@ class BmlLoginFlow {
val tokenJson = tokenResp.body?.string() ?: throw Exception("Empty token response")
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")
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)
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 ──────────────────────────────────────────────────────────────
/**
@@ -4,8 +4,12 @@ import sh.sar.basedbank.api.models.BankAccount
data class BmlSession(
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(
val profileId: String,
@@ -17,7 +17,8 @@ class BmlTransferClient {
amount: Double,
transferType: String,
currency: String,
bank: String? = null
bank: String? = null,
channel: String = "token"
): Boolean {
val jo = JSONObject().apply {
put("debitAccount", debitAccount)
@@ -25,7 +26,7 @@ class BmlTransferClient {
put("debitAmount", amount)
put("transfertype", transferType)
put("currency", currency)
put("channel", "token")
put("channel", channel)
if (bank != null) put("bank", bank)
}
val request = Request.Builder()
@@ -55,7 +56,8 @@ class BmlTransferClient {
currency: String,
otp: String,
remarks: String = "",
bank: String? = null
bank: String? = null,
channel: String = "token"
): BmlTransferResult {
val jo = JSONObject().apply {
put("debitAccount", debitAccount)
@@ -63,7 +65,7 @@ class BmlTransferClient {
put("debitAmount", amount)
put("transfertype", transferType)
put("currency", currency)
put("channel", "token")
put("channel", channel)
put("otp", otp)
if (remarks.isNotBlank()) put("remarks", remarks)
if (bank != null) put("bank", bank)
@@ -101,7 +101,9 @@ class CardSettingsFragment : Fragment() {
tvCardOwner.text = card.cardHolderName
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
tvCardType.text = card.cardTypeDesc
PayWithCardFragment.loadCardImage(ivCardImage, PayWithCardFragment.cardImageAsset(card))
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()
}
@@ -5,14 +5,21 @@ 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.content.ContextCompat
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.LinearSnapHelper
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlForeignLimit
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.api.mib.MibFinanceDeal
import kotlin.math.abs
import sh.sar.basedbank.databinding.FragmentDashboardBinding
@@ -45,6 +52,17 @@ class DashboardFragment : Fragment() {
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()
ViewCompat.setOnApplyWindowInsetsListener(binding.buttonBar) { v, insets ->
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>) {
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" }
if (hide) {
@@ -209,4 +227,51 @@ class DashboardFragment : Fragment() {
super.onDestroyView()
_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)
}
}
}
}
}
@@ -39,7 +39,6 @@ import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.AuthExpiredException
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.BmlForeignLimitsClient
import sh.sar.basedbank.api.bml.BmlLoanDetail
@@ -64,6 +63,7 @@ import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.ContactsCache
import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.CardsCache
import sh.sar.basedbank.util.FinancingCache
import sh.sar.basedbank.util.ForeignLimitsCache
@@ -178,6 +178,8 @@ class HomeActivity : AppCompatActivity() {
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)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
@@ -191,6 +193,7 @@ class HomeActivity : AppCompatActivity() {
}
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
refreshBmlLoanDetails()
triggerRefreshCards()
} else {
// Came from lock screen — show caches immediately, refresh everything in background
val store = CredentialStore(this)
@@ -199,6 +202,8 @@ class HomeActivity : AppCompatActivity() {
val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds())
val merged = cachedMib + cachedBml + cachedFahipay
if (merged.isNotEmpty()) viewModel.accounts.value = merged
val cachedCards = CardsCache.load(this)
if (cachedCards.isNotEmpty()) viewModel.mibCards.value = cachedCards
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
@@ -568,34 +573,63 @@ fun applyNavLabelVisibility() {
}
// One async job per BML login, all run in parallel
val bmlJobs = bmlLoginIds.mapNotNull { loginId ->
val creds = store.loadBmlCredentials(loginId) ?: return@mapNotNull null
val bmlJobs = bmlLoginIds.map { loginId ->
loginId to async(Dispatchers.IO) {
val loginTag = "bml_$loginId"
val app = application as BasedBankApp
val savedProfiles = store.loadBmlProfiles(loginId)
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
// 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()) {
val legacyToken = store.loadBmlSession(loginId)
if (legacyToken != null) {
@@ -604,47 +638,11 @@ fun applyNavLabelVisibility() {
val accounts = BmlAccountClient().fetchAccounts(session, loginTag)
app.bmlSessions[loginId] = session
allAccounts += accounts
anyExpired = false
} 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())
} catch (_: Exception) {
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
}
} else {
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
}
}
@@ -716,6 +714,10 @@ fun applyNavLabelVisibility() {
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
}
refreshBmlLoanDetails()
for ((loginId, session) in app.mibSessions) {
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
refreshMibCards(loginId, session, profiles)
}
}
}
@@ -1011,6 +1013,7 @@ fun applyNavLabelVisibility() {
existing.removeAll { it.loginTag == "mib_$loginId" }
existing += cards
viewModel.mibCards.postValue(existing)
CardsCache.save(this@HomeActivity, existing)
}
} catch (_: Exception) { }
}
@@ -77,8 +77,10 @@ class PayMvQrFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val basePaddingBottom = view.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updatePadding(bottom = basePaddingBottom + navBar.bottom)
val bottomNav = activity?.findViewById<View>(R.id.bottomNavigation)
val navBarBottom = if (bottomNav?.visibility == View.VISIBLE) 0
else insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
v.updatePadding(bottom = basePaddingBottom + navBarBottom)
insets
}
setupDropdown()
@@ -95,7 +97,7 @@ class PayMvQrFragment : Fragment() {
private fun setupDropdown() {
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
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)
binding.actvAccount.setAdapter(adapter)
@@ -18,6 +18,7 @@ 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() {
@@ -57,10 +58,13 @@ class PayWithCardFragment : Fragment() {
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
}
if (viewModel.mibCards.value == null) {
val cached = CardsCache.load(requireContext())
if (cached.isNotEmpty()) {
viewModel.mibCards.value = cached
} else {
binding.loadingView.visibility = View.VISIBLE
(activity as? HomeActivity)?.triggerRefreshCards()
}
(activity as? HomeActivity)?.triggerRefreshCards()
}
override fun onResume() {
@@ -101,25 +105,32 @@ class PayWithCardFragment : Fragment() {
tvCardOwner.text = card.cardHolderName
tvCardNumber.text = formatMasked(card.maskedCardNumber)
tvCardType.text = card.cardTypeDesc
loadCardImage(ivCardImage, cardImageAsset(card))
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()
}
btnPayNfc.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 {
val desc = card.cardTypeDesc.lowercase()
return when {
"corporate" in desc || "business" in desc -> "cards/mib/visa_business.jpg"
"platinum" in desc -> "cards/mib/cards-platinum-bg-w-logo-1.jpg"
else -> "cards/mib/cards-blue-bg-w-logo-1.jpg"
}
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) {
@@ -129,7 +140,7 @@ class PayWithCardFragment : Fragment() {
}
imageView.setImageBitmap(bitmap)
} catch (_: Exception) {
imageView.setImageResource(android.R.color.darker_gray)
imageView.setImageDrawable(null)
}
}
@@ -433,6 +433,10 @@ class SettingsLoginsFragment : Fragment() {
return when (activationResult) {
is BmlActivationResult.Success -> {
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
}
is BmlActivationResult.NeedsBusinessOtp ->
@@ -475,6 +479,10 @@ class SettingsLoginsFragment : Fragment() {
}
verifyProgress.dismiss()
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
} catch (e: Exception) {
verifyProgress.dismiss()
@@ -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
val canUseBiometrics = BiometricManager.from(requireContext())
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
@@ -15,6 +15,8 @@ import android.widget.BaseAdapter
import android.widget.Filter
import android.widget.Filterable
import android.graphics.Typeface
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
@@ -36,6 +38,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
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.BmlTransferResult
import sh.sar.basedbank.api.bml.BmlValidateClient
@@ -83,6 +87,28 @@ class TransferFragment : Fragment() {
// Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL"
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 ->
if (result.resultCode != Activity.RESULT_OK) 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.setOnClickListener { initiateTransfer() }
binding.btnTransfer.setOnClickListener {
if (bmlOtpState == BmlOtpState.AWAITING_OTP) verifyBmlOtp()
else initiateTransfer()
}
binding.etAmount.addTextChangedListener { updateTransferButton() }
@@ -602,6 +631,7 @@ class TransferFragment : Fragment() {
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
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 isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
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 doTransfer: () -> Unit = {
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)
if (isBmlBusiness) {
// Business profile: async OTP channel selection flow
startBmlBusinessOtpFlow(
src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks,
isSrcCard, isDestMib, currency, allAccounts, allContacts, capturedToAvatar
)
} else {
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() {
if (bmlOtpState != BmlOtpState.NONE) return
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
}
private fun clearForm() {
resetBmlOtpState()
selectedAccount = null
binding.actvFrom.setText("", false)
binding.cardFromInfo.visibility = View.GONE
@@ -1008,7 +1337,7 @@ class TransferFragment : Fragment() {
) : BaseAdapter(), Filterable {
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" }
addAll(regular)
if (cards.isNotEmpty()) {
@@ -267,6 +267,10 @@ class CredentialsFragment : Fragment() {
bmlAccumulatedAccounts += result.accounts
val store = CredentialStore(requireContext())
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
app.bmlSessions[profile.profileId] = result.session
}
@@ -326,8 +330,16 @@ class CredentialsFragment : Fragment() {
val session = app.bmlSessions.remove(oldId)
if (session != null) {
app.bmlSessions[customerId] = session
val savedRefresh = store.loadBmlProfileRefreshToken(oldId)
val savedExpiry = store.loadBmlProfileExpiresAt(oldId)
store.clearBmlProfileSession(oldId)
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
val updatedProfiles = profiles.map {
@@ -11,19 +11,19 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
OnboardingSlide(
titleRes = R.string.onboarding_title_1,
descRes = R.string.onboarding_desc_1,
iconRes = R.drawable.ic_launcher_foreground,
iconRes = R.drawable.ic_logo,
isFirst = true
),
OnboardingSlide(
titleRes = R.string.onboarding_title_2,
descRes = R.string.onboarding_desc_2,
iconRes = R.drawable.ic_launcher_foreground,
iconRes = R.drawable.ic_logo,
isFirst = false
),
OnboardingSlide(
titleRes = R.string.onboarding_title_3,
descRes = R.string.onboarding_desc_3,
iconRes = R.drawable.ic_launcher_foreground,
iconRes = R.drawable.ic_logo,
isFirst = false,
isLast = true
)
@@ -215,9 +215,10 @@ class SecuritySetupFragment : Fragment() {
val salt = ByteArray(16).also { SecureRandom().nextBytes(it) }
val saltB64 = Base64.encodeToString(salt, Base64.NO_WRAP)
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)
.apply()
if (method == "pin") edit.putInt("pin_length", input.length)
edit.apply()
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
}
@@ -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()
}
}
@@ -264,10 +264,30 @@ class CredentialStore(context: Context) {
} 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) {
prefs.edit()
.remove("bml_profile_${profileId}_enc_token")
.remove("bml_profile_${profileId}_enc_device_id")
.remove("bml_profile_${profileId}_enc_refresh_token")
.remove("bml_profile_${profileId}_expires_at")
.apply()
}
@@ -5,166 +5,6 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:fillColor="#E8B547"
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>
@@ -1,30 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<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
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
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>
android:viewportWidth="432"
android:viewportHeight="432">
<!--
Rufiyaa symbol centered on canvas.
Original SVG bounding box: 276.85 × 175.60
Scale 0.85 → 235.3 × 149.3, centered at (216, 216):
translateX = 216 235.3/2 = 98
translateY = 216 149.3/2 = 141
-->
<group
android:scaleX="0.85"
android:scaleY="0.85"
android:translateX="98"
android:translateY="141">
<path
android:fillColor="#000000"
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" />
</group>
</vector>
+9
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>
+18 -11
View File
@@ -19,19 +19,26 @@
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
android:layout_height="wrap_content">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/refreshIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:trackCornerRadius="0dp" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
<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>
+18 -28
View File
@@ -216,41 +216,31 @@
android:layout_height="wrap_content"
android:orientation="vertical" />
<!-- Card support WIP -->
<com.google.android.material.card.MaterialCardView
<!-- Card carousel (hidden when no cards loaded) -->
<LinearLayout
android:id="@+id/sectionCards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="16dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutlineVariant">
android:visibility="gone">
<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_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp">
android:clipToPadding="false"
android:paddingEnd="4dp"/>
<TextView
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>
@@ -96,6 +96,46 @@
</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
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -336,6 +336,61 @@
</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
android:id="@+id/btnTransfer"
android:layout_width="match_parent"
@@ -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>
+6 -4
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">BasedBank</string>
<string name="app_name">ތިޖޫރީ</string>
<!-- Onboarding -->
<string name="onboarding_supported_services">ހިދުމަތްތައް</string>
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</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_desc_2">އިތުރު ބޭންކްތަކަށް ސަޕޯޓް ލިބޭ ގޮތަށް ތައްޔާރުވަމުން ދަނީ. ދިވެހިރާއްޖޭގެ ބޭންކްތަކަށް ސަޕޯޓް ފަހި ވަމުން ދިޔަ ވަރަކަށް ހިމަނެމުން ދޭ.</string>
<string name="onboarding_title_3">ފެށޭ ގޮތަށް ތައްޔާރު</string>
@@ -31,7 +31,7 @@
<string name="login">ލޮގިން</string>
<!-- Lock screen -->
<string name="unlock_app">BasedBank ހުޅުވާ</string>
<string name="unlock_app">ތިޖޫރީ ހުޅުވާ</string>
<string name="unlock_pin_subtitle">PIN ޖަހާ</string>
<string name="unlock_pattern_subtitle">ހުޅުވާ ޕެޓަން ކަހާ</string>
<string name="use_biometrics">ބަޔޮމެޓްރިކް ބޭނުން ކުރޭ</string>
@@ -43,7 +43,7 @@
<!-- Security setup -->
<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_desc">4–8 ރިޔަލެއްގެ ނަންބަރު PIN</string>
<string name="method_pattern">ޕެޓަން ކަހާ</string>
@@ -111,6 +111,8 @@
<string name="lang_english">English</string>
<string name="lang_dhivehi">ދިވެހި</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_desc">ރިސެންޓްސް ސްކްރީނުންނާއި ސްކްރީން ކެޕްޗާ ހުއްޓުވައިދޭ</string>
<string name="settings_cache">ކޭޝް</string>
+10 -5
View File
@@ -1,15 +1,15 @@
<resources>
<string name="app_name">BasedBank</string>
<string name="app_name">Thijooree</string>
<!-- Onboarding -->
<string name="onboarding_supported_services">Supported services</string>
<string name="select_language">Select Language</string>
<string name="onboarding_title_1">Your Banks, One App</string>
<string name="onboarding_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_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_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="next">Next</string>
<string name="get_started">Get Started</string>
@@ -38,7 +38,7 @@
<string name="login">Login</string>
<!-- 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_pattern_subtitle">Draw your unlock pattern</string>
<string name="use_biometrics">Use Biometrics</string>
@@ -50,7 +50,7 @@
<!-- Security setup -->
<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_desc">Your app lock is set up.</string>
@@ -155,6 +155,8 @@
<string name="settings_privacy">Privacy</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_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_desc">Prevents the app from appearing in the recents screen and blocks screen capture</string>
<string name="settings_cache">Cache</string>
@@ -232,6 +234,9 @@
<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_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 -->
<string name="contacts_empty">No contacts found</string>