forked from shihaam/thijooree
add bml login, accounts and contacts
This commit is contained in:
@@ -3,6 +3,7 @@ package sh.sar.basedbank
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import sh.sar.basedbank.api.bml.BmlSession
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
import sh.sar.basedbank.api.mib.MibProfile
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
@@ -15,6 +16,8 @@ class BasedBankApp : Application() {
|
|||||||
var fullName: String = ""
|
var fullName: String = ""
|
||||||
var mibSession: MibSession? = null
|
var mibSession: MibSession? = null
|
||||||
var mibProfiles: List<MibProfile> = emptyList()
|
var mibProfiles: List<MibProfile> = emptyList()
|
||||||
|
var bmlSession: BmlSession? = null
|
||||||
|
var bmlAccounts: List<MibAccount> = emptyList()
|
||||||
|
|
||||||
val mibLoginFlow by lazy {
|
val mibLoginFlow by lazy {
|
||||||
MibLoginFlow(getSharedPreferences("mib_prefs", MODE_PRIVATE))
|
MibLoginFlow(getSharedPreferences("mib_prefs", MODE_PRIVATE))
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.util.Totp
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.Base64
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class BmlLoginFlow {
|
||||||
|
|
||||||
|
private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
|
||||||
|
private val CLIENT_ID = "98C83590-513F-4716-B02B-EC68B7D9E7E7"
|
||||||
|
private val REDIRECT_URI = "https://app.bankofmaldives.com.mv/oauth/mobile-callback"
|
||||||
|
private val APP_USER_AGENT = "bml-mobile-banking/345 (POCO; Android 14; 22101320I)"
|
||||||
|
private val APP_VERSION = "2.1.43.345"
|
||||||
|
private val WEB_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"
|
||||||
|
|
||||||
|
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
||||||
|
private val cookieJar = object : CookieJar {
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
val list = cookieStore.getOrPut(url.host) { mutableListOf() }
|
||||||
|
for (c in cookies) {
|
||||||
|
list.removeAll { it.name == c.name }
|
||||||
|
list.add(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> =
|
||||||
|
cookieStore[url.host] ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.cookieJar(cookieJar)
|
||||||
|
.followRedirects(false)
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val apiClient = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/** Full login: returns a BmlSession and the account list. */
|
||||||
|
fun login(username: String, password: String, otpSeed: String): Pair<BmlSession, List<MibAccount>> {
|
||||||
|
// Step 1: GET login page — seeds XSRF-TOKEN + blaze_session cookies
|
||||||
|
client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/web/login")
|
||||||
|
.header("User-Agent", WEB_USER_AGENT).build()
|
||||||
|
).execute().close()
|
||||||
|
|
||||||
|
val xsrf = xsrfToken() ?: throw Exception("Could not fetch login page")
|
||||||
|
|
||||||
|
// Step 2: POST credentials
|
||||||
|
val loginBody = """{"username":${quote(username)},"password":${quote(password)},"code":""}"""
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
val loginResp = client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/web/login").post(loginBody)
|
||||||
|
.header("X-XSRF-TOKEN", xsrf)
|
||||||
|
.header("User-Agent", WEB_USER_AGENT).build()
|
||||||
|
).execute()
|
||||||
|
loginResp.close()
|
||||||
|
if (loginResp.code != 302) throw Exception("Login failed — check your username/password")
|
||||||
|
|
||||||
|
// Step 3: GET 2FA page (refreshes blaze_session)
|
||||||
|
client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/web/login/2fa")
|
||||||
|
.header("X-XSRF-TOKEN", xsrf)
|
||||||
|
.header("User-Agent", WEB_USER_AGENT).build()
|
||||||
|
).execute().close()
|
||||||
|
val xsrf2 = xsrfToken() ?: xsrf
|
||||||
|
|
||||||
|
// Step 4: POST OTP
|
||||||
|
val otp = Totp.generate(otpSeed)
|
||||||
|
val twoFaBody = """{"code":${quote(otp)},"channel":"authenticator"}"""
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
val twoFaResp = client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/web/login/2fa").post(twoFaBody)
|
||||||
|
.header("X-XSRF-TOKEN", xsrf2)
|
||||||
|
.header("User-Agent", WEB_USER_AGENT).build()
|
||||||
|
).execute()
|
||||||
|
twoFaResp.close()
|
||||||
|
if (twoFaResp.code != 302) throw Exception("OTP verification failed — check your OTP seed")
|
||||||
|
|
||||||
|
// Step 5: GET /web/profile (sets blaze_identity cookie for profile selection)
|
||||||
|
client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/web/profile")
|
||||||
|
.header("X-XSRF-TOKEN", xsrf2)
|
||||||
|
.header("User-Agent", WEB_USER_AGENT).build()
|
||||||
|
).execute().close()
|
||||||
|
|
||||||
|
// Step 6: PKCE OAuth authorize → extract auth code
|
||||||
|
val codeVerifier = generateCodeVerifier()
|
||||||
|
val codeChallenge = generateCodeChallenge(codeVerifier)
|
||||||
|
val deviceId = generateDeviceId()
|
||||||
|
|
||||||
|
val authorizeUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https").host("www.bankofmaldives.com.mv")
|
||||||
|
.addPathSegments("internetbanking/oauth/authorize")
|
||||||
|
.addQueryParameter("redirect_uri", REDIRECT_URI)
|
||||||
|
.addQueryParameter("client_id", CLIENT_ID)
|
||||||
|
.addQueryParameter("response_type", "code")
|
||||||
|
.addQueryParameter("state", randomUrlSafe(16))
|
||||||
|
.addQueryParameter("nonce", randomUrlSafe(12))
|
||||||
|
.addQueryParameter("code_challenge", codeChallenge)
|
||||||
|
.addQueryParameter("code_challenge_method", "S256")
|
||||||
|
.addQueryParameter("Device-ID", deviceId)
|
||||||
|
.addQueryParameter("User-Agent", APP_USER_AGENT)
|
||||||
|
.addQueryParameter("x-app-version", APP_VERSION)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val authorizeResp = client.newCall(
|
||||||
|
Request.Builder().url(authorizeUrl)
|
||||||
|
.header("User-Agent", WEB_USER_AGENT).build()
|
||||||
|
).execute()
|
||||||
|
authorizeResp.close()
|
||||||
|
|
||||||
|
val location = authorizeResp.header("Location")
|
||||||
|
?: throw Exception("OAuth authorize did not redirect")
|
||||||
|
val authCode = Uri.parse(location).getQueryParameter("code")
|
||||||
|
?: throw Exception("No auth code in OAuth redirect")
|
||||||
|
|
||||||
|
// Step 7: Exchange auth code for access token
|
||||||
|
val tokenBody = FormBody.Builder()
|
||||||
|
.add("Device-ID", deviceId)
|
||||||
|
.add("code", authCode)
|
||||||
|
.add("grant_type", "authorization_code")
|
||||||
|
.add("User-Agent", APP_USER_AGENT)
|
||||||
|
.add("redirect_uri", REDIRECT_URI)
|
||||||
|
.add("code_verifier", codeVerifier)
|
||||||
|
.add("client_id", CLIENT_ID)
|
||||||
|
.add("x-app-version", APP_VERSION)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val tokenResp = client.newCall(
|
||||||
|
Request.Builder().url("$BASE_URL/oauth/token").post(tokenBody)
|
||||||
|
.header("User-Agent", WEB_USER_AGENT).build()
|
||||||
|
).execute()
|
||||||
|
val tokenJson = tokenResp.body?.string() ?: throw Exception("Empty token response")
|
||||||
|
tokenResp.close()
|
||||||
|
|
||||||
|
val tokenObj = JSONObject(tokenJson)
|
||||||
|
val accessToken = tokenObj.optString("access_token")
|
||||||
|
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
|
||||||
|
|
||||||
|
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
|
||||||
|
val accounts = fetchAccounts(session)
|
||||||
|
return Pair(session, accounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchAccounts(session: BmlSession): List<MibAccount> {
|
||||||
|
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
||||||
|
val json = resp.body?.string() ?: return emptyList()
|
||||||
|
resp.close()
|
||||||
|
return parseDashboard(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
||||||
|
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute()
|
||||||
|
val json = resp.body?.string() ?: return emptyList()
|
||||||
|
resp.close()
|
||||||
|
return parseContacts(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun apiRequest(session: BmlSession, url: String) =
|
||||||
|
Request.Builder().url(url)
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", APP_USER_AGENT)
|
||||||
|
.header("x-app-version", APP_VERSION)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun parseDashboard(json: String): List<MibAccount> {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val dashboard = root.optJSONObject("payload")?.optJSONArray("dashboard") ?: return emptyList()
|
||||||
|
return (0 until dashboard.length()).map { i ->
|
||||||
|
val item = dashboard.getJSONObject(i)
|
||||||
|
val currency = item.optString("currency", "MVR")
|
||||||
|
val available = item.optDouble("availableBalance", 0.0)
|
||||||
|
MibAccount(
|
||||||
|
profileName = "Bank of Maldives",
|
||||||
|
profileType = "BML",
|
||||||
|
accountNumber = item.optString("account"),
|
||||||
|
accountBriefName = item.optString("alias"),
|
||||||
|
currencyName = currency,
|
||||||
|
accountTypeName = item.optString("product"),
|
||||||
|
availableBalance = "%.2f".format(available),
|
||||||
|
currentBalance = "%.2f".format(item.optDouble("ledgerBalance", 0.0)),
|
||||||
|
blockedAmount = "%.2f".format(item.optDouble("lockedAmount", 0.0)),
|
||||||
|
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||||
|
statusDesc = item.optString("account_status", "Active"),
|
||||||
|
profileImageHash = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseContacts(json: String): List<MibBeneficiary> {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList()
|
||||||
|
val result = mutableListOf<MibBeneficiary>()
|
||||||
|
for (i in 0 until payload.length()) {
|
||||||
|
val item = payload.getJSONObject(i)
|
||||||
|
val account = item.optString("account", "")
|
||||||
|
if (account.isBlank()) continue
|
||||||
|
result.add(MibBeneficiary(
|
||||||
|
benefNo = "bml_${item.optInt("id")}",
|
||||||
|
benefName = item.optString("name"),
|
||||||
|
benefNickName = item.optString("alias", item.optString("name")),
|
||||||
|
benefAccount = account,
|
||||||
|
benefType = "I",
|
||||||
|
bankColor = "#0066A1",
|
||||||
|
benefBankName = "Bank of Maldives",
|
||||||
|
bankCode = "",
|
||||||
|
benefStatus = item.optString("status", "S"),
|
||||||
|
transferCyDesc = item.optString("currency", "MVR"),
|
||||||
|
customerImgHash = null,
|
||||||
|
benefCategoryId = "BML"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun xsrfToken(): String? =
|
||||||
|
cookieStore["www.bankofmaldives.com.mv"]?.firstOrNull { it.name == "XSRF-TOKEN" }?.value
|
||||||
|
|
||||||
|
private fun generateCodeVerifier(): String {
|
||||||
|
val bytes = ByteArray(72)
|
||||||
|
SecureRandom().nextBytes(bytes)
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateCodeChallenge(verifier: String): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII))
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomUrlSafe(byteCount: Int): String {
|
||||||
|
val b = ByteArray(byteCount)
|
||||||
|
SecureRandom().nextBytes(b)
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateDeviceId(): String {
|
||||||
|
val bytes = ByteArray(8)
|
||||||
|
SecureRandom().nextBytes(bytes)
|
||||||
|
return bytes.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun quote(s: String) = "\"${s.replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package sh.sar.basedbank.api.bml
|
||||||
|
|
||||||
|
data class BmlSession(
|
||||||
|
val accessToken: String,
|
||||||
|
val deviceId: String
|
||||||
|
)
|
||||||
@@ -61,7 +61,11 @@ class AccountsAdapter(accounts: List<MibAccount>) :
|
|||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(item: Item.Header) {
|
fun bind(item: Item.Header) {
|
||||||
binding.tvProfileName.text = item.profileName
|
binding.tvProfileName.text = item.profileName
|
||||||
binding.tvProfileType.text = if (item.profileType == "0") "Personal" else "Business"
|
binding.tvProfileType.text = when (item.profileType) {
|
||||||
|
"BML" -> "Bank of Maldives"
|
||||||
|
"0" -> "Personal"
|
||||||
|
else -> "Business"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,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.BmlLoginFlow
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
import sh.sar.basedbank.databinding.ActivityHomeBinding
|
import sh.sar.basedbank.databinding.ActivityHomeBinding
|
||||||
import sh.sar.basedbank.ui.login.LoginActivity
|
import sh.sar.basedbank.ui.login.LoginActivity
|
||||||
@@ -63,30 +65,42 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
if (app.accounts.isNotEmpty()) {
|
if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty()) {
|
||||||
// Came from fresh manual login — accounts ready, rest fetched in background
|
// Came from fresh manual login — accounts ready, rest fetched in background
|
||||||
viewModel.accounts.value = app.accounts
|
val mibAccounts = app.accounts.filter { it.profileType != "BML" }
|
||||||
AccountCache.save(this, app.accounts)
|
val merged = mibAccounts + app.bmlAccounts
|
||||||
val cached = FinancingCache.load(this)
|
viewModel.accounts.value = merged
|
||||||
if (cached.isNotEmpty()) viewModel.financing.value = cached
|
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
|
||||||
val cachedContacts = ContactsCache.loadContacts(this)
|
if (app.bmlAccounts.isNotEmpty()) AccountCache.saveBml(this, app.bmlAccounts)
|
||||||
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
|
|
||||||
val cachedCats = ContactsCache.loadCategories(this)
|
|
||||||
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
|
||||||
refreshContacts(app.mibSession, app.mibProfiles)
|
|
||||||
} else {
|
|
||||||
// Came from lock screen — show caches immediately, refresh everything in background
|
|
||||||
val cached = AccountCache.load(this)
|
|
||||||
if (cached.isNotEmpty()) viewModel.accounts.value = cached
|
|
||||||
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 cachedContacts = ContactsCache.loadContacts(this)
|
val cachedContacts = mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this))
|
||||||
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
|
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
|
||||||
val cachedCats = ContactsCache.loadCategories(this)
|
val cachedCats = ContactsCache.loadCategories(this)
|
||||||
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
||||||
val creds = CredentialStore(this).loadMibCredentials()
|
|
||||||
if (creds != null) autoRefresh(creds)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
|
refreshContacts(app.mibSession, app.mibProfiles)
|
||||||
|
if (app.bmlSession != null) refreshBmlContacts(app)
|
||||||
|
} else {
|
||||||
|
// Came from lock screen — show caches immediately, refresh everything in background
|
||||||
|
val cachedMib = AccountCache.load(this)
|
||||||
|
val cachedBml = AccountCache.loadBml(this)
|
||||||
|
val merged = cachedMib + cachedBml
|
||||||
|
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
||||||
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
|
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||||
|
val cachedContacts = mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this))
|
||||||
|
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
|
||||||
|
val cachedCats = ContactsCache.loadCategories(this)
|
||||||
|
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
||||||
|
|
||||||
|
val store = CredentialStore(this)
|
||||||
|
val mibCreds = store.loadMibCredentials()
|
||||||
|
val bmlCreds = store.loadBmlCredentials()
|
||||||
|
if (mibCreds != null) autoRefreshMib(mibCreds, bmlCreds)
|
||||||
|
else if (bmlCreds != null) autoRefreshBml(bmlCreds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show dashboard on first create
|
// Show dashboard on first create
|
||||||
@@ -109,32 +123,96 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoRefresh(creds: CredentialStore.MibCredentials) {
|
private fun autoRefreshMib(
|
||||||
|
mibCreds: CredentialStore.MibCredentials,
|
||||||
|
bmlCreds: CredentialStore.BmlCredentials?
|
||||||
|
) {
|
||||||
binding.refreshIndicator.visibility = View.VISIBLE
|
binding.refreshIndicator.visibility = View.VISIBLE
|
||||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||||
val flow = MibLoginFlow(prefs)
|
val flow = MibLoginFlow(prefs)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
var mibAccounts: List<MibAccount> = AccountCache.load(this@HomeActivity)
|
||||||
try {
|
try {
|
||||||
val accounts = withContext(Dispatchers.IO) {
|
mibAccounts = withContext(Dispatchers.IO) {
|
||||||
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
flow.login(mibCreds.username, mibCreds.passwordHash, mibCreds.otpSeed)
|
||||||
}
|
}
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.accounts = accounts
|
app.accounts = mibAccounts
|
||||||
app.mibSession = flow.lastSession
|
app.mibSession = flow.lastSession
|
||||||
app.mibProfiles = flow.lastProfiles
|
app.mibProfiles = flow.lastProfiles
|
||||||
AccountCache.save(this@HomeActivity, accounts)
|
AccountCache.save(this@HomeActivity, mibAccounts)
|
||||||
viewModel.accounts.postValue(accounts)
|
} catch (_: Exception) { /* keep cached */ }
|
||||||
} catch (_: Exception) {
|
finally { binding.refreshIndicator.visibility = View.GONE }
|
||||||
// Keep cached data silently
|
|
||||||
} finally {
|
val bmlAccounts = AccountCache.loadBml(this@HomeActivity).toMutableList()
|
||||||
binding.refreshIndicator.visibility = View.GONE
|
if (bmlCreds != null) {
|
||||||
|
try {
|
||||||
|
val bmlFlow = BmlLoginFlow()
|
||||||
|
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||||
|
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
|
||||||
|
}
|
||||||
|
val app = application as BasedBankApp
|
||||||
|
app.bmlSession = session
|
||||||
|
app.bmlAccounts = accounts
|
||||||
|
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||||
|
bmlAccounts.clear()
|
||||||
|
bmlAccounts.addAll(accounts)
|
||||||
|
refreshBmlContacts(app)
|
||||||
|
} catch (_: Exception) { /* keep cached */ }
|
||||||
}
|
}
|
||||||
|
viewModel.accounts.postValue(mibAccounts + bmlAccounts)
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
refreshContacts(app.mibSession, app.mibProfiles)
|
refreshContacts(app.mibSession, app.mibProfiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun autoRefreshBml(bmlCreds: CredentialStore.BmlCredentials) {
|
||||||
|
binding.refreshIndicator.visibility = View.VISIBLE
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val cachedMib = AccountCache.load(this@HomeActivity)
|
||||||
|
try {
|
||||||
|
val bmlFlow = BmlLoginFlow()
|
||||||
|
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||||
|
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
|
||||||
|
}
|
||||||
|
val app = application as BasedBankApp
|
||||||
|
app.bmlSession = session
|
||||||
|
app.bmlAccounts = accounts
|
||||||
|
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||||
|
viewModel.accounts.postValue(cachedMib + accounts)
|
||||||
|
refreshBmlContacts(app)
|
||||||
|
} catch (_: Exception) { /* keep cached */ }
|
||||||
|
finally { binding.refreshIndicator.visibility = View.GONE }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshBmlContacts(app: BasedBankApp) {
|
||||||
|
val session = app.bmlSession ?: return
|
||||||
|
val bmlFlow = BmlLoginFlow()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val bmlContacts = withContext(Dispatchers.IO) { bmlFlow.fetchContacts(session) }
|
||||||
|
if (bmlContacts.isNotEmpty()) {
|
||||||
|
ContactsCache.saveBml(this@HomeActivity, bmlContacts)
|
||||||
|
val mibContacts = viewModel.contacts.value ?: ContactsCache.loadContacts(this@HomeActivity)
|
||||||
|
viewModel.contacts.postValue(mergeContacts(mibContacts, bmlContacts))
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* keep cached */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mergeContacts(
|
||||||
|
mib: List<MibBeneficiary>,
|
||||||
|
bml: List<MibBeneficiary>
|
||||||
|
): List<MibBeneficiary> {
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
val result = mutableListOf<MibBeneficiary>()
|
||||||
|
for (c in mib) if (seen.add(c.benefNo)) result.add(c)
|
||||||
|
for (c in bml) if (seen.add(c.benefNo)) result.add(c)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshContacts(session: MibSession?, profiles: List<MibProfile>) {
|
private fun refreshContacts(session: MibSession?, profiles: List<MibProfile>) {
|
||||||
if (session == null || profiles.isEmpty()) return
|
if (session == null || profiles.isEmpty()) return
|
||||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||||
@@ -162,7 +240,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
if (allContacts.isNotEmpty()) {
|
if (allContacts.isNotEmpty()) {
|
||||||
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
||||||
viewModel.contacts.postValue(allContacts)
|
val bmlContacts = ContactsCache.loadBml(this@HomeActivity)
|
||||||
|
viewModel.contacts.postValue(mergeContacts(allContacts, bmlContacts))
|
||||||
viewModel.contactCategories.postValue(allCategories)
|
viewModel.contactCategories.postValue(allCategories)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { /* keep cached data */ }
|
} catch (_: Exception) { /* keep cached data */ }
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ class BankSelectionFragment : Fragment() {
|
|||||||
binding.cardMib.setOnClickListener {
|
binding.cardMib.setOnClickListener {
|
||||||
findNavController().navigate(R.id.action_bankSelection_to_credentials)
|
findNavController().navigate(R.id.action_bankSelection_to_credentials)
|
||||||
}
|
}
|
||||||
|
binding.cardBml.setOnClickListener {
|
||||||
|
val args = android.os.Bundle().apply { putString("bankType", "BML") }
|
||||||
|
findNavController().navigate(R.id.action_bankSelection_to_credentials_bml, args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -16,6 +17,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.util.Totp
|
import sh.sar.basedbank.util.Totp
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
import sh.sar.basedbank.util.AccountCache
|
import sh.sar.basedbank.util.AccountCache
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
@@ -24,7 +27,8 @@ import sh.sar.basedbank.ui.home.HomeActivity
|
|||||||
|
|
||||||
class CredentialsFragment : Fragment() {
|
class CredentialsFragment : Fragment() {
|
||||||
|
|
||||||
private val TAG = "CredentialsFragment"
|
private val bankType: String get() = arguments?.getString("bankType") ?: "MIB"
|
||||||
|
|
||||||
private val otpHandler = Handler(Looper.getMainLooper())
|
private val otpHandler = Handler(Looper.getMainLooper())
|
||||||
private val otpRunnable = object : Runnable {
|
private val otpRunnable = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
@@ -42,6 +46,11 @@ class CredentialsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
if (bankType == "BML") {
|
||||||
|
binding.ivBankLogo.setImageResource(R.drawable.bml_logo_vector)
|
||||||
|
binding.tvSignInDesc.setText(R.string.bml_sign_in_desc)
|
||||||
|
}
|
||||||
|
|
||||||
binding.btnLogin.setOnClickListener { attemptLogin() }
|
binding.btnLogin.setOnClickListener { attemptLogin() }
|
||||||
|
|
||||||
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
|
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -82,11 +91,15 @@ class CredentialsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun attemptLogin() {
|
private fun attemptLogin() {
|
||||||
|
if (bankType == "BML") {
|
||||||
|
attemptBmlLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val username = binding.etUsername.text.toString().trim()
|
val username = binding.etUsername.text.toString().trim()
|
||||||
val password = binding.etPassword.text.toString()
|
val password = binding.etPassword.text.toString()
|
||||||
val otpSeed = binding.etOtpSeed.text.toString().trim()
|
val otpSeed = binding.etOtpSeed.text.toString().trim()
|
||||||
|
|
||||||
|
|
||||||
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
||||||
binding.tvError.text = "Please fill in all fields"
|
binding.tvError.text = "Please fill in all fields"
|
||||||
binding.tvError.visibility = View.VISIBLE
|
binding.tvError.visibility = View.VISIBLE
|
||||||
@@ -125,6 +138,47 @@ class CredentialsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun attemptBmlLogin() {
|
||||||
|
val username = binding.etUsername.text.toString().trim()
|
||||||
|
val password = binding.etPassword.text.toString()
|
||||||
|
val otpSeed = binding.etOtpSeed.text.toString().trim()
|
||||||
|
|
||||||
|
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
||||||
|
binding.tvError.text = "Please fill in all fields"
|
||||||
|
binding.tvError.visibility = View.VISIBLE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tvError.visibility = View.GONE
|
||||||
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
|
binding.btnLogin.isEnabled = false
|
||||||
|
|
||||||
|
val flow = BmlLoginFlow()
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||||
|
flow.login(username, password, otpSeed)
|
||||||
|
}
|
||||||
|
CredentialStore(requireContext()).saveBmlCredentials(username, password, otpSeed)
|
||||||
|
AccountCache.saveBml(requireContext(), accounts)
|
||||||
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
app.bmlSession = session
|
||||||
|
app.bmlAccounts = accounts
|
||||||
|
// Merge with any existing MIB accounts already in app
|
||||||
|
app.accounts = app.accounts + accounts
|
||||||
|
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.tvError.text = e.message ?: "Login failed"
|
||||||
|
binding.tvError.visibility = View.VISIBLE
|
||||||
|
} finally {
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
binding.btnLogin.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ object AccountCache {
|
|||||||
|
|
||||||
private const val PREFS = "account_cache"
|
private const val PREFS = "account_cache"
|
||||||
private const val KEY_MIB = "mib_accounts"
|
private const val KEY_MIB = "mib_accounts"
|
||||||
|
private const val KEY_BML = "bml_accounts"
|
||||||
|
|
||||||
fun save(context: Context, accounts: List<MibAccount>) {
|
fun save(context: Context, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
@@ -32,6 +33,52 @@ object AccountCache {
|
|||||||
.edit().putString(KEY_MIB, arr.toString()).apply()
|
.edit().putString(KEY_MIB, arr.toString()).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveBml(context: Context, accounts: List<MibAccount>) {
|
||||||
|
val arr = JSONArray()
|
||||||
|
for (acc in accounts) {
|
||||||
|
arr.put(JSONObject().apply {
|
||||||
|
put("profileName", acc.profileName)
|
||||||
|
put("profileType", acc.profileType)
|
||||||
|
put("accountNumber", acc.accountNumber)
|
||||||
|
put("accountBriefName", acc.accountBriefName)
|
||||||
|
put("currencyName", acc.currencyName)
|
||||||
|
put("accountTypeName", acc.accountTypeName)
|
||||||
|
put("availableBalance", acc.availableBalance)
|
||||||
|
put("currentBalance", acc.currentBalance)
|
||||||
|
put("blockedAmount", acc.blockedAmount)
|
||||||
|
put("mvrBalance", acc.mvrBalance)
|
||||||
|
put("statusDesc", acc.statusDesc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit().putString(KEY_BML, arr.toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBml(context: Context): List<MibAccount> {
|
||||||
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_BML, null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
MibAccount(
|
||||||
|
profileName = o.optString("profileName"),
|
||||||
|
profileType = o.optString("profileType"),
|
||||||
|
accountNumber = o.optString("accountNumber"),
|
||||||
|
accountBriefName = o.optString("accountBriefName"),
|
||||||
|
currencyName = o.optString("currencyName"),
|
||||||
|
accountTypeName = o.optString("accountTypeName"),
|
||||||
|
availableBalance = o.optString("availableBalance"),
|
||||||
|
currentBalance = o.optString("currentBalance"),
|
||||||
|
blockedAmount = o.optString("blockedAmount"),
|
||||||
|
mvrBalance = o.optString("mvrBalance"),
|
||||||
|
statusDesc = o.optString("statusDesc"),
|
||||||
|
profileImageHash = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
fun load(context: Context): List<MibAccount> {
|
fun load(context: Context): List<MibAccount> {
|
||||||
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_MIB, null) ?: return emptyList()
|
.getString(KEY_MIB, null) ?: return emptyList()
|
||||||
|
|||||||
@@ -77,6 +77,52 @@ object ContactsCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveBml(context: Context, contacts: List<MibBeneficiary>) {
|
||||||
|
val arr = JSONArray()
|
||||||
|
for (c in contacts) {
|
||||||
|
arr.put(JSONObject().apply {
|
||||||
|
put("benefNo", c.benefNo)
|
||||||
|
put("benefName", c.benefName)
|
||||||
|
put("benefNickName", c.benefNickName)
|
||||||
|
put("benefAccount", c.benefAccount)
|
||||||
|
put("benefType", c.benefType)
|
||||||
|
put("bankColor", c.bankColor)
|
||||||
|
put("benefBankName", c.benefBankName)
|
||||||
|
put("bankCode", c.bankCode)
|
||||||
|
put("benefStatus", c.benefStatus)
|
||||||
|
put("transferCyDesc", c.transferCyDesc)
|
||||||
|
put("benefCategoryId", c.benefCategoryId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit().putString("bml_contacts", arr.toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBml(context: Context): List<MibBeneficiary> {
|
||||||
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString("bml_contacts", null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
MibBeneficiary(
|
||||||
|
benefNo = o.optString("benefNo"),
|
||||||
|
benefName = o.optString("benefName"),
|
||||||
|
benefNickName = o.optString("benefNickName"),
|
||||||
|
benefAccount = o.optString("benefAccount"),
|
||||||
|
benefType = o.optString("benefType"),
|
||||||
|
bankColor = o.optString("bankColor", "#0066A1"),
|
||||||
|
benefBankName = o.optString("benefBankName"),
|
||||||
|
bankCode = o.optString("bankCode"),
|
||||||
|
benefStatus = o.optString("benefStatus"),
|
||||||
|
transferCyDesc = o.optString("transferCyDesc", "MVR"),
|
||||||
|
customerImgHash = null,
|
||||||
|
benefCategoryId = o.optString("benefCategoryId", "BML")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
fun loadCategories(context: Context): List<MibBeneficiaryCategory> {
|
fun loadCategories(context: Context): List<MibBeneficiaryCategory> {
|
||||||
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_CATEGORIES, null) ?: return emptyList()
|
.getString(KEY_CATEGORIES, null) ?: return emptyList()
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ class CredentialStore(context: Context) {
|
|||||||
private val transformation = "AES/GCM/NoPadding"
|
private val transformation = "AES/GCM/NoPadding"
|
||||||
|
|
||||||
data class MibCredentials(val username: String, val passwordHash: String, val otpSeed: String)
|
data class MibCredentials(val username: String, val passwordHash: String, val otpSeed: String)
|
||||||
|
data class BmlCredentials(val username: String, val password: String, val otpSeed: String)
|
||||||
|
|
||||||
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
|
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
|
||||||
|
fun hasBmlCredentials(): Boolean = prefs.contains("bml_enc_username")
|
||||||
|
|
||||||
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
|
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
@@ -53,6 +55,33 @@ class CredentialStore(context: Context) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveBmlCredentials(username: String, password: String, otpSeed: String) {
|
||||||
|
val key = getOrCreateKey()
|
||||||
|
prefs.edit()
|
||||||
|
.putString("bml_enc_username", encrypt(username, key))
|
||||||
|
.putString("bml_enc_password", encrypt(password, key))
|
||||||
|
.putString("bml_enc_otp_seed", encrypt(otpSeed, key))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBmlCredentials(): BmlCredentials? {
|
||||||
|
val key = getOrCreateKey()
|
||||||
|
val encUsername = prefs.getString("bml_enc_username", null) ?: return null
|
||||||
|
val encPassword = prefs.getString("bml_enc_password", null) ?: return null
|
||||||
|
val encSeed = prefs.getString("bml_enc_otp_seed", null) ?: return null
|
||||||
|
return try {
|
||||||
|
BmlCredentials(decrypt(encUsername, key), decrypt(encPassword, key), decrypt(encSeed, key))
|
||||||
|
} catch (e: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBmlCredentials() {
|
||||||
|
prefs.edit()
|
||||||
|
.remove("bml_enc_username")
|
||||||
|
.remove("bml_enc_password")
|
||||||
|
.remove("bml_enc_otp_seed")
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getOrCreateKey(): SecretKey {
|
private fun getOrCreateKey(): SecretKey {
|
||||||
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
||||||
ks.getKey(keyAlias, null)?.let { return it as SecretKey }
|
ks.getKey(keyAlias, null)?.let { return it as SecretKey }
|
||||||
|
|||||||
@@ -73,5 +73,49 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- BML Card -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardBml"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
app:strokeColor="?attr/colorOutline">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="138dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/bml_logo_vector"
|
||||||
|
android:contentDescription="@string/bml_name"
|
||||||
|
android:scaleType="fitStart"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/bml_name"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/bml_desc"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -15,10 +15,12 @@
|
|||||||
android:paddingBottom="32dp">
|
android:paddingBottom="32dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:id="@+id/ivBankLogo"
|
||||||
android:layout_width="138dp"
|
android:layout_width="138dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:src="@drawable/mib_faisanet_logo"
|
android:src="@drawable/mib_faisanet_logo"
|
||||||
android:contentDescription="@string/mib_name"
|
android:contentDescription="@string/mib_name"
|
||||||
|
android:scaleType="fitStart"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginBottom="32dp" />
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
android:layout_marginBottom="8dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tvSignInDesc"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/sign_in_desc"
|
android:text="@string/sign_in_desc"
|
||||||
|
|||||||
@@ -17,11 +17,29 @@
|
|||||||
app:popEnterAnim="@anim/slide_in_left"
|
app:popEnterAnim="@anim/slide_in_left"
|
||||||
app:popExitAnim="@anim/slide_out_right" />
|
app:popExitAnim="@anim/slide_out_right" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_bankSelection_to_credentials_bml"
|
||||||
|
app:destination="@id/credentialsFragment"
|
||||||
|
app:enterAnim="@anim/slide_in_right"
|
||||||
|
app:exitAnim="@anim/slide_out_left"
|
||||||
|
app:popEnterAnim="@anim/slide_in_left"
|
||||||
|
app:popExitAnim="@anim/slide_out_right">
|
||||||
|
<argument
|
||||||
|
android:name="bankType"
|
||||||
|
app:argType="string"
|
||||||
|
android:defaultValue="MIB" />
|
||||||
|
</action>
|
||||||
|
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/credentialsFragment"
|
android:id="@+id/credentialsFragment"
|
||||||
android:name="sh.sar.basedbank.ui.login.CredentialsFragment"
|
android:name="sh.sar.basedbank.ui.login.CredentialsFragment"
|
||||||
android:label="Sign In" />
|
android:label="Sign In">
|
||||||
|
<argument
|
||||||
|
android:name="bankType"
|
||||||
|
app:argType="string"
|
||||||
|
android:defaultValue="MIB" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<string name="bml_desc">BML Internet Banking</string>
|
<string name="bml_desc">BML Internet Banking</string>
|
||||||
<string name="sign_in">Sign In</string>
|
<string name="sign_in">Sign In</string>
|
||||||
<string name="sign_in_desc">Enter your Maldives Islamic Bank credentials.</string>
|
<string name="sign_in_desc">Enter your Maldives Islamic Bank credentials.</string>
|
||||||
|
<string name="bml_sign_in_desc">Enter your Bank of Maldives credentials.</string>
|
||||||
<string name="username">Username</string>
|
<string name="username">Username</string>
|
||||||
<string name="password">Password</string>
|
<string name="password">Password</string>
|
||||||
<string name="otp_seed">OTP Seed (TOTP Secret)</string>
|
<string name="otp_seed">OTP Seed (TOTP Secret)</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user