working mib login and list accounts

This commit is contained in:
2026-05-14 05:24:11 +05:00
parent 31c8a5500d
commit 076a58359a
73 changed files with 3076 additions and 550 deletions
+22 -4
View File
@@ -2,7 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".BasedBankApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -11,18 +15,32 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BasedBank"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.BasedBank.NoActionBar">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.onboarding.OnboardingActivity"
android:exported="false" />
<activity
android:name=".ui.login.LoginActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.home.HomeActivity"
android:exported="false" />
</application>
</manifest>
</manifest>
@@ -0,0 +1,17 @@
package sh.sar.basedbank
import android.app.Application
import com.google.android.material.color.DynamicColors
import sh.sar.basedbank.api.mib.MibAccount
class BasedBankApp : Application() {
// Held in memory after successful login; cleared on logout
var accounts: List<MibAccount> = emptyList()
var fullName: String = ""
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
}
@@ -1,58 +1,19 @@
package sh.sar.basedbank
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.navigation.NavigationView
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.drawerlayout.widget.DrawerLayout
import androidx.appcompat.app.AppCompatActivity
import sh.sar.basedbank.databinding.ActivityMainBinding
import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.ui.onboarding.OnboardingActivity
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appBarMain.toolbar)
binding.appBarMain.fab.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null)
.setAnchorView(R.id.fab).show()
}
val drawerLayout: DrawerLayout = binding.drawerLayout
val navView: NavigationView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_content_main)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow
), drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
val onboardingDone = prefs.getBoolean("onboarding_done", false)
val target = if (onboardingDone) LoginActivity::class.java else OnboardingActivity::class.java
startActivity(Intent(this, target))
finish()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}
}
@@ -0,0 +1,50 @@
package sh.sar.basedbank.api.mib
import android.util.Base64
import org.json.JSONObject
import java.math.BigInteger
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
object MibCrypto {
const val DEFAULT_KEY = "8M3L9SBF1AC4FRE56788M3L9SBF1AC4FRE5678"
private val A = BigInteger(
"1563516802667282387226490351799736881442299778484610378722158765594241028592123324764949712696577"
)
private val P = BigInteger(
"2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919"
)
// cmod = G^A mod P (sent in every DH key exchange request)
val CMOD: String = BigInteger.TWO.modPow(A, P).toString()
fun deriveSessionKey(smod: String): String {
val shared = BigInteger(smod).modPow(A, P)
val sha256hex = MessageDigest.getInstance("SHA-256")
.digest(shared.toString().toByteArray())
.joinToString("") { "%02X".format(it) }
val keyBytes = sha256hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
return Base64.encodeToString(keyBytes, Base64.NO_WRAP)
}
fun encrypt(json: JSONObject, key: String): String {
val plaintext = json.toString().toByteArray(Charsets.UTF_8)
val keyBytes = key.toByteArray(Charsets.ISO_8859_1)
val cipher = Cipher.getInstance("Blowfish/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyBytes, "Blowfish"))
val ct = cipher.doFinal(plaintext)
return Base64.encodeToString(ct, Base64.NO_WRAP)
}
fun decrypt(ciphertextB64: String, key: String): JSONObject {
val keyBytes = key.toByteArray(Charsets.ISO_8859_1)
val ct = Base64.decode(ciphertextB64, Base64.DEFAULT)
val cipher = Cipher.getInstance("Blowfish/ECB/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(keyBytes, "Blowfish"))
val plaintext = cipher.doFinal(ct)
return JSONObject(String(plaintext, Charsets.UTF_8))
}
}
@@ -0,0 +1,302 @@
package sh.sar.basedbank.api.mib
import android.util.Log
import okhttp3.FormBody
import sh.sar.basedbank.util.Totp
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
import kotlin.random.Random
class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
private val TAG = "MibLoginFlow"
private val BASE_URL = "https://faisanet.mib.com.mv/faisamobilex_smvc/"
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val req = chain.request().newBuilder()
.header("User-Agent", "android/1.0")
.header("Accept", "application/json")
.header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
.build()
chain.proceed(req)
}
.build()
// ─── Public entry point ───────────────────────────────────────────────────
/**
* Full login flow. Automatically handles first-time device registration
* vs. subsequent logins using stored key1/key2.
*
* Returns list of accounts from all profiles on success.
*/
fun login(username: String, password: String, otpSeed: String): List<MibAccount> {
val appId = getOrCreateAppId()
Log.d(TAG, "login: appId=$appId")
val key1 = prefs.getString("mib_key1_$username", null)
val key2 = prefs.getString("mib_key2_$username", null)
Log.d(TAG, "login: stored keys present=${key1 != null && key2 != null}")
return if (key1 != null && key2 != null) {
Log.d(TAG, "login: taking regular login path")
regularLogin(username, password, appId, key1, key2)
} else {
Log.d(TAG, "login: taking first-time registration path")
firstTimeRegistration(username, password, otpSeed, appId)
}
}
// ─── First-time registration ──────────────────────────────────────────────
private fun firstTimeRegistration(
username: String, password: String, otpSeed: String, appId: String
): List<MibAccount> {
Log.d(TAG, "[reg] step 0: key exchange (sfunc=r)")
val (session1, _) = initialKeyExchange(appId, MibCrypto.DEFAULT_KEY, "r")
Log.d(TAG, "[reg] step 0 done: xxid=${session1.xxid}")
Log.d(TAG, "[reg] step 1: getAuthType (A44)")
val userSalt = getAuthType(session1, username)
Log.d(TAG, "[reg] step 1 done: userSalt length=${userSalt.length}")
Log.d(TAG, "[reg] step 2: registration init (C41)")
Log.d(TAG, "[reg] username='$username' password='$password' userSalt='$userSalt'")
val clientSalt = randomAlpha(32)
val pgf03 = computePgf03(password, userSalt, clientSalt)
Log.d(TAG, "[reg] pgf03=$pgf03")
val regInitPayload = baseData(session1, "C41").apply {
put("uname", username)
put("pgf03", pgf03)
put("clientSalt", clientSalt)
}
val regInitResp = doRequest(session1, regInitPayload, "n")
Log.d(TAG, "[reg] step 2 response: $regInitResp")
check(regInitResp.optBoolean("success", false)) {
regInitResp.optString("reasonText", "Registration init failed")
}
Log.d(TAG, "[reg] step 3: OTP verify (C42)")
val otp = generateOtp(otpSeed)
Log.d(TAG, "[reg] generated OTP=$otp")
val otpPayload = baseData(session1, "C42").apply {
put("otp", otp)
put("uname", username)
put("otpType", "3")
}
val otpResp = doRequest(session1, otpPayload, "n")
Log.d(TAG, "[reg] step 3 response: $otpResp")
check(otpResp.optBoolean("success", false)) {
otpResp.optString("reasonText", "OTP verification failed")
}
val keyData = otpResp.getJSONArray("data").getJSONObject(0)
val key1 = keyData.getString("key1")
val key2 = keyData.getString("key2")
Log.d(TAG, "[reg] stored key1/key2 for user=$username")
prefs.edit().putString("mib_key1_$username", key1).putString("mib_key2_$username", key2).apply()
return regularLogin(username, password, appId, key1, key2)
}
// ─── Regular login ────────────────────────────────────────────────────────
private fun regularLogin(
username: String, password: String,
appId: String, key1: String, key2: String
): List<MibAccount> {
Log.d(TAG, "[login] step 4: key exchange (sfunc=i)")
val (session2, _) = initialKeyExchange(appId, key1, "i", key2)
Log.d(TAG, "[login] step 4 done: xxid=${session2.xxid}")
Log.d(TAG, "[login] step 5: getAuthType (A44)")
val userSalt = getAuthType(session2, username)
Log.d(TAG, "[login] step 5 done: userSalt length=${userSalt.length}")
Log.d(TAG, "[login] step 6: login init (A41)")
val clientSalt = randomAlpha(32)
val pgf03 = computePgf03(password, userSalt, clientSalt)
Log.d(TAG, "[login] pgf03 length=${pgf03.length}")
val loginPayload = baseData(session2, "A41").apply {
put("uname", username)
put("pgf03", pgf03)
put("clientSalt", clientSalt)
put("pmodTime", 0)
put("requireBankData", 1)
}
val loginResp = doRequest(session2, loginPayload, "n")
Log.d(TAG, "[login] step 6 response: success=${loginResp.optBoolean("success")} reasonCode=${loginResp.optString("reasonCode")} reasonText=${loginResp.optString("reasonText")}")
check(loginResp.optBoolean("success", false)) {
loginResp.optString("reasonText", "Login init failed")
}
val profiles = parseProfiles(loginResp)
Log.d(TAG, "[login] parsed ${profiles.size} profiles")
Log.d(TAG, "[login] step 7: fetch all profiles")
return fetchAllProfiles(session2, profiles)
}
// ─── Helpers ─────────────────────────────────────────────────────────────
private fun initialKeyExchange(
appId: String, encKey: String, sfunc: String, key2: String? = null
): Pair<MibSession, String> {
val innerPayload = JSONObject().apply {
put("cmod", MibCrypto.CMOD)
put("appId", appId)
put("routePath", "S40")
put("sodium", MibNonce.randomSodium())
put("xxid", MibNonce.randomXxid())
}
val encrypted = MibCrypto.encrypt(innerPayload, encKey)
val formBody = FormBody.Builder()
.add("sfunc", sfunc)
.apply { if (key2 != null) add("key2", key2) }
.add("data", encrypted)
.build()
val response = post(formBody)
Log.d(TAG, "keyExchange($sfunc) raw response (first 80): ${response.take(80)}")
val respJson = MibCrypto.decrypt(response, encKey)
Log.d(TAG, "keyExchange($sfunc) decrypted: success=${respJson.optBoolean("success")} reasonText=${respJson.optString("reasonText")}")
check(respJson.optBoolean("success", false)) {
respJson.optString("reasonText", "Key exchange failed")
}
val smod = respJson.getString("smod")
val sessionKey = MibCrypto.deriveSessionKey(smod)
val xxid = respJson.getString("xxid")
val nonceGen = respJson.getString("nonceGenerator")
return Pair(MibSession(appId, xxid, nonceGen, sessionKey), xxid)
}
private fun getAuthType(session: MibSession, username: String): String {
val payload = baseData(session, "A44").apply {
put("uname", username)
}
val resp = doRequest(session, payload, "n")
return resp.getJSONArray("data").getJSONObject(0).getString("userSalt")
}
private fun doRequest(session: MibSession, data: JSONObject, sfunc: String): JSONObject {
val routePath = data.optString("routePath", "?")
Log.d(TAG, "doRequest: routePath=$routePath xxid=${session.xxid.take(16)}...")
val encrypted = MibCrypto.encrypt(data, session.sessionKey)
val formBody = FormBody.Builder()
.add("xxid", session.xxid)
.add("sfunc", sfunc)
.add("data", encrypted)
.build()
val response = post(formBody)
Log.d(TAG, "doRequest($routePath) raw response (first 80): ${response.take(80)}")
val result = MibCrypto.decrypt(response, session.sessionKey)
Log.d(TAG, "doRequest($routePath) decrypted: success=${result.optBoolean("success")} reasonText=${result.optString("reasonText")}")
return result
}
private fun baseData(session: MibSession, routePath: String): JSONObject = JSONObject().apply {
put("nonce", MibNonce.generate(session.nonceGenerator))
put("appId", session.appId)
put("sodium", MibNonce.randomSodium())
put("routePath", routePath)
put("xxid", session.xxid)
}
private fun fetchAllProfiles(session: MibSession, profiles: List<MibProfile>): List<MibAccount> {
val allAccounts = mutableListOf<MibAccount>()
for (profile in profiles) {
val payload = baseData(session, "P47").apply {
put("profileType", profile.profileType)
put("profileId", profile.profileId)
}
val resp = doRequest(session, payload, "n")
if (!resp.optBoolean("success", false)) {
Log.w(TAG, "P47 failed for profile ${profile.name}: ${resp.optString("reasonText")}")
continue
}
val accountBalances = resp.optJSONArray("accountBalance") ?: continue
for (i in 0 until accountBalances.length()) {
val a = accountBalances.getJSONObject(i)
allAccounts.add(
MibAccount(
profileName = profile.name,
profileType = profile.profileType,
accountNumber = a.optString("accountNumber"),
accountBriefName = a.optString("accountBriefName"),
currencyName = a.optString("currencyName"),
accountTypeName = a.optString("accountTypeName"),
availableBalance = a.optString("availableBalance"),
currentBalance = a.optString("currentBalance"),
blockedAmount = a.optString("blockedAmount"),
mvrBalance = a.optString("mvrBalance"),
statusDesc = a.optString("statusDesc")
)
)
}
}
return allAccounts
}
private fun parseProfiles(loginResp: JSONObject): List<MibProfile> {
val arr = loginResp.optJSONArray("operatingProfiles") ?: return emptyList()
return (0 until arr.length()).map { i ->
val p = arr.getJSONObject(i)
MibProfile(
profileId = p.optString("profileId"),
customerProfileId = p.optString("customerProfileId"),
annexId = p.optString("annexId"),
customerId = p.optString("customerId"),
name = p.optString("name"),
cifType = p.optString("cifType"),
profileType = p.optString("profileType"),
color = p.optString("color")
)
}
}
private fun post(body: FormBody): String {
val request = Request.Builder()
.url(BASE_URL)
.post(body)
.build()
val response = client.newCall(request).execute()
return response.body?.string() ?: throw IllegalStateException("Empty response body")
}
private fun computePgf03(password: String, userSalt: String, clientSalt: String): String {
fun sha256Upper(input: String) = MessageDigest.getInstance("SHA-256")
.digest(input.toByteArray())
.joinToString("") { "%02X".format(it) }
val h1 = sha256Upper(password)
val h2 = sha256Upper(h1 + userSalt)
return sha256Upper(clientSalt + h2)
}
private fun generateOtp(seed: String): String = Totp.generate(seed)
private fun getOrCreateAppId(): String {
var id = prefs.getString("mib_app_id", null)
if (id == null) {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
id = "IOS17.2-" + (1..15).map { chars[Random.nextInt(chars.length)] }.joinToString("")
prefs.edit().putString("mib_app_id", id).apply()
}
return id
}
private fun randomAlpha(length: Int): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
return (1..length).map { chars[Random.nextInt(chars.length)] }.joinToString("")
}
}
@@ -0,0 +1,33 @@
package sh.sar.basedbank.api.mib
data class MibSession(
val appId: String,
val xxid: String,
val nonceGenerator: String,
val sessionKey: String
)
data class MibProfile(
val profileId: String,
val customerProfileId: String,
val annexId: String,
val customerId: String,
val name: String,
val cifType: String,
val profileType: String,
val color: String
)
data class MibAccount(
val profileName: String,
val profileType: String,
val accountNumber: String,
val accountBriefName: String,
val currencyName: String,
val accountTypeName: String,
val availableBalance: String,
val currentBalance: String,
val blockedAmount: String,
val mvrBalance: String,
val statusDesc: String
)
@@ -0,0 +1,79 @@
package sh.sar.basedbank.api.mib
import kotlin.math.floor
import kotlin.random.Random
object MibNonce {
/**
* Generate a nonce string from the nonceGenerator returned by the DH key exchange.
*
* Phase 1: for each group, take the first token's number * random(1-99), pad to 5 digits.
* Collect lastTwo (last 2 digits), digitSum (sum of all digits), cumSum.
* Phase 2: for each group, tokens 1-7 compute values using the operation letter.
* Each result's last 2 digits become the nonce digit and carry for next step.
*
* Operations: M=(carry%num)+ds+cs, A=carry+num+ds+cs, S=(carry^2)+num+ds+cs,
* X=(carry*num)+ds+cs, C=(carry^3)+num+ds+cs
*/
fun generate(nonceGenerator: String): String {
val groups = nonceGenerator.split("-")
val paddedList = mutableListOf<String>()
val lastTwoList = mutableListOf<Int>()
val digitSumList = mutableListOf<Int>()
var cumSum = 0
// Phase 1
for (group in groups) {
val tokens = group.trim().split(" ")
val n = tokens[0].filter { it.isDigit() }.toInt()
val r = floor(Random.nextDouble() * 99).toInt() + 1
val product = n * r
val padded = product.toString().padStart(5, '0')
val ds = padded.sumOf { it.digitToInt() }
val lt = padded.takeLast(2).toInt()
paddedList.add(padded)
lastTwoList.add(lt)
digitSumList.add(ds)
cumSum += ds
}
// Phase 2
val resultGroups = mutableListOf<String>()
for ((i, group) in groups.withIndex()) {
val tokens = group.trim().split(" ")
var carry = lastTwoList[i]
val ds = digitSumList[i]
val nonceDigits = mutableListOf<Int>()
for (j in 1..7) {
val token = tokens[j]
val op = token.filter { it.isLetter() }
val num = token.filter { it.isDigit() }.toInt()
val value: Long = when (op) {
"M" -> (carry % num).toLong() + ds + cumSum
"A" -> carry.toLong() + num + ds + cumSum
"S" -> (carry.toLong() * carry) + num + ds + cumSum
"X" -> carry.toLong() * num + ds + cumSum
"C" -> (carry.toLong() * carry * carry) + num + ds + cumSum
else -> 0L
}
val digit = value.toString().takeLast(2).toInt()
nonceDigits.add(digit)
carry = digit
}
val groupStr = paddedList[i] + " " + nonceDigits.joinToString(" ") {
it.toString().padStart(2, '0')
}
resultGroups.add(groupStr)
}
return resultGroups.joinToString("-")
}
fun randomSodium(): String = (Random.nextLong(1_000_000L, 16_000_000L)).toString()
fun randomXxid(): String = Random.nextLong(0L, (1L shl 40)).toString()
}
@@ -1,42 +0,0 @@
package sh.sar.basedbank.ui.gallery
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import sh.sar.basedbank.databinding.FragmentGalleryBinding
class GalleryFragment : Fragment() {
private var _binding: FragmentGalleryBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val galleryViewModel =
ViewModelProvider(this).get(GalleryViewModel::class.java)
_binding = FragmentGalleryBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textGallery
galleryViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -1,13 +0,0 @@
package sh.sar.basedbank.ui.gallery
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class GalleryViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is gallery Fragment"
}
val text: LiveData<String> = _text
}
@@ -0,0 +1,75 @@
package sh.sar.basedbank.ui.home
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.databinding.ItemAccountBinding
import sh.sar.basedbank.databinding.ItemProfileHeaderBinding
class AccountsAdapter(private val accounts: List<MibAccount>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private sealed class Item {
data class Header(val profileName: String, val profileType: String) : Item()
data class Account(val account: MibAccount) : Item()
}
private val items: List<Item> = buildList {
var lastProfile = ""
for (account in accounts) {
if (account.profileName != lastProfile) {
add(Item.Header(account.profileName, account.profileType))
lastProfile = account.profileName
}
add(Item.Account(account))
}
}
override fun getItemViewType(position: Int) = when (items[position]) {
is Item.Header -> TYPE_HEADER
is Item.Account -> TYPE_ACCOUNT
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return if (viewType == TYPE_HEADER) {
HeaderViewHolder(ItemProfileHeaderBinding.inflate(inflater, parent, false))
} else {
AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = items[position]) {
is Item.Header -> (holder as HeaderViewHolder).bind(item)
is Item.Account -> (holder as AccountViewHolder).bind(item.account)
}
}
override fun getItemCount() = items.size
private inner class HeaderViewHolder(private val binding: ItemProfileHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item.Header) {
binding.tvProfileName.text = item.profileName
binding.tvProfileType.text = if (item.profileType == "0") "Personal" else "Business"
}
}
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: MibAccount) {
binding.tvAccountName.text = account.accountBriefName
binding.tvAccountNumber.text = account.accountNumber
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
binding.tvAccountType.text = account.accountTypeName
binding.tvStatus.text = account.statusDesc
}
}
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ACCOUNT = 1
}
}
@@ -0,0 +1,26 @@
package sh.sar.basedbank.ui.home
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.databinding.ActivityHomeBinding
class HomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityHomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val accounts = (application as BasedBankApp).accounts
val adapter = AccountsAdapter(accounts)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
}
}
@@ -1,42 +0,0 @@
package sh.sar.basedbank.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import sh.sar.basedbank.databinding.FragmentHomeBinding
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textHome
homeViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -1,13 +0,0 @@
package sh.sar.basedbank.ui.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class HomeViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is home Fragment"
}
val text: LiveData<String> = _text
}
@@ -0,0 +1,32 @@
package sh.sar.basedbank.ui.login
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentBankSelectionBinding
class BankSelectionFragment : Fragment() {
private var _binding: FragmentBankSelectionBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentBankSelectionBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.cardMib.setOnClickListener {
findNavController().navigate(R.id.action_bankSelection_to_credentials)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,129 @@
package sh.sar.basedbank.ui.login
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.util.Totp
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
import sh.sar.basedbank.ui.home.HomeActivity
class CredentialsFragment : Fragment() {
private val TAG = "CredentialsFragment"
private val otpHandler = Handler(Looper.getMainLooper())
private val otpRunnable = object : Runnable {
override fun run() {
updateOtpDisplay()
otpHandler.postDelayed(this, 1000)
}
}
private var _binding: FragmentCredentialsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCredentialsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.btnLogin.setOnClickListener { attemptLogin() }
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { updateOtpDisplay() }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
override fun onResume() {
super.onResume()
otpHandler.post(otpRunnable)
}
override fun onPause() {
super.onPause()
otpHandler.removeCallbacks(otpRunnable)
}
private fun updateOtpDisplay() {
val seed = binding.etOtpSeed.text.toString().trim()
if (seed.isEmpty()) {
binding.cardOtp.visibility = View.GONE
return
}
try {
val otp = Totp.generate(seed)
val secondsInPeriod = (System.currentTimeMillis() / 1000L % 30).toInt()
val remaining = 30 - secondsInPeriod
binding.tvOtpCode.text = otp
binding.otpTimer.max = 30
binding.otpTimer.progress = remaining
binding.cardOtp.visibility = View.VISIBLE
} catch (e: Exception) {
binding.cardOtp.visibility = View.GONE
}
}
private fun attemptLogin() {
val username = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString()
val otpSeed = binding.etOtpSeed.text.toString().trim()
Log.d(TAG, "Login button pressed for username=$username")
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
Log.w(TAG, "Validation failed: empty fields")
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 prefs = requireContext().getSharedPreferences("mib_prefs", android.content.Context.MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
viewLifecycleOwner.lifecycleScope.launch {
try {
Log.d(TAG, "Starting login flow on IO dispatcher")
val accounts = withContext(Dispatchers.IO) {
flow.login(username, password, otpSeed)
}
Log.d(TAG, "Login succeeded, got ${accounts.size} accounts")
(requireActivity().application as BasedBankApp).accounts = accounts
startActivity(Intent(requireContext(), HomeActivity::class.java))
requireActivity().finish()
} catch (e: Exception) {
Log.e(TAG, "Login failed: ${e.message}", e)
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() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,18 @@
package sh.sar.basedbank.ui.login
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.ActivityLoginBinding
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
@@ -0,0 +1,51 @@
package sh.sar.basedbank.ui.onboarding
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import sh.sar.basedbank.databinding.ActivityOnboardingBinding
import sh.sar.basedbank.ui.login.LoginActivity
class OnboardingActivity : AppCompatActivity() {
private lateinit var binding: ActivityOnboardingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityOnboardingBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = OnboardingPagerAdapter(this)
binding.viewPager.adapter = adapter
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach()
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
updateButtons(position, adapter.itemCount)
}
})
updateButtons(0, adapter.itemCount)
binding.btnNext.setOnClickListener {
val next = binding.viewPager.currentItem + 1
if (next < adapter.itemCount) binding.viewPager.currentItem = next
}
binding.btnGetStarted.setOnClickListener {
getSharedPreferences("prefs", MODE_PRIVATE)
.edit().putBoolean("onboarding_done", true).apply()
startActivity(Intent(this, LoginActivity::class.java))
finish()
}
}
private fun updateButtons(position: Int, count: Int) {
val isLast = position == count - 1
binding.btnNext.visibility = if (isLast) View.GONE else View.VISIBLE
binding.btnGetStarted.visibility = if (isLast) View.VISIBLE else View.GONE
}
}
@@ -0,0 +1,55 @@
package sh.sar.basedbank.ui.onboarding
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import sh.sar.basedbank.databinding.FragmentOnboardingSlideBinding
class OnboardingFragment : Fragment() {
private var _binding: FragmentOnboardingSlideBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentOnboardingSlideBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val title = requireArguments().getString(ARG_TITLE, "")
val desc = requireArguments().getString(ARG_DESC, "")
val icon = requireArguments().getInt(ARG_ICON, 0)
val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false)
binding.icon.setImageResource(icon)
binding.title.text = title
binding.description.text = desc
// On the first slide, show the two placeholder cards for upcoming banks
binding.placeholderCards.visibility = if (isFirst) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val ARG_TITLE = "title"
private const val ARG_DESC = "desc"
private const val ARG_ICON = "icon"
private const val ARG_IS_FIRST = "is_first"
fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply {
arguments = bundleOf(
ARG_TITLE to slide.title,
ARG_DESC to slide.description,
ARG_ICON to slide.iconRes,
ARG_IS_FIRST to slide.isFirst
)
}
}
}
@@ -0,0 +1,42 @@
package sh.sar.basedbank.ui.onboarding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import sh.sar.basedbank.R
class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
private val slides = listOf(
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_1),
description = activity.getString(R.string.onboarding_desc_1),
iconRes = R.drawable.ic_launcher_foreground,
isFirst = true
),
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_2),
description = activity.getString(R.string.onboarding_desc_2),
iconRes = R.drawable.ic_launcher_foreground,
isFirst = false
),
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_3),
description = activity.getString(R.string.onboarding_desc_3),
iconRes = R.drawable.ic_launcher_foreground,
isFirst = false
)
)
override fun getItemCount() = slides.size
override fun createFragment(position: Int): Fragment =
OnboardingFragment.newInstance(slides[position])
}
data class OnboardingSlide(
val title: String,
val description: String,
val iconRes: Int,
val isFirst: Boolean
)
@@ -1,42 +0,0 @@
package sh.sar.basedbank.ui.slideshow
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import sh.sar.basedbank.databinding.FragmentSlideshowBinding
class SlideshowFragment : Fragment() {
private var _binding: FragmentSlideshowBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val slideshowViewModel =
ViewModelProvider(this).get(SlideshowViewModel::class.java)
_binding = FragmentSlideshowBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textSlideshow
slideshowViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -1,13 +0,0 @@
package sh.sar.basedbank.ui.slideshow
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SlideshowViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is slideshow Fragment"
}
val text: LiveData<String> = _text
}
@@ -0,0 +1,56 @@
package sh.sar.basedbank.util
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.and
object Totp {
/**
* Generate a 6-digit TOTP code from a Base32-encoded secret (RFC 6238 / RFC 4226).
* Uses HmacSHA1, 30-second window, 6 digits — matching standard authenticator apps.
*/
fun generate(base32Secret: String, digits: Int = 6, periodSeconds: Long = 30): String {
val key = base32Decode(base32Secret.uppercase().replace(" ", "").replace("-", ""))
val counter = System.currentTimeMillis() / 1000L / periodSeconds
val otp = hotp(key, counter, digits)
return otp.toString().padStart(digits, '0')
}
private fun hotp(key: ByteArray, counter: Long, digits: Int): Int {
val msg = ByteArray(8) { i -> ((counter shr ((7 - i) * 8)) and 0xFF).toByte() }
val mac = Mac.getInstance("HmacSHA1")
mac.init(SecretKeySpec(key, "HmacSHA1"))
val hash = mac.doFinal(msg)
val offset = (hash[hash.size - 1] and 0x0F).toInt()
val code = ((hash[offset].toInt() and 0x7F) shl 24) or
((hash[offset + 1].toInt() and 0xFF) shl 16) or
((hash[offset + 2].toInt() and 0xFF) shl 8) or
(hash[offset + 3].toInt() and 0xFF)
val modulus = Math.pow(10.0, digits.toDouble()).toInt()
return code % modulus
}
private val BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
private fun base32Decode(input: String): ByteArray {
val clean = input.trimEnd('=')
val output = ByteArray(clean.length * 5 / 8)
var buffer = 0
var bitsLeft = 0
var outIndex = 0
for (c in clean) {
val v = BASE32_CHARS.indexOf(c)
require(v >= 0) { "Invalid Base32 character: $c" }
buffer = (buffer shl 5) or v
bitsLeft += 5
if (bitsLeft >= 8) {
bitsLeft -= 8
output[outIndex++] = ((buffer shr bitsLeft) and 0xFF).toByte()
}
}
return output
}
}
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%" android:toXDelta="0%"
android:duration="300" android:interpolator="@android:interpolator/decelerate_cubic" />
</set>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%" android:toXDelta="0%"
android:duration="300" android:interpolator="@android:interpolator/decelerate_cubic" />
</set>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0%" android:toXDelta="-100%"
android:duration="300" android:interpolator="@android:interpolator/decelerate_cubic" />
</set>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0%" android:toXDelta="100%"
android:duration="300" android:interpolator="@android:interpolator/decelerate_cubic" />
</set>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="100dp" />
<solid android:color="?attr/colorSecondaryContainer" />
</shape>
@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zm3,15c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z" />
</vector>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="138dp"
android:height="40dp"
android:viewportWidth="276"
android:viewportHeight="80">
<!-- Blue paths (fil0: #3F65AD) -->
<path
android:fillColor="#3F65AD"
android:pathData="M272.33 28.36l-12.05 0 0 -6.16c0,-1 -0.36,-1.87 -1.08,-2.59 -0.73,-0.73 -1.61,-1.09 -2.63,-1.08 -0.95,0 -1.87,0.41 -2.52,1.11 -0.67,0.71 -1.01,1.58 -1.01,2.56l0 6.16 -4.07 0c-4.95,0 -4.66,6.64 0,6.64l4.07 0 0 27.8c0,5.47 1.19,9.74 3.53,12.67 2.41,3.01 6.48,4.53 12.11,4.53l3.65 0c1.72,0 3.16,-1.2 3.54,-2.81l0 -1.63c-0.38,-1.61 -1.82,-2.81 -3.54,-2.81l-3.65 0c-3.2,0 -5.41,-0.79 -6.56,-2.35 -1.22,-1.65 -1.84,-4.22 -1.84,-7.6l0 -27.8 12.05 0c4.74,0 4.74,-6.64 0,-6.64z" />
<path
android:fillColor="#3F65AD"
android:pathData="M169.85 76.38l0 -41.07 15.31 0c0.74,0 1.47,0.08 2.18,0.27 1.63,0.46 3.04,1.32 4.16,2.55 0.82,0.91 1.49,2.02 1.99,3.29 0.5,1.27 0.76,2.7 0.76,4.24l0 30.72c0,2 1.62,3.62 3.62,3.62l0 0c2,0 3.63,-1.62 3.63,-3.62l0 -30.72c0,-2.72 -0.44,-5.17 -1.3,-7.3 -0.86,-2.14 -2.07,-3.98 -3.59,-5.47 -1.53,-1.49 -3.3,-2.63 -5.27,-3.39 -1.97,-0.76 -4.04,-1.14 -6.17,-1.14l-17.43 0c-1.54,0 -2.8,0.54 -3.75,1.61 -0.92,1.04 -1.38,2.26 -1.38,3.62l0 42.79c0,2 1.62,3.62 3.62,3.62l0 0c2,0 3.62,-1.62 3.62,-3.62z" />
<path
android:fillColor="#3F65AD"
android:pathData="M225.3 28.36c15.78,0 18.49,6.01 19.17,19.96 0.27,5.45 -0.89,8.87 -5.93,9.27l-17.03 0c-1.8,0 -3.27,-1.51 -3.27,-3.37 0,-1.86 1.47,-3.37 3.27,-3.37l0.77 0 12.95 0 2.56 0c0,-9.79 0.21,-16.11 -12.49,-15.54 -3.89,0 -6.8,1.13 -8.92,3.46 -2.11,2.32 -3.18,5.53 -3.18,9.52l0.03 9.51c0.09,2.35 0.32,4.45 0.68,6.25 0.39,2 1.04,3.69 1.93,5.03 0.87,1.29 2.03,2.28 3.47,2.94 1.47,0.68 3.39,1.02 5.69,1.02l2 0 4.51 0 3.48 0c1.77,0.01 3.21,1.54 3.21,3.38 0,1.83 -1.44,3.58 -3.21,3.58l-3.48 0 -4.51 0 -2.09 0c-3.63,0 -6.69,-0.5 -9.11,-1.49 -2.48,-1.01 -4.48,-2.57 -5.96,-4.64 -1.44,-2.03 -2.47,-4.6 -3.04,-7.62 -0.57,-2.94 -0.85,-6.42 -0.85,-10.37l0 -7.59c0,-3.11 0.5,-5.94 1.49,-8.42 0.99,-2.49 2.38,-4.61 4.14,-6.3 1.75,-1.68 3.81,-2.99 6.14,-3.88 2.32,-0.88 4.87,-1.33 7.58,-1.33z" />
<!-- Green paths (fil1: #9AD141) -->
<path
android:fillColor="#9AD141"
android:pathData="M72.13 28.36c-1.86,0 -3.37,1.55 -3.37,3.47l0 44.7c0,1.92 1.51,3.47 3.37,3.47 1.86,0 3.36,-1.55 3.36,-3.47l0 -44.7c0,-1.92 -1.5,-3.47 -3.36,-3.47z" />
<path
android:fillColor="#9AD141"
android:pathData="M62.69 37.55c-0.67,-1.86 -1.63,-3.49 -2.84,-4.84 -1.21,-1.36 -2.68,-2.43 -4.37,-3.2 -1.68,-0.76 -3.56,-1.15 -5.57,-1.15l-12.86 0 0 0c-1.78,0 -3.22,1.49 -3.22,3.32 0,1.83 1.44,3.32 3.22,3.32l12.86 0c2.22,0 3.9,0.81 5.13,2.46 1.28,1.7 1.92,3.84 1.92,6.38l0 29.32 -15.7 0c-1.09,0 -2.13,-0.21 -3.1,-0.62 -0.97,-0.41 -1.82,-0.99 -2.53,-1.72 -0.71,-0.74 -1.28,-1.6 -1.68,-2.57 -0.39,-0.96 -0.59,-2.02 -0.59,-3.14 0,-1.12 0.2,-2.2 0.6,-3.2 0.4,-0.99 0.96,-1.87 1.67,-2.61 0.72,-0.73 1.57,-1.33 2.55,-1.78 0.96,-0.43 2,-0.66 3.08,-0.66l7.38 0 0 0.01c1.8,0 3.27,-1.52 3.27,-3.38 0,-1.86 -1.47,-3.37 -3.27,-3.37l-7.38 0c-2.01,0 -3.93,0.4 -5.71,1.2 -1.77,0.79 -3.33,1.88 -4.63,3.22 -1.31,1.35 -2.35,2.95 -3.09,4.74 -0.73,1.8 -1.11,3.76 -1.11,5.83 0,2.07 0.38,4.03 1.11,5.83 0.74,1.79 1.78,3.38 3.09,4.74 1.31,1.35 2.87,2.42 4.64,3.18 1.77,0.76 3.69,1.14 5.7,1.14l17.78 0c1.31,0 2.42,-0.47 3.31,-1.38 0.89,-0.92 1.34,-2.07 1.34,-3.41l0 -31.67c0,-2.11 -0.34,-4.13 -1,-5.99z" />
<path
android:fillColor="#9AD141"
android:pathData="M26.66 28.36l-9.83 0 0 -7.66 0 0c0,-1.08 0,-1.83 0,-1.83 0,-3.31 1.04,-6.76 3.05,-9.42 1.11,-1.47 2.8,-2.4 4.63,-2.77 1.27,-0.25 5.79,-0.34 7.59,-0.35l0 0c1.75,0 3.16,-1.42 3.16,-3.17 0,-1.75 -1.41,-3.16 -3.16,-3.16 -3.39,0.02 -9.41,0.24 -12.15,1.58 -3.98,1.94 -7.03,5.42 -8.56,9.56 -0.85,2.28 -1.27,4.73 -1.27,7.34l0 9.88 -6.84 0c-1.75,0 -3.17,1.42 -3.17,3.16 0,1.75 1.42,3.48 3.17,3.48l0 0 6.84 0 0 41.64c0,1.86 1.5,3.36 3.36,3.36 1.85,0 3.35,-1.5 3.35,-3.36l0 -41.64 9.83 0 0 0.01c1.75,0 3.17,-1.74 3.17,-3.48 0,-1.75 -1.42,-3.17 -3.17,-3.17z" />
<path
android:fillColor="#9AD141"
android:pathData="M156.53 37.55c-0.67,-1.86 -1.63,-3.49 -2.84,-4.84 -1.21,-1.36 -2.68,-2.43 -4.37,-3.2 -1.68,-0.76 -3.56,-1.15 -5.57,-1.15l-12.86 0 0 0c-1.78,0 -3.21,1.49 -3.21,3.32 0,1.83 1.43,3.32 3.21,3.32l12.86 0c2.22,0 3.9,0.81 5.14,2.46 1.27,1.7 1.91,3.84 1.91,6.38l0 29.32 -15.7 0c-1.09,0 -2.13,-0.21 -3.1,-0.62 -0.97,-0.41 -1.82,-0.99 -2.53,-1.72 -0.71,-0.74 -1.27,-1.6 -1.67,-2.57 -0.4,-0.96 -0.6,-2.02 -0.6,-3.14 0,-1.12 0.2,-2.2 0.6,-3.2 0.4,-0.99 0.96,-1.87 1.67,-2.61 0.72,-0.73 1.58,-1.33 2.55,-1.78 0.96,-0.43 2,-0.66 3.08,-0.66l7.38 0 0 0.01c1.81,0 3.27,-1.52 3.27,-3.38 0,-1.86 -1.46,-3.37 -3.27,-3.37l-7.38 0c-2.01,0 -3.93,0.4 -5.71,1.2 -1.77,0.79 -3.33,1.88 -4.63,3.22 -1.31,1.35 -2.35,2.95 -3.08,4.74 -0.74,1.8 -1.12,3.76 -1.12,5.83 0,2.07 0.38,4.03 1.12,5.83 0.73,1.79 1.77,3.38 3.08,4.74 1.31,1.35 2.87,2.42 4.64,3.18 1.78,0.76 3.69,1.14 5.7,1.14l17.78 0c1.31,0 2.42,-0.47 3.31,-1.38 0.89,-0.92 1.34,-2.07 1.34,-3.41l0 -31.67c0,-2.11 -0.33,-4.13 -1,-5.99z" />
<path
android:fillColor="#9AD141"
android:pathData="M112.72 54.91c-2.71,-2.17 -6.46,-3.62 -11.16,-4.29l-3.62 -0.49c-3.92,-0.51 -6.78,-1.31 -8.51,-2.38 -1.58,-0.98 -2.35,-2.65 -2.35,-5.09 0,-2.76 0.9,-4.65 2.73,-5.77 1.95,-1.19 4.4,-1.89 7.34,-1.89l15.72 0.01 0 0c1.78,0 3.22,-1.48 3.22,-3.32 0,-1.83 -1.44,-3.32 -3.22,-3.32l-15.72 -0.01c-2.21,0 -4.33,0.35 -6.36,0.85 -2.06,0.52 -3.88,1.36 -5.42,2.5 -1.56,1.15 -2.83,2.67 -3.78,4.51 -0.94,1.85 -1.43,4.09 -1.43,6.64 0,4.73 1.51,8.17 4.47,10.23 2.82,1.97 6.79,3.23 11.78,3.77l4.07 0.49c3.13,0.43 5.55,1.33 7.2,2.66 1.58,1.28 2.34,2.91 2.34,5 0,3.16 -0.98,5.27 -2.99,6.45 -2.15,1.26 -5.11,1.9 -8.81,1.9l-14.83 0 0 0c-1.78,0 -3.23,1.48 -3.23,3.32 0,1.83 1.45,3.32 3.23,3.32l14.83 0c2.71,0 5.21,-0.24 7.44,-0.7 2.29,-0.48 4.29,-1.32 5.93,-2.49 1.66,-1.19 2.98,-2.77 3.93,-4.69 0.94,-1.91 1.42,-4.33 1.42,-7.2 0,-4.39 -1.42,-7.75 -4.22,-10.01z" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@@ -1,9 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<layer-list>
<item android:gravity="center" android:width="10dp" android:height="10dp">
<shape android:shape="oval">
<solid android:color="?attr/colorPrimary" />
</shape>
</item>
</layer-list>
</item>
<item>
<layer-list>
<item android:gravity="center" android:width="8dp" android:height="8dp">
<shape android:shape="oval">
<solid android:color="?attr/colorOutline" />
</shape>
</item>
</layer-list>
</item>
</selector>
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/recyclerView">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/accounts"
app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingBottom="16dp"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/navHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/login_nav"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
-25
View File
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
android:id="@+id/app_bar_main"
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottomBar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:id="@+id/bottomBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingBottom="40dp"
android:paddingTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/dotsIndicator"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:minHeight="0dp"
android:background="@null"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"
app:tabBackground="@drawable/tab_indicator_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"
app:tabMaxWidth="16dp"
app:tabMinWidth="16dp"
app:tabPaddingStart="3dp"
app:tabPaddingEnd="3dp"
app:tabPaddingTop="0dp"
app:tabPaddingBottom="0dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNext"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="@string/next"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnGetStarted"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="@string/get_started"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
-34
View File
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.BasedBank.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.BasedBank.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
-20
View File
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/app_bar_main">
<fragment
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingTop="48dp"
android:paddingBottom="32dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/select_bank"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/select_bank_desc"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="32dp" />
<!-- MIB Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardMib"
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/mib_faisanet_logo"
android:contentDescription="@string/mib_name"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mib_name"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mib_desc"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginTop="4dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingTop="48dp"
android:paddingBottom="32dp">
<ImageView
android:layout_width="138dp"
android:layout_height="40dp"
android:src="@drawable/mib_faisanet_logo"
android:contentDescription="@string/mib_name"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="32dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sign_in"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sign_in_desc"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="32dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username"
android:layout_marginBottom="16dp"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:imeOptions="actionNext"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:layout_marginBottom="16dp"
app:endIconMode="password_toggle"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:imeOptions="actionNext"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/otp_seed"
android:layout_marginBottom="8dp"
app:endIconMode="password_toggle"
app:helperText="@string/otp_seed_hint"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etOtpSeed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:imeOptions="actionDone"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardOtp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Current OTP"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSecondaryContainer" />
<TextView
android:id="@+id/tvOtpCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textColor="?attr/colorOnSecondaryContainer"
android:letterSpacing="0.15"
android:fontFamily="monospace" />
</LinearLayout>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/otpTimer"
android:layout_width="32dp"
android:layout_height="32dp"
app:indicatorSize="32dp"
app:trackThickness="3dp"
app:indicatorColor="?attr/colorOnSecondaryContainer"
app:trackColor="?attr/colorSecondaryContainer"
android:indeterminate="false" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/tvError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textColor="?attr/colorError"
android:textAppearance="?attr/textAppearanceBodySmall"
android:visibility="gone" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:trackCornerRadius="4dp"
android:indeterminate="true" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="@string/login"
android:textSize="16sp"
android:layout_marginTop="8dp" />
</LinearLayout>
</ScrollView>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.gallery.GalleryFragment">
<TextView
android:id="@+id/text_gallery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment">
<TextView
android:id="@+id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingHorizontal="32dp"
android:paddingTop="64dp"
android:paddingBottom="16dp">
<ImageView
android:id="@+id/icon"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginBottom="40dp"
android:contentDescription="@string/app_name" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnSurface"
android:gravity="center"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center"
android:lineSpacingMultiplier="1.4" />
<!-- Placeholder bank cards shown only on first slide -->
<LinearLayout
android:id="@+id/placeholderCards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="40dp"
android:weightSum="2"
android:visibility="gone">
<com.google.android.material.card.MaterialCardView
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/coming_soon"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/coming_soon"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</LinearLayout>
</ScrollView>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.slideshow.SlideshowFragment">
<TextView
android:id="@+id/text_slideshow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+107
View File
@@ -0,0 +1,107 @@
<?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="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutlineVariant">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvAccountName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvAccountNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginTop="2dp" />
</LinearLayout>
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="4dp"
android:background="@drawable/chip_background"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorOutlineVariant"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/available_balance"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvBalance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginTop="2dp" />
</LinearLayout>
<TextView
android:id="@+id/tvAccountType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvProfileName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvProfileType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="4dp"
android:background="@drawable/chip_background"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar"
android:gravity="bottom"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/nav_header_desc"
android:paddingTop="@dimen/nav_header_vertical_spacing"
app:srcCompat="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="@string/nav_header_title"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_header_subtitle" />
</LinearLayout>
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_menu_camera"
android:title="@string/menu_home" />
<item
android:id="@+id/nav_gallery"
android:icon="@drawable/ic_menu_gallery"
android:title="@string/menu_gallery" />
<item
android:id="@+id/nav_slideshow"
android:icon="@drawable/ic_menu_slideshow"
android:title="@string/menu_slideshow" />
</group>
</menu>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/login_nav"
app:startDestination="@id/bankSelectionFragment">
<fragment
android:id="@+id/bankSelectionFragment"
android:name="sh.sar.basedbank.ui.login.BankSelectionFragment"
android:label="Select Bank">
<action
android:id="@+id/action_bankSelection_to_credentials"
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" />
</fragment>
<fragment
android:id="@+id/credentialsFragment"
android:name="sh.sar.basedbank.ui.login.CredentialsFragment"
android:label="Sign In" />
</navigation>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/nav_home">
<fragment
android:id="@+id/nav_home"
android:name="sh.sar.basedbank.ui.home.HomeFragment"
android:label="@string/menu_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/nav_gallery"
android:name="sh.sar.basedbank.ui.gallery.GalleryFragment"
android:label="@string/menu_gallery"
tools:layout="@layout/fragment_gallery" />
<fragment
android:id="@+id/nav_slideshow"
android:name="sh.sar.basedbank.ui.slideshow.SlideshowFragment"
android:label="@string/menu_slideshow"
tools:layout="@layout/fragment_slideshow" />
</navigation>
+7 -15
View File
@@ -1,16 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.BasedBank" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<resources>
<!-- Night mode inherits from base theme; DynamicColors handles the rest -->
<style name="Theme.BasedBank" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/seed_primary</item>
<item name="colorSecondary">@color/seed_secondary</item>
<item name="android:windowSoftInputMode">adjustResize</item>
</style>
</resources>
</resources>
+4 -8
View File
@@ -1,10 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
<!-- Seed colors from MIB brand (blue + green) — used as M3 fallback on Android <12 -->
<color name="seed_primary">#3F65AD</color>
<color name="seed_secondary">#9AD141</color>
</resources>
+29 -11
View File
@@ -1,13 +1,31 @@
<resources>
<string name="app_name">Based Bank</string>
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>
<string name="nav_header_title">Android Studio</string>
<string name="nav_header_subtitle">android.studio@android.com</string>
<string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string>
<string name="app_name">BasedBank</string>
<string name="menu_home">Home</string>
<string name="menu_gallery">Gallery</string>
<string name="menu_slideshow">Slideshow</string>
</resources>
<!-- Onboarding -->
<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_title_2">More Banks Coming</string>
<string name="onboarding_desc_2">Support for additional banks is on the way. Stay tuned as we expand coverage across the Maldives.</string>
<string name="onboarding_title_3">Get Started</string>
<string name="onboarding_desc_3">Add your bank credentials and start viewing your accounts. Your data stays on your device.</string>
<string name="coming_soon">Coming Soon</string>
<string name="next">Next</string>
<string name="get_started">Get Started</string>
<!-- Login -->
<string name="select_bank">Select Your Bank</string>
<string name="select_bank_desc">Choose the bank you want to sign in to.</string>
<string name="mib_name">Maldives Islamic Bank</string>
<string name="mib_desc">Faisanet Mobile Banking</string>
<string name="sign_in">Sign In</string>
<string name="sign_in_desc">Enter your Maldives Islamic Bank credentials.</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="otp_seed">OTP Seed (TOTP Secret)</string>
<string name="otp_seed_hint">The Base32 secret from your authenticator setup</string>
<string name="login">Login</string>
<!-- Home -->
<string name="accounts">Accounts</string>
<string name="available_balance">Available Balance</string>
</resources>
+5 -23
View File
@@ -1,25 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.BasedBank" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<style name="Theme.BasedBank" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/seed_primary</item>
<item name="colorSecondary">@color/seed_secondary</item>
<item name="android:windowSoftInputMode">adjustResize</item>
</style>
<style name="Theme.BasedBank.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.BasedBank.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.BasedBank.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">faisanet.mib.com.mv</domain>
</domain-config>
</network-security-config>