Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ea227bf3b9
|
|||
|
6b3131069e
|
|||
|
8037ce3f02
|
|||
|
cecf0bedfc
|
|||
|
256f216da4
|
|||
|
0a27de4a34
|
|||
|
a3f8852163
|
|||
|
8e345746ed
|
|||
|
473e051282
|
|||
|
f9c182fe9a
|
|||
|
339dae8a37
|
|||
|
a6a1f28144
|
|||
|
523d1248bd
|
|||
|
ee9f98b720
|
|||
|
219ca9bf00
|
|||
|
e9f0cec698
|
|||
|
268f3dada0
|
|||
|
e0a554c769
|
|||
|
94b280a177
|
|||
|
88c9f153e5
|
|||
|
eb7da01b2e
|
|||
|
27270f1b7a
|
|||
|
fd7fcb41a6
|
|||
|
c9ae614fc7
|
|||
|
b784085605
|
|||
|
01e5c17284
|
|||
|
6d3c7036b5
|
|||
|
804712d22d
|
|||
|
f208ee6ad1
|
|||
|
51dbed94d4
|
|||
|
0b5a452046
|
|||
|
00297da71e
|
|||
|
1602d061c1
|
|||
|
ddd64e8624
|
|||
|
77f367844d
|
|||
|
e2729b1d1a
|
@@ -1,11 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
release:
|
release:
|
||||||
# image: git.shihaam.dev/dockerfiles/android-builder
|
|
||||||
image: git.shihaam.dev/dockerfiles/runners/gradle
|
image: git.shihaam.dev/dockerfiles/runners/gradle
|
||||||
hostname: isodroid
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
env_file: .env
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- ./release:/release
|
- ./release:/release
|
||||||
- ../../:/source
|
- ../../:/source
|
||||||
# - /root/.cache/cache-runners/gradle:/root/.gradle
|
- /root/.cache/cache-runners/gradle:/root/.gradle
|
||||||
|
|||||||
8
.idea/deploymentTargetSelector.xml
generated
@@ -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">
|
||||||
|
|||||||
8
.idea/markdown.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettings">
|
||||||
|
<option name="previewPanelProviderInfo">
|
||||||
|
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "sh.sar.basedbank"
|
applicationId = "sh.sar.basedbank"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 5
|
versionCode = 7
|
||||||
versionName = "1.0.6"
|
versionName = "1.0.8"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/main/assets/cards/bml/amex_credit_gold.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
app/src/main/assets/cards/bml/amex_credit_green.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
app/src/main/assets/cards/bml/amex_debit_gold.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
app/src/main/assets/cards/bml/amex_debit_green.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
app/src/main/assets/cards/bml/amex_platinum.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
app/src/main/assets/cards/bml/defaultcard.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
app/src/main/assets/cards/bml/master.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
app/src/main/assets/cards/bml/master_business_debit.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
app/src/main/assets/cards/bml/master_gold.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
app/src/main/assets/cards/bml/master_islamic.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
app/src/main/assets/cards/bml/master_masveriyaa.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
app/src/main/assets/cards/bml/master_odiveriyaa.png
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
app/src/main/assets/cards/bml/master_passport.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
app/src/main/assets/cards/bml/master_platinum.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
app/src/main/assets/cards/bml/master_prepaid.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
app/src/main/assets/cards/bml/master_prepaid_business.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
app/src/main/assets/cards/bml/master_prepaid_travel.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
app/src/main/assets/cards/bml/master_world.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/main/assets/cards/bml/visa_corporate.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/assets/cards/bml/visa_credit.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit_generic.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit_islamic.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit_platinum.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
app/src/main/assets/cards/bml/visa_gold.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
app/src/main/assets/cards/bml/visa_infinite.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/assets/cards/bml/visa_platinum.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
app/src/main/assets/cards/bml/visa_student_black.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
app/src/main/assets/cards/bml/visa_student_blue.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
app/src/main/assets/cards/mib/visa_black_platinum.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
app/src/main/assets/cards/mib/visa_blue_everyday.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
app/src/main/assets/cards/mib/visa_business.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -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() {
|
||||||
@@ -194,15 +203,15 @@ class LockActivity : AppCompatActivity() {
|
|||||||
if (remaining <= 0) return false
|
if (remaining <= 0) return false
|
||||||
val secs = ((remaining + 999L) / 1000L).toInt()
|
val secs = ((remaining + 999L) / 1000L).toInt()
|
||||||
val msg = getString(R.string.unlock_locked_out, secs)
|
val msg = getString(R.string.unlock_locked_out, secs)
|
||||||
binding.tvLockPinDots.text = msg
|
binding.tvPinHint.text = msg
|
||||||
binding.root.postDelayed({ updateDots() }, remaining)
|
binding.root.postDelayed({ binding.tvPinHint.text = ""; updateDots() }, remaining)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFailure() {
|
private fun showFailure() {
|
||||||
val msg = failureMessage()
|
val msg = failureMessage()
|
||||||
binding.tvLockPinDots.text = msg
|
binding.tvPinHint.text = msg
|
||||||
binding.root.postDelayed({ updateDots() }, 1200)
|
binding.root.postDelayed({ binding.tvPinHint.text = ""; updateDots() }, 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun failureMessage(): String {
|
private fun failureMessage(): String {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package sh.sar.basedbank.api.bml
|
|||||||
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import sh.sar.basedbank.api.models.BankAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
import sh.sar.basedbank.api.models.BankServerException
|
||||||
|
|
||||||
data class BmlUserInfo(
|
data class BmlUserInfo(
|
||||||
val fullName: String,
|
val fullName: String,
|
||||||
@@ -27,9 +28,19 @@ class BmlAccountClient {
|
|||||||
val json = resp.body?.string()
|
val json = resp.body?.string()
|
||||||
resp.close()
|
resp.close()
|
||||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
if (code in 500..599) throw BankServerException("BML")
|
||||||
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()
|
||||||
|
if (code in 500..599) throw BankServerException("BML")
|
||||||
|
}
|
||||||
|
|
||||||
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 +84,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,
|
||||||
@@ -137,16 +169,22 @@ class BmlAccountClient {
|
|||||||
internalId = internalId
|
internalId = internalId
|
||||||
))
|
))
|
||||||
} else if (accountType == "Card") {
|
} else if (accountType == "Card") {
|
||||||
val isVisible = item.optBoolean("account_visible", false)
|
|
||||||
if (!isVisible) continue
|
|
||||||
val isPrepaid = item.optBoolean("prepaid_card", false)
|
val isPrepaid = item.optBoolean("prepaid_card", false)
|
||||||
|
val productCode = item.optString("product_code", "")
|
||||||
val cardBalance = item.optJSONObject("cardBalance")
|
val cardBalance = item.optJSONObject("cardBalance")
|
||||||
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
||||||
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
||||||
|
val isVisible = item.optBoolean("account_visible", false)
|
||||||
|
val cardProfileType = when {
|
||||||
|
isPrepaid -> "BML_PREPAID"
|
||||||
|
isVisible -> "BML_CREDIT" // non-prepaid, visible = credit card
|
||||||
|
else -> "BML_DEBIT" // non-prepaid, not visible = debit card
|
||||||
|
}
|
||||||
prepaidCards.add(BankAccount(
|
prepaidCards.add(BankAccount(
|
||||||
bank = "BML",
|
bank = "BML",
|
||||||
profileName = profileName,
|
profileName = profileName,
|
||||||
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
profileType = cardProfileType,
|
||||||
|
productCode = productCode,
|
||||||
accountNumber = accountNumber,
|
accountNumber = accountNumber,
|
||||||
accountBriefName = item.optString("alias").ifBlank { product },
|
accountBriefName = item.optString("alias").ifBlank { product },
|
||||||
currencyName = currency,
|
currencyName = currency,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ 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 ${android.os.Build.VERSION.RELEASE}; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0"
|
||||||
|
|
||||||
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
||||||
private val cookieJar = object : CookieJar {
|
private val cookieJar = object : CookieJar {
|
||||||
@@ -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 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import sh.sar.basedbank.api.models.BankAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
import sh.sar.basedbank.api.models.BankServerException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class FahipayAccountClient {
|
class FahipayAccountClient {
|
||||||
@@ -27,8 +28,10 @@ class FahipayAccountClient {
|
|||||||
Request.Builder().url("$BASE_URL/actions/getprofile/?lang=en")
|
Request.Builder().url("$BASE_URL/actions/getprofile/?lang=en")
|
||||||
.auth(session).build()
|
.auth(session).build()
|
||||||
).execute()
|
).execute()
|
||||||
|
val code = resp.code
|
||||||
val json = resp.body?.string() ?: throw Exception("Empty profile response")
|
val json = resp.body?.string() ?: throw Exception("Empty profile response")
|
||||||
resp.close()
|
resp.close()
|
||||||
|
if (code in 500..599) throw BankServerException("Fahipay")
|
||||||
val obj = JSONObject(json)
|
val obj = JSONObject(json)
|
||||||
val props = obj.optJSONObject("props") ?: JSONObject()
|
val props = obj.optJSONObject("props") ?: JSONObject()
|
||||||
return FahipayUserProfile(
|
return FahipayUserProfile(
|
||||||
@@ -47,8 +50,10 @@ class FahipayAccountClient {
|
|||||||
Request.Builder().url("$BASE_URL/actions/getbalance/?lang=en")
|
Request.Builder().url("$BASE_URL/actions/getbalance/?lang=en")
|
||||||
.auth(session).build()
|
.auth(session).build()
|
||||||
).execute()
|
).execute()
|
||||||
|
val code = resp.code
|
||||||
val json = resp.body?.string() ?: return 0.0
|
val json = resp.body?.string() ?: return 0.0
|
||||||
resp.close()
|
resp.close()
|
||||||
|
if (code in 500..599) throw BankServerException("Fahipay")
|
||||||
return try {
|
return try {
|
||||||
val obj = JSONObject(json)
|
val obj = JSONObject(json)
|
||||||
if (obj.optBoolean("error")) 0.0 else obj.optDouble("balance", 0.0)
|
if (obj.optBoolean("error")) 0.0 else obj.optDouble("balance", 0.0)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
63
app/src/main/java/sh/sar/basedbank/api/mib/MibCardsClient.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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", "*/*")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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", "*/*")
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
bank = "MIB",
|
bank = "MIB",
|
||||||
profileName = profile.name,
|
profileName = profile.name,
|
||||||
profileType = profile.profileType,
|
profileType = profile.profileType,
|
||||||
cifType = profile.cifType,
|
productCode = profile.cifType,
|
||||||
accountNumber = a.optString("accountNumber"),
|
accountNumber = a.optString("accountNumber"),
|
||||||
accountBriefName = a.optString("accountBriefName"),
|
accountBriefName = a.optString("accountBriefName"),
|
||||||
currencyName = a.optString("currencyName"),
|
currencyName = a.optString("currencyName"),
|
||||||
@@ -318,7 +318,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
bank = "MIB",
|
bank = "MIB",
|
||||||
profileName = profile.name,
|
profileName = profile.name,
|
||||||
profileType = profile.profileType,
|
profileType = profile.profileType,
|
||||||
cifType = profile.cifType,
|
productCode = profile.cifType,
|
||||||
accountNumber = a.optString("accountNumber"),
|
accountNumber = a.optString("accountNumber"),
|
||||||
accountBriefName = a.optString("accountBriefName"),
|
accountBriefName = a.optString("accountBriefName"),
|
||||||
currencyName = a.optString("currencyName"),
|
currencyName = a.optString("currencyName"),
|
||||||
@@ -373,6 +373,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
.build()
|
.build()
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
if (response.code == 419) throw SessionExpiredException()
|
if (response.code == 419) throw SessionExpiredException()
|
||||||
|
if (response.code in 500..599) throw sh.sar.basedbank.api.models.BankServerException("MIB")
|
||||||
return response.body?.string() ?: throw IllegalStateException("Empty response body")
|
return response.body?.string() ?: throw IllegalStateException("Empty response body")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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", "*/*")
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package sh.sar.basedbank.api.models
|
package sh.sar.basedbank.api.models
|
||||||
|
|
||||||
|
/** Thrown by a bank API client when the server returns an HTTP 5xx response. */
|
||||||
|
class BankServerException(val bankName: String) : Exception("Server error from $bankName")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified account model used across all banks (MIB, BML, Fahipay, ...).
|
* Unified account model used across all banks (MIB, BML, Fahipay, ...).
|
||||||
* The [bank] field identifies which bank owns this account.
|
* The [bank] field identifies which bank owns this account.
|
||||||
@@ -8,7 +11,7 @@ data class BankAccount(
|
|||||||
val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow
|
val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow
|
||||||
val profileName: String,
|
val profileName: String,
|
||||||
val profileType: String,
|
val profileType: String,
|
||||||
val cifType: String = "", // MIB: human-readable profile category (e.g. "Individual", "Sole Propr"); empty for other banks
|
val productCode: String = "", // bank-specific product/subtype code: MIB: CIF type label ("Individual", "Sole Propr"); BML: card product code ("C8201", "C1007")
|
||||||
val accountNumber: String,
|
val accountNumber: String,
|
||||||
val accountBriefName: String,
|
val accountBriefName: String,
|
||||||
val currencyName: String,
|
val currencyName: String,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class AccountsAdapter(
|
|||||||
else -> account.bank
|
else -> account.bank
|
||||||
}
|
}
|
||||||
val profileLabel = when (account.bank) {
|
val profileLabel = when (account.bank) {
|
||||||
"MIB" -> account.cifType.ifBlank { account.profileName }
|
"MIB" -> account.productCode.ifBlank { account.profileName }
|
||||||
else -> account.profileName
|
else -> account.profileName
|
||||||
}
|
}
|
||||||
return if (profileLabel.isNotBlank()) "$profileLabel · $bankName" else bankName
|
return if (profileLabel.isNotBlank()) "$profileLabel · $bankName" else bankName
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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.databinding.FragmentCardSettingsBinding
|
||||||
|
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||||
|
|
||||||
|
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)?.triggerRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateCardList = {
|
||||||
|
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
|
||||||
|
val bmlItems = (viewModel.accounts.value ?: emptyList())
|
||||||
|
.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
|
||||||
|
.map { CardItem.Bml(it) }
|
||||||
|
val all = mibItems + bmlItems
|
||||||
|
adapter.update(all)
|
||||||
|
binding.loadingView.visibility = View.GONE
|
||||||
|
binding.swipeRefresh.isRefreshing = false
|
||||||
|
binding.emptyView.visibility = if (all.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
binding.recyclerView.visibility = if (all.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
|
||||||
|
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<CardItem>,
|
||||||
|
private val context: Context
|
||||||
|
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
|
||||||
|
|
||||||
|
fun update(newCards: List<CardItem>) {
|
||||||
|
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 tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus)
|
||||||
|
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(item: CardItem) {
|
||||||
|
when (item) {
|
||||||
|
is CardItem.Mib -> {
|
||||||
|
tvCardOwner.text = item.card.cardHolderName
|
||||||
|
tvCardNumber.text = PayWithCardFragment.formatMasked(item.card.maskedCardNumber)
|
||||||
|
tvCardType.text = item.card.cardTypeDesc
|
||||||
|
val assetPath = PayWithCardFragment.cardImageAsset(item.card)
|
||||||
|
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||||
|
else ivCardImage.setImageDrawable(null)
|
||||||
|
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.mibCardStatusLabel(item.card.cardStatus))
|
||||||
|
itemView.alpha = 1f
|
||||||
|
}
|
||||||
|
is CardItem.Bml -> {
|
||||||
|
tvCardOwner.text = item.account.accountBriefName
|
||||||
|
tvCardNumber.text = PayWithCardFragment.formatMasked(item.account.accountNumber)
|
||||||
|
tvCardType.text = item.account.accountTypeName
|
||||||
|
PayWithCardFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||||
|
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
||||||
|
val bmlStatus = item.account.statusDesc.takeUnless { isActive }
|
||||||
|
PayWithCardFragment.bindCardStatus(tvCardStatus, bmlStatus)
|
||||||
|
itemView.alpha = if (isActive) 1f else 0.45f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,6 +145,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
viewModel.contacts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
viewModel.contacts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||||
|
viewModel.hideAmounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||||
|
|
||||||
(activity as? HomeActivity)?.loadAllContacts()
|
(activity as? HomeActivity)?.loadAllContacts()
|
||||||
}
|
}
|
||||||
@@ -183,6 +184,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
private fun buildPageItems(tabTag: String?): List<ContactPickerAdapter.PickerItem> {
|
private fun buildPageItems(tabTag: String?): List<ContactPickerAdapter.PickerItem> {
|
||||||
val search = binding.etSheetSearch.text?.toString()?.trim() ?: ""
|
val search = binding.etSheetSearch.text?.toString()?.trim() ?: ""
|
||||||
|
val hide = viewModel.hideAmounts.value ?: false
|
||||||
val items = mutableListOf<ContactPickerAdapter.PickerItem>()
|
val items = mutableListOf<ContactPickerAdapter.PickerItem>()
|
||||||
|
|
||||||
if (tabTag == RECENTS_TAG) {
|
if (tabTag == RECENTS_TAG) {
|
||||||
@@ -209,11 +211,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val fromAccount = accounts.find { it.accountNumber == fromAccountNumber }
|
val fromAccount = accounts.find { it.accountNumber == fromAccountNumber }
|
||||||
val fromCurrency = fromAccount?.currencyName ?: ""
|
val fromCurrency = fromAccount?.currencyName ?: ""
|
||||||
val fromLoginTag = fromAccount?.loginTag ?: ""
|
val fromLoginTag = fromAccount?.loginTag ?: ""
|
||||||
val fromIsCard = fromAccount?.profileType == "BML_PREPAID" || fromAccount?.profileType == "BML_CREDIT"
|
val fromIsCard = fromAccount?.profileType == "BML_PREPAID" || fromAccount?.profileType == "BML_CREDIT" || fromAccount?.profileType == "BML_DEBIT"
|
||||||
|
|
||||||
if (tabTag == MY_ACCOUNTS_TAG) {
|
if (tabTag == MY_ACCOUNTS_TAG) {
|
||||||
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" }
|
||||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
|
||||||
|
|
||||||
val filteredRegular = if (search.isBlank()) regularAccounts else regularAccounts.filter {
|
val filteredRegular = if (search.isBlank()) regularAccounts else regularAccounts.filter {
|
||||||
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
||||||
@@ -223,10 +225,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
for (acc in filteredRegular) {
|
for (acc in filteredRegular) {
|
||||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||||
val isSame = acc.accountNumber == fromAccountNumber
|
val isSame = acc.accountNumber == fromAccountNumber
|
||||||
|
val accBal = if (hide) "••••••" else acc.availableBalance
|
||||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||||
accountNumber = acc.accountNumber,
|
accountNumber = acc.accountNumber,
|
||||||
displayName = acc.accountBriefName,
|
displayName = acc.accountBriefName,
|
||||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
subtitle = "${acc.accountNumber} · ${acc.currencyName} $accBal",
|
||||||
colorHex = "#FE860E",
|
colorHex = "#FE860E",
|
||||||
isSameAsFrom = isSame,
|
isSameAsFrom = isSame,
|
||||||
imageHash = acc.profileImageHash,
|
imageHash = acc.profileImageHash,
|
||||||
@@ -246,10 +249,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||||
val isSame = acc.accountNumber == fromAccountNumber
|
val isSame = acc.accountNumber == fromAccountNumber
|
||||||
val isActive = acc.statusDesc.equals("Active", ignoreCase = true)
|
val isActive = acc.statusDesc.equals("Active", ignoreCase = true)
|
||||||
|
val cardBal = if (hide) "••••••" else acc.availableBalance
|
||||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||||
accountNumber = acc.accountNumber,
|
accountNumber = acc.accountNumber,
|
||||||
displayName = acc.accountBriefName,
|
displayName = acc.accountBriefName,
|
||||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
subtitle = "${acc.accountNumber} · ${acc.currencyName} $cardBal",
|
||||||
colorHex = "#FE860E",
|
colorHex = "#FE860E",
|
||||||
isSameAsFrom = isSame,
|
isSameAsFrom = isSame,
|
||||||
imageHash = acc.profileImageHash,
|
imageHash = acc.profileImageHash,
|
||||||
@@ -306,6 +310,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun maskAmount(formatted: String): String {
|
||||||
|
val currency = formatted.substringBefore(' ', formatted)
|
||||||
|
return "$currency ••••••"
|
||||||
|
}
|
||||||
|
|
||||||
private fun currencyMismatchReason(fromCurrency: String, toCurrency: String): String? =
|
private fun currencyMismatchReason(fromCurrency: String, toCurrency: String): String? =
|
||||||
if (fromCurrency == "MVR" && toCurrency == "USD") "Cannot transfer from MVR to USD account" else null
|
if (fromCurrency == "MVR" && toCurrency == "USD") "Cannot transfer from MVR to USD account" else null
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,23 @@ 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 sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||||
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||||
@@ -45,6 +53,27 @@ class DashboardFragment : Fragment() {
|
|||||||
binding.swipeRefresh.isRefreshing = false
|
binding.swipeRefresh.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.cardPendingFinances.setOnClickListener {
|
||||||
|
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardAdapter = DashboardCardAdapter()
|
||||||
|
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
binding.rvCards.adapter = cardAdapter
|
||||||
|
LinearSnapHelper().attachToRecyclerView(binding.rvCards)
|
||||||
|
|
||||||
|
val updateCardList = {
|
||||||
|
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
|
||||||
|
val bmlItems = (viewModel.accounts.value ?: emptyList())
|
||||||
|
.filter { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.statusDesc.equals("Active", ignoreCase = true) }
|
||||||
|
.map { CardItem.Bml(it) }
|
||||||
|
val all = mibItems + bmlItems
|
||||||
|
cardAdapter.update(all)
|
||||||
|
binding.sectionCards.visibility = if (all.isNotEmpty()) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
|
||||||
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 +107,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 +238,63 @@ class DashboardFragment : Fragment() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class DashboardCardAdapter : RecyclerView.Adapter<DashboardCardAdapter.VH>() {
|
||||||
|
private var cards: List<CardItem> = emptyList()
|
||||||
|
|
||||||
|
fun update(newCards: List<CardItem>) {
|
||||||
|
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 tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus)
|
||||||
|
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
|
||||||
|
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
|
||||||
|
|
||||||
|
fun bind(item: CardItem) {
|
||||||
|
when (item) {
|
||||||
|
is CardItem.Mib -> {
|
||||||
|
tvCardOwner.text = item.card.cardHolderName
|
||||||
|
tvCardNumber.text = PayWithCardFragment.formatMasked(item.card.maskedCardNumber)
|
||||||
|
val assetPath = PayWithCardFragment.cardImageAsset(item.card)
|
||||||
|
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||||
|
else ivCardImage.setImageDrawable(null)
|
||||||
|
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.mibCardStatusLabel(item.card.cardStatus))
|
||||||
|
}
|
||||||
|
is CardItem.Bml -> {
|
||||||
|
tvCardOwner.text = item.account.accountBriefName
|
||||||
|
tvCardNumber.text = PayWithCardFragment.formatMasked(item.account.accountNumber)
|
||||||
|
PayWithCardFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||||
|
PayWithCardFragment.bindCardStatus(tvCardStatus, null) // only Active BML cards reach dashboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -36,8 +38,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
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.AuthExpiredException
|
import sh.sar.basedbank.api.bml.AuthExpiredException
|
||||||
|
import sh.sar.basedbank.api.models.BankServerException
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
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 +57,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 +65,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 +76,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 +147,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 +180,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 +195,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 +204,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 +224,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 +359,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)
|
||||||
@@ -495,14 +549,25 @@ fun applyNavLabelVisibility() {
|
|||||||
autoRefresh(store)
|
autoRefresh(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showConnectivityBanner(message: String) {
|
||||||
|
binding.connectivityBanner.text = message
|
||||||
|
binding.connectivityBanner.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideConnectivityBanner() {
|
||||||
|
binding.connectivityBanner.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
private fun autoRefresh(store: CredentialStore) {
|
private fun autoRefresh(store: CredentialStore) {
|
||||||
val mibLoginIds = store.getMibLoginIds()
|
val mibLoginIds = store.getMibLoginIds()
|
||||||
val bmlLoginIds = store.getBmlLoginIds()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
val fahipayLoginIds = store.getFahipayLoginIds()
|
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||||
if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) return
|
if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) return
|
||||||
binding.refreshIndicator.visibility = View.VISIBLE
|
binding.refreshIndicator.visibility = View.VISIBLE
|
||||||
|
hideConnectivityBanner()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
val refreshErrors = ConcurrentLinkedQueue<String>()
|
||||||
// One async job per MIB login, all run in parallel
|
// One async job per MIB login, all run in parallel
|
||||||
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
||||||
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
||||||
@@ -516,39 +581,84 @@ fun applyNavLabelVisibility() {
|
|||||||
app.mibLoginFlows[loginId] = flow
|
app.mibLoginFlows[loginId] = flow
|
||||||
store.saveMibProfiles(loginId, flow.lastProfiles)
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
accounts
|
accounts
|
||||||
} catch (_: Exception) { AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" } }
|
} catch (e: java.io.IOException) {
|
||||||
|
refreshErrors.add("NO_INTERNET")
|
||||||
|
AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||||
|
} catch (e: BankServerException) {
|
||||||
|
refreshErrors.add("SERVER:${e.bankName}")
|
||||||
|
AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (e: java.io.IOException) {
|
||||||
|
refreshErrors.add("NO_INTERNET")
|
||||||
|
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||||
|
.filter { it.profileId == profile.profileId }
|
||||||
|
} catch (e: BankServerException) {
|
||||||
|
refreshErrors.add("SERVER:${e.bankName}")
|
||||||
|
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||||
|
.filter { it.profileId == profile.profileId }
|
||||||
|
} 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 +667,17 @@ 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 (e: java.io.IOException) {
|
||||||
} catch (_: AuthExpiredException) { anyExpired = true
|
refreshErrors.add("NO_INTERNET")
|
||||||
} 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)
|
||||||
|
} catch (e: BankServerException) {
|
||||||
|
refreshErrors.add("SERVER:${e.bankName}")
|
||||||
|
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,6 +724,12 @@ fun applyNavLabelVisibility() {
|
|||||||
app.fahipaySessions[loginId] = session
|
app.fahipaySessions[loginId] = session
|
||||||
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
accounts
|
accounts
|
||||||
|
} catch (e: java.io.IOException) {
|
||||||
|
refreshErrors.add("NO_INTERNET")
|
||||||
|
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
|
} catch (e: BankServerException) {
|
||||||
|
refreshErrors.add("SERVER:${e.bankName}")
|
||||||
|
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
}
|
}
|
||||||
@@ -663,12 +749,32 @@ fun applyNavLabelVisibility() {
|
|||||||
viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts())
|
viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts())
|
||||||
binding.refreshIndicator.visibility = View.GONE
|
binding.refreshIndicator.visibility = View.GONE
|
||||||
|
|
||||||
|
val noInternet = refreshErrors.any { it == "NO_INTERNET" }
|
||||||
|
val serverErrors = refreshErrors.filter { it.startsWith("SERVER:") }
|
||||||
|
.map { it.removePrefix("SERVER:") }.distinct()
|
||||||
|
when {
|
||||||
|
noInternet -> showConnectivityBanner(getString(R.string.connectivity_no_internet))
|
||||||
|
serverErrors.isNotEmpty() -> showConnectivityBanner(
|
||||||
|
getString(R.string.connectivity_server_error, serverErrors.joinToString(", "))
|
||||||
|
)
|
||||||
|
else -> hideConnectivityBanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
val errors = mutableSetOf<String>()
|
||||||
|
if (noInternet) errors.add("NO_INTERNET")
|
||||||
|
serverErrors.forEach { errors.add(it.uppercase()) }
|
||||||
|
viewModel.connectivityErrors.postValue(errors)
|
||||||
|
|
||||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
for ((loginId, session) in app.mibSessions) {
|
for ((loginId, session) in app.mibSessions) {
|
||||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
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 +1038,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)
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ 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
|
||||||
|
|
||||||
|
sealed class CardItem {
|
||||||
|
data class Mib(val card: MibCard) : CardItem()
|
||||||
|
data class Bml(val account: BankAccount) : CardItem()
|
||||||
|
}
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
|
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
|
||||||
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||||
@@ -20,5 +26,14 @@ 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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of connectivity error keys from the last refresh.
|
||||||
|
* Contains "NO_INTERNET" for no network, or uppercase bank names ("MIB", "BML", "FAHIPAY")
|
||||||
|
* for HTTP 5xx server errors from specific banks.
|
||||||
|
*/
|
||||||
|
val connectivityErrors = MutableLiveData<Set<String>>(emptySet())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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_DEBIT" && it.profileType != "BML_LOAN"
|
||||||
}
|
}
|
||||||
val adapter = QrAccountAdapter(requireContext(), eligible)
|
val adapter = QrAccountAdapter(requireContext(), eligible)
|
||||||
binding.actvAccount.setAdapter(adapter)
|
binding.actvAccount.setAdapter(adapter)
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
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.models.BankAccount
|
||||||
|
import sh.sar.basedbank.api.mib.MibCard
|
||||||
|
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding
|
||||||
|
import sh.sar.basedbank.util.CardsCache
|
||||||
|
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||||
|
|
||||||
|
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)?.triggerRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateCardList = {
|
||||||
|
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
|
||||||
|
val bmlItems = (viewModel.accounts.value ?: emptyList())
|
||||||
|
.filter { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.statusDesc.equals("Active", ignoreCase = true) }
|
||||||
|
.map { CardItem.Bml(it) }
|
||||||
|
val all = mibItems + bmlItems
|
||||||
|
adapter.update(all)
|
||||||
|
binding.loadingView.visibility = View.GONE
|
||||||
|
binding.swipeRefresh.isRefreshing = false
|
||||||
|
binding.emptyView.visibility = if (all.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
binding.recyclerView.visibility = if (all.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
|
||||||
|
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<CardItem>,
|
||||||
|
private val context: Context
|
||||||
|
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
|
||||||
|
|
||||||
|
fun update(newCards: List<CardItem>) {
|
||||||
|
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 tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus)
|
||||||
|
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
|
||||||
|
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
|
||||||
|
|
||||||
|
fun bind(item: CardItem) {
|
||||||
|
when (item) {
|
||||||
|
is CardItem.Mib -> {
|
||||||
|
tvCardOwner.text = item.card.cardHolderName
|
||||||
|
tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
|
||||||
|
tvCardType.text = item.card.cardTypeDesc
|
||||||
|
val assetPath = cardImageAsset(item.card)
|
||||||
|
if (assetPath != null) loadCardImage(ivCardImage, assetPath)
|
||||||
|
else ivCardImage.setImageDrawable(null)
|
||||||
|
bindCardStatus(tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
|
||||||
|
}
|
||||||
|
is CardItem.Bml -> {
|
||||||
|
tvCardOwner.text = item.account.accountBriefName
|
||||||
|
tvCardNumber.text = formatMasked(item.account.accountNumber)
|
||||||
|
tvCardType.text = item.account.accountTypeName
|
||||||
|
loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||||
|
val bmlStatus = item.account.statusDesc.takeUnless { it.equals("Active", ignoreCase = true) }
|
||||||
|
bindCardStatus(tvCardStatus, bmlStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.png"
|
||||||
|
"57" -> "cards/mib/visa_blue_everyday.png"
|
||||||
|
"70" -> "cards/mib/visa_business.png"
|
||||||
|
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 mibCardStatusLabel(cardStatus: String): String? = when (cardStatus) {
|
||||||
|
"CHST0" -> null // Active — no badge
|
||||||
|
else -> cardStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindCardStatus(tv: TextView, statusLabel: String?) {
|
||||||
|
if (statusLabel == null) { tv.visibility = View.GONE; return }
|
||||||
|
tv.visibility = View.VISIBLE
|
||||||
|
tv.text = statusLabel
|
||||||
|
val dp = tv.context.resources.displayMetrics.density
|
||||||
|
tv.background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
cornerRadius = 12 * dp
|
||||||
|
setColor(0xCC212121.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatMasked(masked: String): String {
|
||||||
|
if (masked.length < 4) return masked
|
||||||
|
return "\u2022\u2022\u2022\u2022 ${masked.takeLast(4)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,29 @@ 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 var accountDropdownAdapter: AccountDropdownAdapter? = 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
|
||||||
@@ -152,6 +179,11 @@ class TransferFragment : Fragment() {
|
|||||||
setupFromDropdown()
|
setupFromDropdown()
|
||||||
setupAccountLookup()
|
setupAccountLookup()
|
||||||
|
|
||||||
|
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
||||||
|
accountDropdownAdapter?.notifyDataSetChanged()
|
||||||
|
selectedAccount?.let { showFromCard(it) }
|
||||||
|
}
|
||||||
|
|
||||||
childFragmentManager.setFragmentResultListener(ContactPickerSheetFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
|
childFragmentManager.setFragmentResultListener(ContactPickerSheetFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
|
||||||
val accountNumber = bundle.getString(ContactPickerSheetFragment.KEY_ACCOUNT_NUMBER) ?: return@setFragmentResultListener
|
val accountNumber = bundle.getString(ContactPickerSheetFragment.KEY_ACCOUNT_NUMBER) ?: return@setFragmentResultListener
|
||||||
val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: ""
|
val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: ""
|
||||||
@@ -170,8 +202,13 @@ class TransferFragment : Fragment() {
|
|||||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.connectivityErrors.observe(viewLifecycleOwner) { updateTransferButton() }
|
||||||
|
|
||||||
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() }
|
||||||
|
|
||||||
@@ -205,6 +242,13 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tilTo.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
|
binding.tilTo.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sensitive masking helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun maskAmount(formatted: String): String {
|
||||||
|
val currency = formatted.substringBefore(' ', formatted)
|
||||||
|
return "$currency ••••••"
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupFromDropdown() {
|
private fun setupFromDropdown() {
|
||||||
binding.btnClearFromInfo.setOnClickListener {
|
binding.btnClearFromInfo.setOnClickListener {
|
||||||
selectedAccount = null
|
selectedAccount = null
|
||||||
@@ -216,11 +260,11 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||||
val adapter = AccountDropdownAdapter(requireContext(), accounts)
|
accountDropdownAdapter = AccountDropdownAdapter(requireContext(), accounts)
|
||||||
binding.actvFrom.setAdapter(adapter)
|
binding.actvFrom.setAdapter(accountDropdownAdapter)
|
||||||
|
|
||||||
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
||||||
val picked = adapter.getAccount(position) ?: return@setOnItemClickListener
|
val picked = accountDropdownAdapter?.getAccount(position) ?: return@setOnItemClickListener
|
||||||
selectedAccount = picked
|
selectedAccount = picked
|
||||||
updateAmountPrefix(picked)
|
updateAmountPrefix(picked)
|
||||||
showFromCard(picked)
|
showFromCard(picked)
|
||||||
@@ -254,13 +298,16 @@ class TransferFragment : Fragment() {
|
|||||||
val typeLabel = when {
|
val typeLabel = when {
|
||||||
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
||||||
account.profileType == "BML_CREDIT" -> "Credit Card"
|
account.profileType == "BML_CREDIT" -> "Credit Card"
|
||||||
|
account.profileType == "BML_DEBIT" -> "Debit Card"
|
||||||
account.accountTypeName.isNotBlank() -> account.accountTypeName
|
account.accountTypeName.isNotBlank() -> account.accountTypeName
|
||||||
else -> account.profileType
|
else -> account.profileType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hide = viewModel.hideAmounts.value ?: false
|
||||||
|
val balancePart = "${account.currencyName} ${account.availableBalance}"
|
||||||
binding.tvFromAccountName.text = account.accountBriefName
|
binding.tvFromAccountName.text = account.accountBriefName
|
||||||
binding.tvFromAccountNumber.text = account.accountNumber
|
binding.tvFromAccountNumber.text = account.accountNumber
|
||||||
binding.tvFromAccountDetails.text = listOfNotNull(bankLabel, typeLabel, "${account.currencyName} ${account.availableBalance}").joinToString(" · ")
|
binding.tvFromAccountDetails.text = listOfNotNull(bankLabel, typeLabel, if (hide) maskAmount(balancePart) else balancePart).joinToString(" · ")
|
||||||
binding.ivFromPhoto.setImageBitmap(makeInitialsBitmap(account.accountBriefName, colorHex))
|
binding.ivFromPhoto.setImageBitmap(makeInitialsBitmap(account.accountBriefName, colorHex))
|
||||||
binding.tilFrom.visibility = View.GONE
|
binding.tilFrom.visibility = View.GONE
|
||||||
binding.cardFromInfo.visibility = View.VISIBLE
|
binding.cardFromInfo.visibility = View.VISIBLE
|
||||||
@@ -602,7 +649,8 @@ 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 isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
val isBmlBusiness = isSrcBml && isBusinessProfile(src) // to test on personal accounts: use `isSrcBml`
|
||||||
|
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT" || src.profileType == "BML_DEBIT"
|
||||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||||
val currency = src.currencyName.ifBlank { "MVR" }
|
val currency = src.currencyName.ifBlank { "MVR" }
|
||||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
val allAccounts = viewModel.accounts.value ?: emptyList()
|
||||||
@@ -636,26 +684,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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,12 +798,6 @@ class TransferFragment : Fragment() {
|
|||||||
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val loginId = src.loginTag.removePrefix("mib_")
|
val loginId = src.loginTag.removePrefix("mib_")
|
||||||
// Switch to the profile that owns the source account
|
|
||||||
if (src.profileId.isNotBlank()) {
|
|
||||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
|
||||||
val profile = profiles.firstOrNull { it.profileId == src.profileId }
|
|
||||||
if (profile != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
|
||||||
}
|
|
||||||
val otp = CredentialStore(requireContext()).loadMibCredentials(loginId)?.otpSeed
|
val otp = CredentialStore(requireContext()).loadMibCredentials(loginId)?.otpSeed
|
||||||
?.let { Totp.generate(it) }
|
?.let { Totp.generate(it) }
|
||||||
?: return Triple(false, "OTP unavailable", null)
|
?: return Triple(false, "OTP unavailable", null)
|
||||||
@@ -764,6 +814,12 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
|
// Switch to the profile that owns the source account
|
||||||
|
if (src.profileId.isNotBlank()) {
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
val profile = profiles.firstOrNull { it.profileId == src.profileId }
|
||||||
|
if (profile != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
||||||
|
}
|
||||||
val result = MibTransferClient().transfer(
|
val result = MibTransferClient().transfer(
|
||||||
session = sess,
|
session = sess,
|
||||||
fromAccount = src.accountNumber,
|
fromAccount = src.accountNumber,
|
||||||
@@ -822,7 +878,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine type + credit account
|
// Determine type + credit account
|
||||||
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount }
|
||||||
val (transferType, creditAccount, bank) = when {
|
val (transferType, creditAccount, bank) = when {
|
||||||
isSrcCard -> {
|
isSrcCard -> {
|
||||||
// CAD: card → own BML account
|
// CAD: card → own BML account
|
||||||
@@ -831,7 +887,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
isDestMyCard -> {
|
isDestMyCard -> {
|
||||||
// CPA: BML CASA → own card top-up
|
// CPA: BML CASA → own card top-up
|
||||||
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount }
|
||||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
||||||
}
|
}
|
||||||
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
||||||
@@ -884,12 +940,308 @@ 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.profileType == "BML_DEBIT") && 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.profileType == "BML_DEBIT") && 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
|
val hasAll = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
|
||||||
|
if (!hasAll) { binding.btnTransfer.isEnabled = false; return }
|
||||||
|
val errors = viewModel.connectivityErrors.value ?: emptySet()
|
||||||
|
val bankOffline = "NO_INTERNET" in errors ||
|
||||||
|
selectedAccount?.bank?.uppercase()?.let { it in errors } == true
|
||||||
|
binding.btnTransfer.isEnabled = !bankOffline
|
||||||
}
|
}
|
||||||
|
|
||||||
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,8 +1360,8 @@ class TransferFragment : Fragment() {
|
|||||||
) : BaseAdapter(), Filterable {
|
) : BaseAdapter(), Filterable {
|
||||||
|
|
||||||
private val items: List<Any> = buildList {
|
private val items: List<Any> = buildList {
|
||||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && 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" || it.profileType == "BML_DEBIT" }
|
||||||
addAll(regular)
|
addAll(regular)
|
||||||
if (cards.isNotEmpty()) {
|
if (cards.isNotEmpty()) {
|
||||||
add(getString(R.string.cards))
|
add(getString(R.string.cards))
|
||||||
@@ -1018,7 +1370,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAccount(position: Int): BankAccount? = (items.getOrNull(position) as? BankAccount)
|
fun getAccount(position: Int): BankAccount? = (items.getOrNull(position) as? BankAccount)
|
||||||
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||||
|
|
||||||
override fun getCount() = items.size
|
override fun getCount() = items.size
|
||||||
override fun getItem(position: Int) = items[position]
|
override fun getItem(position: Int) = items[position]
|
||||||
@@ -1049,12 +1401,14 @@ class TransferFragment : Fragment() {
|
|||||||
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
.also { it.root.tag = it }
|
.also { it.root.tag = it }
|
||||||
}
|
}
|
||||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT" || acc.profileType == "BML_DEBIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||||
val isBmlAccount = acc.bank == "BML"
|
val isBmlAccount = acc.bank == "BML"
|
||||||
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||||
|
val hide = viewModel.hideAmounts.value ?: false
|
||||||
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
||||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||||
b.tvDropdownBalance.text = AccountListParser.from(acc)?.balance ?: ""
|
val balance = AccountListParser.from(acc)?.balance ?: ""
|
||||||
|
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
|
||||||
b.root.alpha = if (inactive) 0.4f else 1f
|
b.root.alpha = if (inactive) 0.4f else 1f
|
||||||
b.root
|
b.root
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
) {
|
) {
|
||||||
fun hasMore(): Boolean = when {
|
fun hasMore(): Boolean = when {
|
||||||
account.bank == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
account.bank == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||||
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
|
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" || account.profileType == "BML_DEBIT" -> cardMonthOffset < 2
|
||||||
account.bank == "BML" -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
account.bank == "BML" -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||||
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
async {
|
async {
|
||||||
try {
|
try {
|
||||||
when {
|
when {
|
||||||
state.account.profileType == "BML_PREPAID" || state.account.profileType == "BML_CREDIT" -> {
|
state.account.profileType == "BML_PREPAID" || state.account.profileType == "BML_CREDIT" || state.account.profileType == "BML_DEBIT" -> {
|
||||||
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
|
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
|
||||||
val cal = Calendar.getInstance()
|
val cal = Calendar.getInstance()
|
||||||
cal.add(Calendar.MONTH, -state.cardMonthOffset)
|
cal.add(Calendar.MONTH, -state.cardMonthOffset)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ object AccountCache {
|
|||||||
put("bank", acc.bank)
|
put("bank", acc.bank)
|
||||||
put("profileName", acc.profileName)
|
put("profileName", acc.profileName)
|
||||||
put("profileType", acc.profileType)
|
put("profileType", acc.profileType)
|
||||||
put("cifType", acc.cifType)
|
put("productCode", acc.productCode)
|
||||||
put("accountNumber", acc.accountNumber)
|
put("accountNumber", acc.accountNumber)
|
||||||
put("accountBriefName", acc.accountBriefName)
|
put("accountBriefName", acc.accountBriefName)
|
||||||
put("currencyName", acc.currencyName)
|
put("currencyName", acc.currencyName)
|
||||||
@@ -44,6 +44,7 @@ object AccountCache {
|
|||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
put("profileName", acc.profileName)
|
put("profileName", acc.profileName)
|
||||||
put("profileType", acc.profileType)
|
put("profileType", acc.profileType)
|
||||||
|
put("productCode", acc.productCode)
|
||||||
put("accountNumber", acc.accountNumber)
|
put("accountNumber", acc.accountNumber)
|
||||||
put("accountBriefName", acc.accountBriefName)
|
put("accountBriefName", acc.accountBriefName)
|
||||||
put("currencyName", acc.currencyName)
|
put("currencyName", acc.currencyName)
|
||||||
@@ -55,6 +56,7 @@ object AccountCache {
|
|||||||
put("statusDesc", acc.statusDesc)
|
put("statusDesc", acc.statusDesc)
|
||||||
put("loginTag", acc.loginTag)
|
put("loginTag", acc.loginTag)
|
||||||
put("internalId", acc.internalId)
|
put("internalId", acc.internalId)
|
||||||
|
put("profileId", acc.profileId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
@@ -72,6 +74,7 @@ object AccountCache {
|
|||||||
bank = "BML",
|
bank = "BML",
|
||||||
profileName = o.optString("profileName"),
|
profileName = o.optString("profileName"),
|
||||||
profileType = o.optString("profileType"),
|
profileType = o.optString("profileType"),
|
||||||
|
productCode = o.optString("productCode", ""),
|
||||||
accountNumber = o.optString("accountNumber"),
|
accountNumber = o.optString("accountNumber"),
|
||||||
accountBriefName = o.optString("accountBriefName"),
|
accountBriefName = o.optString("accountBriefName"),
|
||||||
currencyName = o.optString("currencyName"),
|
currencyName = o.optString("currencyName"),
|
||||||
@@ -83,7 +86,8 @@ object AccountCache {
|
|||||||
statusDesc = o.optString("statusDesc"),
|
statusDesc = o.optString("statusDesc"),
|
||||||
profileImageHash = null,
|
profileImageHash = null,
|
||||||
loginTag = o.optString("loginTag"),
|
loginTag = o.optString("loginTag"),
|
||||||
internalId = o.optString("internalId", "")
|
internalId = o.optString("internalId", ""),
|
||||||
|
profileId = o.optString("profileId", "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
@@ -162,7 +166,7 @@ object AccountCache {
|
|||||||
bank = o.optString("bank", "MIB"),
|
bank = o.optString("bank", "MIB"),
|
||||||
profileName = o.optString("profileName"),
|
profileName = o.optString("profileName"),
|
||||||
profileType = o.optString("profileType"),
|
profileType = o.optString("profileType"),
|
||||||
cifType = o.optString("cifType", ""),
|
productCode = o.optString("productCode", ""),
|
||||||
accountNumber = o.optString("accountNumber"),
|
accountNumber = o.optString("accountNumber"),
|
||||||
accountBriefName = o.optString("accountBriefName"),
|
accountBriefName = o.optString("accountBriefName"),
|
||||||
currencyName = o.optString("currencyName"),
|
currencyName = o.optString("currencyName"),
|
||||||
|
|||||||
57
app/src/main/java/sh/sar/basedbank/util/CardsCache.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.Locale
|
|||||||
class HistoryFetcher(private val account: BankAccount) {
|
class HistoryFetcher(private val account: BankAccount) {
|
||||||
|
|
||||||
private val isMib get() = account.bank == "MIB"
|
private val isMib get() = account.bank == "MIB"
|
||||||
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" || account.profileType == "BML_DEBIT"
|
||||||
private val isBmlLoan get() = account.profileType == "BML_LOAN"
|
private val isBmlLoan get() = account.profileType == "BML_LOAN"
|
||||||
private val isFahipay get() = account.bank == "FAHIPAY"
|
private val isFahipay get() = account.bank == "FAHIPAY"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package sh.sar.basedbank.util.bmlapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
|
||||||
|
object BmlCardParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the asset path for the card image.
|
||||||
|
* The product code is stored in [BankAccount.productCode] for BML Card accounts.
|
||||||
|
*/
|
||||||
|
fun cardImageAsset(account: BankAccount): String =
|
||||||
|
productCodeToAsset(account.productCode)
|
||||||
|
|
||||||
|
fun productCodeToAsset(productCode: String): String = when (productCode) {
|
||||||
|
"C8201", "C8001", "C8009" -> "cards/bml/master_prepaid.png"
|
||||||
|
"C8205", "C8005", "C8008" -> "cards/bml/master_prepaid_travel.png"
|
||||||
|
"C3007", "C3017", "C3097", "C3095", "C3077", "C3177" -> "cards/bml/amex_debit_green.png"
|
||||||
|
"C3003", "C3013", "C3053", "C3023", "C3033", "C3052" -> "cards/bml/amex_debit_gold.png"
|
||||||
|
"C3009", "C3019", "C3029", "C3099", "C3088", "C3188" -> "cards/bml/amex_credit_gold.png"
|
||||||
|
"C3001", "C3011", "C3050", "C3051", "C3031" -> "cards/bml/amex_credit_green.png"
|
||||||
|
"C3005", "C3015", "C3055", "C3054" -> "cards/bml/amex_platinum.png"
|
||||||
|
"C1003", "C1013", "C1083", "C1084", "C1103", "C1113", "C1183", "C1184" -> "cards/bml/visa_gold.png"
|
||||||
|
"C1007", "C1027", "C1097", "C1107", "C1197", "C1077", "C1177" -> "cards/bml/visa_debit.png"
|
||||||
|
"C1020", "C1021" -> "cards/bml/visa_debit_platinum.png"
|
||||||
|
"C8020", "C8022" -> "cards/bml/master_gold.png"
|
||||||
|
"C8902", "C8907", "C8909", "C8912", "C8992", "C8996", "C8997", "C8982", "C8983" -> "cards/bml/master_islamic.png"
|
||||||
|
"C8101" -> "cards/bml/master_masveriyaa.png"
|
||||||
|
"C8102" -> "cards/bml/master_odiveriyaa.png"
|
||||||
|
"C8010", "C8011" -> "cards/bml/master_platinum.png"
|
||||||
|
"C8040", "C8044" -> "cards/bml/master_world.png"
|
||||||
|
"C8030", "C8033" -> "cards/bml/master_business_debit.png"
|
||||||
|
"C8901", "C8991", "C8980", "C8981" -> "cards/bml/master_passport.png"
|
||||||
|
"C1030", "C1090", "C1130", "C1033", "C1133" -> "cards/bml/visa_corporate.png"
|
||||||
|
"C8905", "C8995" -> "cards/bml/visa_credit.png"
|
||||||
|
"C1001", "C1011", "C1082", "C1081", "C1101", "C1111", "C1181", "C1182" -> "cards/bml/visa_debit_generic.png"
|
||||||
|
"C1005", "C1006", "C1089" -> "cards/bml/visa_debit_islamic.png"
|
||||||
|
"C1017" -> "cards/bml/visa_infinite.png"
|
||||||
|
"C1009", "C1019", "C1085", "C1086", "C1109", "C1119", "C1185", "C1186" -> "cards/bml/visa_platinum.png"
|
||||||
|
"C1050", "C1051", "C1087", "C1088", "C1150", "C1151", "C1187", "C1188", "C1040", "C1041", "C1047", "C1048", "C1140", "C1141", "C1147", "C1148" -> "cards/bml/visa_student_black.png"
|
||||||
|
"C8925", "C8926" -> "cards/bml/visa_student_blue.png"
|
||||||
|
"C1071", "C1073", "C1061", "C1063", "C1161", "C1163" -> "cards/bml/master.png"
|
||||||
|
"C1070", "C1072", "C1059", "C1062", "C1159", "C1162" -> "cards/bml/master_prepaid_business.png"
|
||||||
|
else -> "cards/bml/defaultcard.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@ object BmlDashboardParser {
|
|||||||
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
||||||
*/
|
*/
|
||||||
fun displayData(account: BankAccount): AccountListDisplay? {
|
fun displayData(account: BankAccount): AccountListDisplay? {
|
||||||
if (account.profileType == "BML_LOAN") return null // Loans shown on financing page only
|
if (account.profileType == "BML_LOAN") return null // Loans shown on financing page only
|
||||||
|
if (account.profileType == "BML_DEBIT") return null // Debit cards shown on card screens only
|
||||||
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
return if (isCard) {
|
return if (isCard) {
|
||||||
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
||||||
|
|||||||
7
app/src/main/res/drawable/bg_card_overlay_gradient.xml
Normal 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>
|
||||||
10
app/src/main/res/drawable/ic_block.xml
Normal 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>
|
||||||
10
app/src/main/res/drawable/ic_freeze.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
9
app/src/main/res/drawable/ic_logo.xml
Normal 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>
|
||||||
10
app/src/main/res/drawable/ic_nfc.xml
Normal 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>
|
||||||
@@ -19,19 +19,40 @@
|
|||||||
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_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>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/connectivityBanner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"
|
android:background="#C62828"
|
||||||
android:visibility="gone"
|
android:textColor="#FFFFFF"
|
||||||
app:trackCornerRadius="0dp" />
|
android:gravity="center"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,16 @@
|
|||||||
android:textSize="26sp"
|
android:textSize="26sp"
|
||||||
android:letterSpacing="0.3"
|
android:letterSpacing="0.3"
|
||||||
android:textColor="?attr/colorPrimary"
|
android:textColor="?attr/colorPrimary"
|
||||||
android:layout_marginBottom="24dp" />
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPinHint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="20dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/lockNumpadContainer"
|
android:id="@+id/lockNumpadContainer"
|
||||||
|
|||||||
48
app/src/main/res/layout/fragment_card_settings.xml
Normal 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>
|
||||||
@@ -178,11 +178,14 @@
|
|||||||
|
|
||||||
<!-- Pending Finances card -->
|
<!-- Pending Finances card -->
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardPendingFinances"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
app:cardElevation="1dp"
|
app:cardElevation="1dp"
|
||||||
app:cardCornerRadius="12dp">
|
app:cardCornerRadius="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -216,41 +219,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>
|
||||||
|
|
||||||
|
|||||||
48
app/src/main/res/layout/fragment_pay_with_card.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
124
app/src/main/res/layout/item_card_dashboard.xml
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?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"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCardStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textSize="9sp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<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>
|
||||||
154
app/src/main/res/layout/item_card_settings_entry.xml
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<?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"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCardStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="3dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
138
app/src/main/res/layout/item_card_wallet.xml
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?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"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCardStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="3dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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">4–8 ރިޔަލެއްގެ ނަންބަރު PIN</string>
|
<string name="method_pin_desc">4–8 ރިޔަލެއްގެ ނަންބަރު 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>
|
||||||
@@ -124,4 +141,8 @@
|
|||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<string name="accounts">އެކައުންޓްތައް</string>
|
<string name="accounts">އެކައުންޓްތައް</string>
|
||||||
<string name="available_balance">ލިބެން ހުރި ބެލެންސް</string>
|
<string name="available_balance">ލިބެން ހުރި ބެލެންސް</string>
|
||||||
|
|
||||||
|
<!-- Connectivity banner -->
|
||||||
|
<string name="connectivity_no_internet">އިންޓަނެޓް ބައްލަވާ، ދެން ތިޖޫރީ ލޯޑް ކުރޭ</string>
|
||||||
|
<string name="connectivity_server_error">%s އާ ގުޅުމުގައި މައްސަލައެއް</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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 & Security</string>
|
<string name="settings_privacy_security">Privacy & 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,17 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<!-- Connectivity banner -->
|
||||||
|
<string name="connectivity_no_internet">Please check your connection and reload Thijooree</string>
|
||||||
|
<string name="connectivity_server_error">Connectivity issue with %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -187,15 +187,15 @@ The app presents different identities to different backends, which is intentiona
|
|||||||
| Backend | User-Agent sent |
|
| Backend | User-Agent sent |
|
||||||
|---|---|
|
|---|---|
|
||||||
| MIB API | `android/1.0` |
|
| MIB API | `android/1.0` |
|
||||||
| MIB WebView | `Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 ... Mobile Safari/537.36` |
|
| MIB WebView | `Mozilla/5.0 (Linux; Android {version}; wv) AppleWebKit/537.36 ... Mobile Safari/537.36` |
|
||||||
| BML web steps | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` |
|
| BML web steps | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` |
|
||||||
| BML API calls | `bml-mobile-banking/345 (POCO; Android 14; 22101320I)` |
|
| BML API calls | `bml-mobile-banking/345 (POCO; Android {version}; {model})` |
|
||||||
| Fahipay login/OTP | WebView UA with actual `Build.MODEL` |
|
| Fahipay login/OTP | WebView UA with actual `Build.MODEL` |
|
||||||
| Fahipay API calls | `okhttp/4.12.0` |
|
| Fahipay API calls | `okhttp/4.12.0` |
|
||||||
| Ooredoo | No custom UA |
|
| Ooredoo | No custom UA |
|
||||||
| Dhiraagu | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` |
|
| Dhiraagu | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` |
|
||||||
|
|
||||||
The BML API User-Agent hardcodes a specific device model (`POCO; Android 14; 22101320I`) to mimic the official BML mobile app. This is required for the API to accept requests.
|
The BML API User-Agent hardcodes a specific device model (`POCO; Android {version}; {model}`) to mimic the official BML mobile app. This is required for the API to accept requests.
|
||||||
|
|
||||||
The Fahipay login includes `Build.MODEL` and `Build.MANUFACTURER` from the actual device, sent as `device[model]` and `device[manufacturer]` form fields. This is a **device fingerprint** sent to Fahipay on every login.
|
The Fahipay login includes `Build.MODEL` and `Build.MANUFACTURER` from the actual device, sent as `device[model]` and `device[manufacturer]` form fields. This is a **device fingerprint** sent to Fahipay on every login.
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ All hardcoded values in this codebase are protocol constants extracted from reve
|
|||||||
| `P = BigInteger("2410312426921...")` | `MibCrypto.kt:17` | MIB DH prime modulus. Same value as in the official app. | Public parameter — no risk. |
|
| `P = BigInteger("2410312426921...")` | `MibCrypto.kt:17` | MIB DH prime modulus. Same value as in the official app. | Public parameter — no risk. |
|
||||||
| `CLIENT_ID = "98C83590-513F-4716-B02B-EC68B7D9E7E7"` | `BmlLoginFlow.kt:30` | BML OAuth client ID, extracted from the official BML mobile app APK. | Not a personal secret — it is the same value for all BML mobile clients. |
|
| `CLIENT_ID = "98C83590-513F-4716-B02B-EC68B7D9E7E7"` | `BmlLoginFlow.kt:30` | BML OAuth client ID, extracted from the official BML mobile app APK. | Not a personal secret — it is the same value for all BML mobile clients. |
|
||||||
| `REDIRECT_URI = "https://app.bankofmaldives.com.mv/oauth/mobile-callback"` | `BmlLoginFlow.kt:31` | BML OAuth redirect URI, must match what BML's server expects. | Fixed protocol value. |
|
| `REDIRECT_URI = "https://app.bankofmaldives.com.mv/oauth/mobile-callback"` | `BmlLoginFlow.kt:31` | BML OAuth redirect URI, must match what BML's server expects. | Fixed protocol value. |
|
||||||
| `APP_USER_AGENT = "bml-mobile-banking/345 (POCO; Android 14; 22101320I)"` | `BmlLoginFlow.kt:32` | Impersonates the official BML app and a specific POCO device model. | Intentional compatibility measure; no personal data. |
|
| `APP_USER_AGENT = "bml-mobile-banking/345 (POCO; Android {version}; {model})"` | `BmlLoginFlow.kt:32` | Impersonates the official BML app and a specific POCO device model. | Intentional compatibility measure; no personal data. |
|
||||||
| `APP_VERSION = "2.1.43.345"` | `BmlLoginFlow.kt:33` | BML app version string being impersonated. | Fixed protocol value. |
|
| `APP_VERSION = "2.1.43.345"` | `BmlLoginFlow.kt:33` | BML app version string being impersonated. | Fixed protocol value. |
|
||||||
| `website_id = "CA2BB809-3A22-485B-A518-DA6B6DE653A5"` | `DhiraaguClient.kt:45` | Dhiraagu SDK identifier embedded in the lookup URL. Extracted from Dhiraagu's public Easy Pay page. | Public value embedded in their web page; not a secret. |
|
| `website_id = "CA2BB809-3A22-485B-A518-DA6B6DE653A5"` | `DhiraaguClient.kt:45` | Dhiraagu SDK identifier embedded in the lookup URL. Extracted from Dhiraagu's public Easy Pay page. | Public value embedded in their web page; not a secret. |
|
||||||
| `MIB_SWIFT_ON_BML = "F4E79935-3E73-E611-80DD-00155D020F0A"` | `AddContactSheetFragment.kt:457` | BML's internal bank code (SWIFT/FinInstnId) used to identify MIB as the counterpart bank during BML transfers. | Protocol constant, not a secret. |
|
| `MIB_SWIFT_ON_BML = "F4E79935-3E73-E611-80DD-00155D020F0A"` | `AddContactSheetFragment.kt:457` | BML's internal bank code (SWIFT/FinInstnId) used to identify MIB as the counterpart bank during BML transfers. | Protocol constant, not a secret. |
|
||||||
|
|||||||
256
docs/bmlapi/01-login.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# Login
|
||||||
|
|
||||||
|
Authenticate a user through the BML web session flow. This involves four sequential HTTP steps before OAuth can begin: seeding cookies, posting credentials, verifying TOTP, and selecting a profile.
|
||||||
|
|
||||||
|
All requests in this flow use the **web User-Agent** and a shared cookie jar that persists cookies (`XSRF-TOKEN`, `blaze_session`, `blaze_identity`) across steps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Seed Session Cookies
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://www.bankofmaldives.com.mv/internetbanking/web/login
|
||||||
|
```
|
||||||
|
|
||||||
|
This request initialises the session. The server sets the `XSRF-TOKEN` and `blaze_session` cookies which must be carried through the entire login flow.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request GET \
|
||||||
|
--url 'https://www.bankofmaldives.com.mv/internetbanking/web/login' \
|
||||||
|
--header 'User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
`200 OK` — HTML page. The cookies set in the response headers are the only output needed:
|
||||||
|
|
||||||
|
```
|
||||||
|
Set-Cookie: XSRF-TOKEN=<token>; Path=/; SameSite=Lax
|
||||||
|
Set-Cookie: blaze_session=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract `XSRF-TOKEN` from the cookie store for use in Step 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Submit Credentials
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://www.bankofmaldives.com.mv/internetbanking/web/login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
**Content-Type:** `application/json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "A123456",
|
||||||
|
"password": "your_password",
|
||||||
|
"code": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `username` | BML online banking username (typically the national ID card number) |
|
||||||
|
| `password` | Online banking password |
|
||||||
|
| `code` | Always empty string `""` at this step |
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
| Header | Value |
|
||||||
|
|---|---|
|
||||||
|
| `X-XSRF-TOKEN` | Value of the `XSRF-TOKEN` cookie from Step 1 |
|
||||||
|
| `Content-Type` | `application/json` |
|
||||||
|
| `User-Agent` | `Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url 'https://www.bankofmaldives.com.mv/internetbanking/web/login' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0' \
|
||||||
|
--header 'X-XSRF-TOKEN: <xsrf_token>' \
|
||||||
|
--cookie 'XSRF-TOKEN=<xsrf_token>; blaze_session=<session>' \
|
||||||
|
--data '{"username":"A123456","password":"your_password","code":""}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
**Success:** `302` redirect to `/web/login/2fa`. The `blaze_session` cookie is updated.
|
||||||
|
|
||||||
|
**Failure:** Any non-`302` response (typically `200` with the login page re-rendered) means the credentials were rejected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Fetch 2FA Page
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://www.bankofmaldives.com.mv/internetbanking/web/login/2fa
|
||||||
|
```
|
||||||
|
|
||||||
|
This request refreshes the `XSRF-TOKEN` cookie for the TOTP submission in Step 4. The response body is an HTML page that can be discarded.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request GET \
|
||||||
|
--url 'https://www.bankofmaldives.com.mv/internetbanking/web/login/2fa' \
|
||||||
|
--header 'User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0' \
|
||||||
|
--cookie 'XSRF-TOKEN=<xsrf_token>; blaze_session=<session>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract the fresh `XSRF-TOKEN` from the updated cookie store before Step 4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Submit TOTP
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://www.bankofmaldives.com.mv/internetbanking/web/login/2fa
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
**Content-Type:** `application/json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "123456",
|
||||||
|
"channel": "authenticator"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Value | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `code` | `123456` | 6-digit TOTP from the user's authenticator app |
|
||||||
|
| `channel` | `authenticator` | Always `authenticator` for TOTP |
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
| Header | Value |
|
||||||
|
|---|---|
|
||||||
|
| `X-XSRF-TOKEN` | Fresh value from Step 3 |
|
||||||
|
| `Content-Type` | `application/json` |
|
||||||
|
| `User-Agent` | Web UA |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url 'https://www.bankofmaldives.com.mv/internetbanking/web/login/2fa' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0' \
|
||||||
|
--header 'X-XSRF-TOKEN: <xsrf_token_2>' \
|
||||||
|
--cookie 'XSRF-TOKEN=<xsrf_token_2>; blaze_session=<session>' \
|
||||||
|
--data '{"code":"123456","channel":"authenticator"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
**Success:** `302` redirect to `/web/profile`.
|
||||||
|
|
||||||
|
**Failure:** Any non-`302` (typically a `200` with the 2FA page) means the TOTP was invalid. Generate a fresh code and retry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TOTP Details
|
||||||
|
|
||||||
|
BML uses standard RFC 6238 TOTP:
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|---|---|
|
||||||
|
| Algorithm | HMAC-SHA1 |
|
||||||
|
| Period | 30 seconds |
|
||||||
|
| Digits | 6 |
|
||||||
|
| Encoding | Base32 secret |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Profile Selection
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://www.bankofmaldives.com.mv/internetbanking/web/profile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response — Multi-profile account
|
||||||
|
|
||||||
|
`200 OK` with an HTML page containing an Inertia.js `data-page` payload. After [extracting the Inertia JSON](README.md#inertiajs-response-format):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"props": {
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "12345",
|
||||||
|
"name": "Mohamed Ali",
|
||||||
|
"type": "Profile",
|
||||||
|
"profile": {
|
||||||
|
"profile_type": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "67890",
|
||||||
|
"name": "My Company Ltd",
|
||||||
|
"type": "Business",
|
||||||
|
"profile": {
|
||||||
|
"profile_type": "business"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `profile_id` | `string` | ID used in Step 6 to activate this profile |
|
||||||
|
| `name` | `string` | Display name of the profile |
|
||||||
|
| `type` | `string` | `"Profile"` for personal, `"Business"` for business |
|
||||||
|
| `profile.profile_type` | `string` | `"default"` (personal) or `"business"` |
|
||||||
|
|
||||||
|
### Response — Single-profile account
|
||||||
|
|
||||||
|
`302` redirect (to `/web/redirect` or similar). The server has auto-activated the sole profile and set the `blaze_identity` cookie. Skip Step 6 and proceed directly to [OAuth Token Exchange](03-oauth-token.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6 — Activate Profile
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://www.bankofmaldives.com.mv/internetbanking/web/profile/{profile_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `{profile_id}` with the value from the profile list.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request GET \
|
||||||
|
--url 'https://www.bankofmaldives.com.mv/internetbanking/web/profile/12345' \
|
||||||
|
--header 'User-Agent: Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0' \
|
||||||
|
--cookie 'XSRF-TOKEN=<xsrf_token>; blaze_session=<session>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response — Personal profile (`profile_type: "default"`)
|
||||||
|
|
||||||
|
`302` redirect (to `/web/redirect` or any path other than `/web/profile/2fa/business`), or `409 Conflict` if the profile was already active.
|
||||||
|
|
||||||
|
Both outcomes mean success: the `blaze_identity` cookie is now set. Proceed to [OAuth Token Exchange](03-oauth-token.md).
|
||||||
|
|
||||||
|
### Response — Business profile (`profile_type: "business"`)
|
||||||
|
|
||||||
|
`302` redirect to `/web/profile/2fa/business`. The business profile requires an additional SMS/email OTP verification step.
|
||||||
|
|
||||||
|
Proceed to [Business Profile OTP](02-business-otp.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Personal profile activated** → proceed to **[OAuth Token Exchange](03-oauth-token.md)**
|
||||||
|
- **Business profile** → proceed to **[Business Profile OTP](02-business-otp.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[← README](README.md) **Next →** [Business Profile OTP](02-business-otp.md)
|
||||||