Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
89a9731797
|
|||
|
50badc7d54
|
|||
|
d4f86bb738
|
|||
|
b35f44f35b
|
|||
|
1a58ce8b54
|
|||
|
2dd84ec50a
|
|||
|
33651ca107
|
|||
|
ae307e3118
|
|||
|
3ab75bff92
|
|||
|
1753d648bd
|
|||
|
423b0bf1e1
|
|||
|
d713047970
|
|||
|
d59a6fad82
|
|||
|
8e47101401
|
|||
|
9431a90cd0
|
|||
|
3a10f36c39
|
17
.idea/deploymentTargetSelector.xml
generated
17
.idea/deploymentTargetSelector.xml
generated
@@ -3,7 +3,7 @@
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<option name="selectionMode" value="DIALOG" />
|
||||
<DropdownSelection timestamp="2026-05-15T13:54:16.798188666Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
@@ -11,7 +11,20 @@
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
<DialogSelection>
|
||||
<targets>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" />
|
||||
</handle>
|
||||
</Target>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=683a9830" />
|
||||
</handle>
|
||||
</Target>
|
||||
</targets>
|
||||
</DialogSelection>
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "sh.sar.basedbank"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
versionCode = 2
|
||||
versionName = "1.0.3"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -64,6 +64,9 @@ dependencies {
|
||||
// RecyclerView for accounts list
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
|
||||
// CircularProgressDrawable for spinning search icons
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
// OkHttp for API calls
|
||||
implementation("com.squareup.okhttp3:okhttp:4.11.0")
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeActivity"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustPan" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.QrScannerActivity"
|
||||
|
||||
@@ -10,6 +10,9 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -18,8 +21,6 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.databinding.ActivityLockBinding
|
||||
import sh.sar.basedbank.ui.home.HomeActivity
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
|
||||
@@ -31,7 +32,6 @@ class LockActivity : AppCompatActivity() {
|
||||
private lateinit var salt: String
|
||||
private lateinit var storedHash: String
|
||||
private var biometricsEnabled = false
|
||||
private var isLegacyFormat = false
|
||||
private var isVerifying = false
|
||||
|
||||
private val lockPrefs get() = getSharedPreferences("lock_attempts", MODE_PRIVATE)
|
||||
@@ -39,28 +39,32 @@ class LockActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val MAX_ATTEMPTS = 5
|
||||
private const val LOCKOUT_MS = 30_000L
|
||||
const val EXTRA_RESUME = "resume"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = ActivityLockBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val isLight = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = isLight
|
||||
isAppearanceLightNavigationBars = isLight
|
||||
}
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
|
||||
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||
method = prefs.getString("security_method", "pin") ?: "pin"
|
||||
biometricsEnabled = prefs.getBoolean("biometrics_enabled", false)
|
||||
|
||||
// Try new encrypted format first; fall back to legacy SHA-256
|
||||
val stored = CredentialStore(this).loadSecurityHash()
|
||||
if (stored != null) {
|
||||
salt = stored.first
|
||||
storedHash = stored.second
|
||||
isLegacyFormat = false
|
||||
} else {
|
||||
salt = prefs.getString("security_salt", "") ?: ""
|
||||
storedHash = prefs.getString("security_hash", "") ?: ""
|
||||
isLegacyFormat = true
|
||||
}
|
||||
val stored = CredentialStore(this).loadSecurityHash() ?: run { finish(); return }
|
||||
salt = stored.first
|
||||
storedHash = stored.second
|
||||
|
||||
if (method == "pin") {
|
||||
binding.viewPin.visibility = View.VISIBLE
|
||||
@@ -149,7 +153,6 @@ class LockActivity : AppCompatActivity() {
|
||||
val ok = withContext(Dispatchers.Default) { verify(entered) }
|
||||
isVerifying = false
|
||||
if (ok) {
|
||||
migrateIfNeeded(entered)
|
||||
resetFailures()
|
||||
proceed()
|
||||
} else {
|
||||
@@ -174,7 +177,6 @@ class LockActivity : AppCompatActivity() {
|
||||
val ok = withContext(Dispatchers.Default) { verify(entered) }
|
||||
isVerifying = false
|
||||
if (ok) {
|
||||
migrateIfNeeded(entered)
|
||||
resetFailures()
|
||||
proceed()
|
||||
} else {
|
||||
@@ -216,30 +218,8 @@ class LockActivity : AppCompatActivity() {
|
||||
|
||||
private fun verify(input: String): Boolean {
|
||||
if (storedHash.isBlank()) return false
|
||||
return if (isLegacyFormat) {
|
||||
sha256Legacy(salt + input) == storedHash
|
||||
} else {
|
||||
val saltBytes = Base64.decode(salt, Base64.NO_WRAP)
|
||||
pbkdf2(input, saltBytes) == storedHash
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On the first successful unlock after legacy SHA-256 format is detected,
|
||||
* transparently migrate to PBKDF2 + CredentialStore.
|
||||
*/
|
||||
private fun migrateIfNeeded(input: String) {
|
||||
if (!isLegacyFormat) return
|
||||
try {
|
||||
val newSalt = ByteArray(16).also { SecureRandom().nextBytes(it) }
|
||||
val newHash = pbkdf2(input, newSalt)
|
||||
val saltB64 = Base64.encodeToString(newSalt, Base64.NO_WRAP)
|
||||
CredentialStore(this).saveSecurityHash(saltB64, newHash)
|
||||
// Remove legacy plaintext fields
|
||||
getSharedPreferences("prefs", MODE_PRIVATE).edit()
|
||||
.remove("security_salt").remove("security_hash").apply()
|
||||
isLegacyFormat = false
|
||||
} catch (_: Exception) { /* migration will retry next unlock */ }
|
||||
val saltBytes = Base64.decode(salt, Base64.NO_WRAP)
|
||||
return pbkdf2(input, saltBytes) == storedHash
|
||||
}
|
||||
|
||||
private fun triggerBiometric() {
|
||||
@@ -270,8 +250,12 @@ class LockActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun proceed() {
|
||||
startActivity(Intent(this, HomeActivity::class.java))
|
||||
finish()
|
||||
if (intent.getBooleanExtra(EXTRA_RESUME, false)) {
|
||||
finish()
|
||||
} else {
|
||||
startActivity(Intent(this, HomeActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Brute-force tracking ──────────────────────────────────────────────────
|
||||
@@ -308,9 +292,4 @@ class LockActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Legacy: raw SHA-256(salt + input) — only used for migration path. */
|
||||
private fun sha256Legacy(input: String) = MessageDigest.getInstance("SHA-256")
|
||||
.digest(input.toByteArray()).joinToString("") { "%02x".format(it) }
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ package sh.sar.basedbank.ui.home
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.databinding.ItemAccountBinding
|
||||
import sh.sar.basedbank.databinding.ItemCardBinding
|
||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||
import sh.sar.basedbank.util.BmlDashboardParser
|
||||
import sh.sar.basedbank.util.MibAccountParser
|
||||
|
||||
class AccountsAdapter(
|
||||
accounts: List<MibAccount>,
|
||||
@@ -106,7 +107,11 @@ class AccountsAdapter(
|
||||
fun bind(account: MibAccount) {
|
||||
binding.tvAccountName.text = account.accountBriefName
|
||||
binding.tvAccountNumber.text = account.accountNumber
|
||||
binding.tvPillType.text = friendlyAccountType(account.accountTypeName)
|
||||
val label = if (account.profileType.startsWith("BML"))
|
||||
BmlDashboardParser.productLabel(account.accountTypeName)
|
||||
else
|
||||
MibAccountParser.productLabel(account.accountTypeName)
|
||||
binding.tvPillType.text = label
|
||||
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||
binding.root.setOnClickListener { onAccountClick(account) }
|
||||
binding.root.setOnLongClickListener {
|
||||
@@ -119,15 +124,10 @@ class AccountsAdapter(
|
||||
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(account: MibAccount) {
|
||||
val brand = cardBrand(account.accountTypeName)
|
||||
binding.tvCardBrand.text = brand.label
|
||||
binding.tvCardBrand.background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = 100f
|
||||
setColor(Color.parseColor(brand.color))
|
||||
}
|
||||
binding.tvCardName.text = account.accountBriefName
|
||||
binding.tvCardNumber.text = account.accountNumber
|
||||
binding.ivCardBrand.setImageResource(cardBrandIcon(account.accountTypeName))
|
||||
binding.tvCardName.text = account.accountBriefName
|
||||
binding.tvCardNumber.text = account.accountNumber
|
||||
binding.tvCardProduct.text = BmlDashboardParser.productLabel(account.accountTypeName)
|
||||
binding.layoutCardBalance.visibility = View.VISIBLE
|
||||
binding.tvCardBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||
|
||||
@@ -155,29 +155,12 @@ class AccountsAdapter(
|
||||
Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun friendlyAccountType(raw: String): String {
|
||||
val u = raw.trim().uppercase()
|
||||
return when {
|
||||
u == "SAVINGS ACCOUNT" ||
|
||||
u == "SAVING ACCOUNT" -> "Savings"
|
||||
u == "CURRENT ACCOUNT" ||
|
||||
u == "CURRENT ACCOUNT(PERSONAL)" ||
|
||||
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
|
||||
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
|
||||
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
|
||||
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
|
||||
else -> raw.trim()
|
||||
}
|
||||
}
|
||||
|
||||
private data class Brand(val label: String, val color: String)
|
||||
|
||||
private fun cardBrand(productName: String): Brand = when {
|
||||
private fun cardBrandIcon(productName: String): Int = when {
|
||||
productName.contains("AMEX", ignoreCase = true) ||
|
||||
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> Brand("AMEX", "#016FD0")
|
||||
productName.contains("VISA", ignoreCase = true) -> Brand("VISA", "#1A1F71")
|
||||
productName.contains("MASTERCARD", ignoreCase = true) -> Brand("MC", "#FF5F00")
|
||||
else -> Brand("CARD", "#555555")
|
||||
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> R.drawable.americanexpress
|
||||
productName.contains("VISA", ignoreCase = true) -> R.drawable.visa
|
||||
productName.contains("MASTERCARD", ignoreCase = true) -> R.drawable.mastercard
|
||||
else -> R.drawable.ic_nav_card
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,18 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import sh.sar.basedbank.util.ContactsCache
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -43,7 +49,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
private data class DestinationOption(
|
||||
val label: String,
|
||||
val isBml: Boolean,
|
||||
val mibProfile: MibProfile? = null
|
||||
val mibProfile: MibProfile? = null,
|
||||
val bmlLoginId: String? = null,
|
||||
val subtitle: String = ""
|
||||
)
|
||||
|
||||
private var destinations: List<DestinationOption> = emptyList()
|
||||
@@ -86,18 +94,44 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
for (profile in app.mibProfiles) {
|
||||
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile))
|
||||
}
|
||||
if (app.anyBmlSession() != null) {
|
||||
list.add(DestinationOption("BML · Personal", isBml = true))
|
||||
val store = CredentialStore(requireContext())
|
||||
for ((loginId, _) in app.bmlSessions) {
|
||||
val ownerName = store.loadBmlUserProfile(loginId)?.fullName?.takeIf { it.isNotBlank() } ?: loginId
|
||||
val profileName = app.bmlAccounts.firstOrNull { it.loginTag == "bml_$loginId" }?.profileName ?: ""
|
||||
list.add(DestinationOption("BML · $ownerName", isBml = true, bmlLoginId = loginId, subtitle = profileName))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
private fun setupDestinationDropdown() {
|
||||
val labels = destinations.map { it.label }
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, labels)
|
||||
val adapter = object : ArrayAdapter<DestinationOption>(requireContext(), android.R.layout.simple_list_item_2, destinations) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
|
||||
getDropDownView(position, convertView, parent)
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_2, parent, false)
|
||||
val opt = destinations[position]
|
||||
view.findViewById<TextView>(android.R.id.text1).text = opt.label
|
||||
val text2 = view.findViewById<TextView>(android.R.id.text2)
|
||||
if (opt.subtitle.isNotBlank()) {
|
||||
text2.text = opt.subtitle
|
||||
text2.visibility = View.VISIBLE
|
||||
} else {
|
||||
text2.visibility = View.GONE
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getFilter() = object : Filter() {
|
||||
override fun performFiltering(c: CharSequence?) = FilterResults().apply { values = destinations; count = destinations.size }
|
||||
override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged()
|
||||
override fun convertResultToString(r: Any?) = ""
|
||||
}
|
||||
}
|
||||
binding.actvDestination.setAdapter(adapter)
|
||||
binding.actvDestination.setOnItemClickListener { _, _, position, _ ->
|
||||
selectedDest = destinations[position]
|
||||
binding.actvDestination.setText(destinations[position].label, false)
|
||||
clearLookupResult()
|
||||
updateMibOnlyVisibility()
|
||||
binding.btnSave.isEnabled = false
|
||||
@@ -129,6 +163,22 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLookupLoading() {
|
||||
val spinner = CircularProgressDrawable(requireContext()).apply {
|
||||
setStyle(CircularProgressDrawable.DEFAULT)
|
||||
setColorSchemeColors(com.google.android.material.color.MaterialColors.getColor(
|
||||
requireView(), com.google.android.material.R.attr.colorPrimary, Color.GRAY))
|
||||
start()
|
||||
}
|
||||
binding.tilAccount.endIconDrawable = spinner
|
||||
binding.tilAccount.isEnabled = false
|
||||
}
|
||||
|
||||
private fun stopLookupLoading() {
|
||||
binding.tilAccount.isEnabled = true
|
||||
binding.tilAccount.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
|
||||
}
|
||||
|
||||
private fun setupAccountSearch() {
|
||||
binding.tilAccount.setEndIconOnClickListener { performLookup() }
|
||||
}
|
||||
@@ -167,7 +217,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
return
|
||||
}
|
||||
|
||||
binding.tilAccount.isEnabled = false
|
||||
startLookupLoading()
|
||||
binding.tilDestination.isEnabled = false
|
||||
binding.btnSave.isEnabled = false
|
||||
|
||||
@@ -175,7 +225,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
if (dest.isBml) lookupForBml(input) else lookupForMib(dest, input)
|
||||
}
|
||||
binding.tilAccount.isEnabled = true
|
||||
stopLookupLoading()
|
||||
binding.tilDestination.isEnabled = true
|
||||
if (result != null) {
|
||||
showLookupResult(result, input)
|
||||
@@ -186,7 +236,8 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
private fun lookupForBml(input: String): BmlAccountValidation? {
|
||||
val bmlSess = app.anyBmlSession() ?: return null
|
||||
val loginId = selectedDest?.bmlLoginId ?: return null
|
||||
val bmlSess = app.bmlSessions[loginId] ?: return null
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
|
||||
// 1) Try BML validate
|
||||
@@ -341,6 +392,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
binding.tilAlias.error = null
|
||||
|
||||
binding.btnSave.isEnabled = false
|
||||
binding.btnSave.text = "Saving..."
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
@@ -352,13 +404,15 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
} else {
|
||||
binding.btnSave.isEnabled = true
|
||||
binding.btnSave.text = "Save"
|
||||
Toast.makeText(requireContext(), R.string.contact_save_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveToBml(alias: String): Boolean {
|
||||
val bmlSess = app.anyBmlSession() ?: return false
|
||||
val loginId = selectedDest?.bmlLoginId ?: return false
|
||||
val bmlSess = app.bmlSessions[loginId] ?: return false
|
||||
val lookup = bmlLookup ?: return false
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
val account = lookup.account
|
||||
@@ -425,8 +479,8 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
requireActivity().lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (dest.isBml) {
|
||||
val bmlSess = app.anyBmlSession() ?: return@launch
|
||||
val loginId = app.bmlSessions.entries.firstOrNull { it.value == bmlSess }?.key ?: ""
|
||||
val loginId = dest.bmlLoginId ?: return@launch
|
||||
val bmlSess = app.bmlSessions[loginId] ?: return@launch
|
||||
val fresh = BmlLoginFlow().fetchContacts(bmlSess, loginId)
|
||||
val existing = viewModel.contacts.value ?: emptyList()
|
||||
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import android.os.Handler
|
||||
@@ -14,6 +15,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -64,24 +66,37 @@ class HomeActivity : AppCompatActivity() {
|
||||
|
||||
private val warningRunnable = Runnable { showAutolockWarning() }
|
||||
|
||||
private var isLocked = false
|
||||
|
||||
private val autolockRunnable = Runnable {
|
||||
countdownTimer?.cancel(); countdownTimer = null
|
||||
warningDialog?.dismiss(); warningDialog = null
|
||||
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||
.getString("security_method", null) != null
|
||||
if (securitySet) {
|
||||
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
if (securitySet) lock()
|
||||
}
|
||||
|
||||
private fun lock() {
|
||||
isLocked = true
|
||||
startActivity(
|
||||
Intent(this, sh.sar.basedbank.LockActivity::class.java)
|
||||
.putExtra(sh.sar.basedbank.LockActivity.EXTRA_RESUME, true)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||
if (getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("block_screenshots", true)) {
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
setContentView(binding.root)
|
||||
val isLight = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = isLight
|
||||
isAppearanceLightNavigationBars = isLight
|
||||
}
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
toggle = ActionBarDrawerToggle(
|
||||
@@ -224,6 +239,13 @@ class HomeActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun setBottomNavVisible(visible: Boolean) {
|
||||
val isBottom = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
if (isBottom) {
|
||||
binding.bottomNavigation.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setRefreshing(visible: Boolean) {
|
||||
binding.refreshIndicator.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
@@ -237,6 +259,13 @@ class HomeActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Returning from LockActivity — skip the elapsed check and reset state.
|
||||
if (isLocked) {
|
||||
isLocked = false
|
||||
pauseTime = 0L
|
||||
resetAutolockTimer()
|
||||
return
|
||||
}
|
||||
// If we were away long enough to have hit the autolock timeout (e.g. while
|
||||
// QrScannerActivity was in the foreground), lock immediately.
|
||||
if (pauseTime > 0L) {
|
||||
@@ -244,8 +273,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
val timeout = getSharedPreferences("prefs", MODE_PRIVATE).getLong("autolock_timeout", 60_000L)
|
||||
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE).getString("security_method", null) != null
|
||||
if (timeout > 0L && elapsed >= timeout && securitySet) {
|
||||
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
|
||||
finish()
|
||||
lock()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -316,8 +344,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_lock) {
|
||||
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
|
||||
finish()
|
||||
lock()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
||||
@@ -14,6 +14,9 @@ import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
||||
@@ -84,8 +87,23 @@ class QrScannerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = ActivityQrScannerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
// Black camera background — always use light (white) system bar icons
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = false
|
||||
isAppearanceLightNavigationBars = false
|
||||
}
|
||||
val originalBtnMarginBottom = (48 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.btnContainer) { view, insets ->
|
||||
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
(view.layoutParams as android.widget.FrameLayout.LayoutParams).also {
|
||||
it.bottomMargin = originalBtnMarginBottom + bars.bottom
|
||||
view.layoutParams = it
|
||||
}
|
||||
insets
|
||||
}
|
||||
binding.btnCancel.setOnClickListener { finish() }
|
||||
binding.btnPickImage.setOnClickListener { pickImageLauncher.launch("image/*") }
|
||||
|
||||
|
||||
@@ -35,8 +35,6 @@ class SettingsSecurityFragment : Fragment() {
|
||||
val canUseBiometrics = BiometricManager.from(requireContext())
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
if (canUseBiometrics) {
|
||||
binding.rowBiometrics.visibility = View.VISIBLE
|
||||
|
||||
val unlockEnabled = prefs.getBoolean("biometrics_enabled", false)
|
||||
binding.switchBiometrics.isChecked = unlockEnabled
|
||||
binding.switchBiometricsTransfer.isChecked = prefs.getBoolean("biometrics_transfer_confirm", false)
|
||||
@@ -54,6 +52,10 @@ class SettingsSecurityFragment : Fragment() {
|
||||
binding.switchBiometricsTransfer.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean("biometrics_transfer_confirm", isChecked).apply()
|
||||
}
|
||||
} else {
|
||||
binding.tvBiometricsHint.visibility = View.VISIBLE
|
||||
binding.switchBiometrics.isEnabled = false
|
||||
binding.switchBiometricsTransfer.isEnabled = false
|
||||
}
|
||||
|
||||
// Auto-lock
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -159,6 +160,22 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLookupLoading() {
|
||||
val spinner = CircularProgressDrawable(requireContext()).apply {
|
||||
setStyle(CircularProgressDrawable.DEFAULT)
|
||||
setColorSchemeColors(com.google.android.material.color.MaterialColors.getColor(
|
||||
requireView(), com.google.android.material.R.attr.colorPrimary, Color.GRAY))
|
||||
start()
|
||||
}
|
||||
binding.tilTo.endIconDrawable = spinner
|
||||
binding.tilTo.isEnabled = false
|
||||
}
|
||||
|
||||
private fun stopLookupLoading() {
|
||||
binding.tilTo.isEnabled = true
|
||||
binding.tilTo.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
|
||||
}
|
||||
|
||||
private fun setupFromDropdown() {
|
||||
binding.btnClearFromInfo.setOnClickListener {
|
||||
selectedAccount = null
|
||||
@@ -313,7 +330,7 @@ class TransferFragment : Fragment() {
|
||||
|
||||
val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true
|
||||
|
||||
binding.tilTo.isEnabled = false
|
||||
startLookupLoading()
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
var errorMsg: String? = null
|
||||
@@ -357,7 +374,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.tilTo.isEnabled = true
|
||||
stopLookupLoading()
|
||||
if (info != null) {
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val matchedAcc = accounts.firstOrNull { it.accountNumber == info.accountNumber }
|
||||
@@ -392,7 +409,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun lookupFahipayTarget(number: String) {
|
||||
binding.tilTo.isEnabled = false
|
||||
startLookupLoading()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
data class LookupResult(
|
||||
val dhiraagu: DhiraaguClient.Result,
|
||||
@@ -419,7 +436,7 @@ class TransferFragment : Fragment() {
|
||||
LookupResult(d, o)
|
||||
}
|
||||
}
|
||||
binding.tilTo.isEnabled = true
|
||||
stopLookupLoading()
|
||||
|
||||
val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() }
|
||||
|
||||
@@ -586,6 +603,7 @@ class TransferFragment : Fragment() {
|
||||
|
||||
val doTransfer: () -> Unit = {
|
||||
binding.btnTransfer.isEnabled = false
|
||||
(activity as? HomeActivity)?.setRefreshing(true)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||
if (!isSrcBml) {
|
||||
@@ -595,6 +613,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
binding.btnTransfer.isEnabled = true
|
||||
(activity as? HomeActivity)?.setRefreshing(false)
|
||||
if (ok && receipt != null) {
|
||||
clearForm()
|
||||
val activity = requireActivity() as HomeActivity
|
||||
@@ -990,7 +1009,9 @@ class TransferFragment : Fragment() {
|
||||
.also { it.root.tag = it }
|
||||
}
|
||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
b.tvDropdownAccountName.text = acc.accountBriefName
|
||||
val isBmlAccount = acc.profileType.startsWith("BML")
|
||||
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"
|
||||
b.root.alpha = if (inactive) 0.4f else 1f
|
||||
|
||||
@@ -313,6 +313,12 @@ class TransferReceiptFragment : Fragment() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = "Receipt"
|
||||
(activity as? HomeActivity)?.setBottomNavVisible(false)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
(activity as? HomeActivity)?.setBottomNavVisible(true)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
||||
@@ -67,11 +67,27 @@ class CredentialsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnLogin.isEnabled = false
|
||||
binding.btnLogin.setOnClickListener { attemptLogin() }
|
||||
|
||||
val loginFieldWatcher = object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) { updateLoginButtonState() }
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
}
|
||||
binding.etUsername.addTextChangedListener(loginFieldWatcher)
|
||||
binding.etPassword.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) { updateLoginButtonState(); updateOtpDisplay() }
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
if (bankType != "FAHIPAY") {
|
||||
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) { updateOtpDisplay() }
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
updateOtpDisplay()
|
||||
updateLoginButtonState()
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
@@ -88,10 +104,35 @@ class CredentialsFragment : Fragment() {
|
||||
otpHandler.removeCallbacks(otpRunnable)
|
||||
}
|
||||
|
||||
private fun resolveOtpSeed(input: String): String {
|
||||
val secret = if (input.startsWith("otpauth://totp/"))
|
||||
android.net.Uri.parse(input).getQueryParameter("secret") ?: input
|
||||
else
|
||||
input
|
||||
return secret.replace("\\s".toRegex(), "").replace("-", "").uppercase()
|
||||
}
|
||||
|
||||
private fun updateLoginButtonState() {
|
||||
val username = binding.etUsername.text.toString().trim()
|
||||
val password = binding.etPassword.text.toString()
|
||||
val otpSeedRaw = binding.etOtpSeed.text.toString().trim()
|
||||
val otpSeed = resolveOtpSeed(otpSeedRaw)
|
||||
binding.btnLogin.isEnabled = when (bankType) {
|
||||
"FAHIPAY" -> username.isNotEmpty() && password.isNotEmpty()
|
||||
else -> username.isNotEmpty() && password.isNotEmpty() && otpSeed.isNotEmpty() && password != otpSeedRaw
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOtpDisplay() {
|
||||
val seed = binding.etOtpSeed.text.toString().trim()
|
||||
val otpSeedRaw = binding.etOtpSeed.text.toString().trim()
|
||||
val seed = resolveOtpSeed(otpSeedRaw)
|
||||
if (seed.isEmpty()) {
|
||||
binding.cardOtp.visibility = View.GONE
|
||||
binding.cardOtp.visibility = View.INVISIBLE
|
||||
return
|
||||
}
|
||||
val password = binding.etPassword.text.toString()
|
||||
if (otpSeedRaw == password || seed.matches(Regex("\\d{6}"))) {
|
||||
binding.cardOtp.visibility = View.INVISIBLE
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -104,7 +145,7 @@ class CredentialsFragment : Fragment() {
|
||||
binding.otpTimer.progress = remaining
|
||||
binding.cardOtp.visibility = View.VISIBLE
|
||||
} catch (e: Exception) {
|
||||
binding.cardOtp.visibility = View.GONE
|
||||
binding.cardOtp.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +157,7 @@ class CredentialsFragment : Fragment() {
|
||||
|
||||
val username = binding.etUsername.text.toString().trim()
|
||||
val password = binding.etPassword.text.toString()
|
||||
val otpSeed = binding.etOtpSeed.text.toString().trim()
|
||||
val otpSeed = resolveOtpSeed(binding.etOtpSeed.text.toString().trim())
|
||||
|
||||
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
||||
binding.tvError.text = "Please fill in all fields"
|
||||
@@ -173,7 +214,7 @@ 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()
|
||||
val otpSeed = resolveOtpSeed(binding.etOtpSeed.text.toString().trim())
|
||||
|
||||
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
|
||||
binding.tvError.text = "Please fill in all fields"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package sh.sar.basedbank.ui.login
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import sh.sar.basedbank.databinding.ActivityLoginBinding
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
@@ -10,7 +12,13 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val isLight = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = isLight
|
||||
isAppearanceLightNavigationBars = isLight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,14 @@ package sh.sar.basedbank.ui.onboarding
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import sh.sar.basedbank.R
|
||||
@@ -17,46 +21,68 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
|
||||
private lateinit var binding: ActivityOnboardingBinding
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = ActivityOnboardingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val isLight = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = isLight
|
||||
isAppearanceLightNavigationBars = isLight
|
||||
}
|
||||
prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||
val originalBottomPadding = binding.bottomBar.paddingBottom
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets ->
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, originalBottomPadding + navBar.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
val adapter = OnboardingPagerAdapter(this)
|
||||
binding.viewPager.adapter = adapter
|
||||
|
||||
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach()
|
||||
|
||||
// Pre-select language chip without triggering the listener
|
||||
val savedLang = prefs.getString("language", null)
|
||||
binding.languageChipGroup.setOnCheckedStateChangeListener(null)
|
||||
when (savedLang) {
|
||||
"en" -> binding.chipEnglish.isChecked = true
|
||||
"dv" -> binding.chipDhivehi.isChecked = true
|
||||
}
|
||||
binding.languageChipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
if (checkedIds.isNotEmpty()) {
|
||||
selectLanguage(if (checkedIds[0] == R.id.chipEnglish) "en" else "dv")
|
||||
// Disable tap-to-navigate on dots: touch listener must be on the individual
|
||||
// tab views inside SlidingTabStrip (child 0), because they consume ACTION_DOWN
|
||||
// before the TabLayout's own touch listener ever fires.
|
||||
val tabStrip = binding.dotsIndicator.getChildAt(0) as? android.view.ViewGroup
|
||||
tabStrip?.let {
|
||||
for (i in 0 until it.childCount) {
|
||||
it.getChildAt(i).setOnTouchListener { _, _ -> true }
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-select language button without triggering the listener
|
||||
val savedLang = prefs.getString("language", null)
|
||||
binding.languageToggle.clearOnButtonCheckedListeners()
|
||||
when (savedLang) {
|
||||
"en" -> binding.btnLangEnglish.isChecked = true
|
||||
"dv" -> binding.btnLangDhivehi.isChecked = true
|
||||
}
|
||||
binding.languageToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
if (isChecked) selectLanguage(if (checkedId == R.id.btnLangEnglish) "en" else "dv")
|
||||
}
|
||||
|
||||
supportFragmentManager.setFragmentResultListener(OnboardingFragment.RESULT_SCROLLED_TO_BOTTOM, this) { _, _ ->
|
||||
startGetStartedCountdown()
|
||||
}
|
||||
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
binding.languageChipGroup.visibility = if (position == 0) View.VISIBLE else View.GONE
|
||||
// Block forward swipe on slide 1 until security is set up
|
||||
if (position == 1) {
|
||||
binding.viewPager.isUserInputEnabled =
|
||||
prefs.getString("security_method", null) != null
|
||||
} else {
|
||||
binding.viewPager.isUserInputEnabled = true
|
||||
binding.languageSection.visibility = if (position == 0) View.VISIBLE else View.GONE
|
||||
binding.viewPager.isUserInputEnabled = when {
|
||||
position > 2 -> false
|
||||
position == 1 -> prefs.getString("security_method", null) != null
|
||||
else -> true
|
||||
}
|
||||
updateButtons(position, adapter.itemCount)
|
||||
}
|
||||
})
|
||||
|
||||
binding.languageChipGroup.visibility = View.VISIBLE
|
||||
binding.languageSection.visibility = View.VISIBLE
|
||||
updateButtons(0, adapter.itemCount)
|
||||
|
||||
binding.btnNext.setOnClickListener {
|
||||
@@ -71,10 +97,21 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
countDownTimer?.cancel()
|
||||
}
|
||||
|
||||
// Called by SecuritySetupFragment when setup is complete
|
||||
override fun onSecuritySetupComplete() {
|
||||
binding.viewPager.isUserInputEnabled = true
|
||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
|
||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 4)
|
||||
}
|
||||
|
||||
// Called by SecuritySetupFragment when user resets to reconfigure
|
||||
override fun onSecuritySetupReset() {
|
||||
binding.viewPager.isUserInputEnabled = false
|
||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 4)
|
||||
}
|
||||
|
||||
private fun selectLanguage(lang: String) {
|
||||
@@ -83,23 +120,34 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
|
||||
}
|
||||
|
||||
private fun startGetStartedCountdown() {
|
||||
binding.btnGetStarted.isEnabled = false
|
||||
countDownTimer?.cancel()
|
||||
countDownTimer = object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
val seconds = (millisUntilFinished / 1000 + 1).toInt()
|
||||
binding.btnGetStarted.text = "${getString(R.string.get_started)} ($seconds)"
|
||||
}
|
||||
override fun onFinish() {
|
||||
binding.btnGetStarted.text = getString(R.string.get_started)
|
||||
binding.btnGetStarted.isEnabled = true
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun updateButtons(position: Int, count: Int) {
|
||||
val langSelected = prefs.getString("language", null) != null
|
||||
val securityDone = prefs.getString("security_method", null) != null
|
||||
val isLast = position == count - 1
|
||||
|
||||
binding.btnGetStarted.visibility = if (isLast) View.VISIBLE else View.GONE
|
||||
if (isLast) binding.btnGetStarted.isEnabled = false
|
||||
|
||||
// Hide Next on slide 1 until security is done (avoids a disabled-button-with-no-explanation)
|
||||
binding.btnNext.visibility = when {
|
||||
isLast -> View.GONE
|
||||
position == 1 && !securityDone -> View.GONE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
binding.btnNext.visibility = if (isLast) View.GONE else View.VISIBLE
|
||||
binding.btnNext.isEnabled = when (position) {
|
||||
0 -> langSelected
|
||||
1 -> securityDone
|
||||
else -> true
|
||||
else -> true // position 2 (configure) has no gate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package sh.sar.basedbank.ui.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.databinding.FragmentOnboardingConfigureBinding
|
||||
|
||||
class OnboardingConfigureFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentOnboardingConfigureBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentOnboardingConfigureBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
|
||||
// Navigation — default Drawer
|
||||
val isBottom = prefs.getBoolean("bottom_nav", false)
|
||||
binding.navModeToggle.check(if (isBottom) R.id.btnNavBottom else R.id.btnNavDrawer)
|
||||
binding.navModeToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
if (!isChecked) return@addOnButtonCheckedListener
|
||||
prefs.edit().putBoolean("bottom_nav", checkedId == R.id.btnNavBottom).apply()
|
||||
}
|
||||
|
||||
// Theme — default System
|
||||
val savedTheme = prefs.getString("theme", "system")
|
||||
binding.themeToggle.check(when (savedTheme) {
|
||||
"light" -> R.id.btnThemeLight
|
||||
"dark" -> R.id.btnThemeDark
|
||||
else -> R.id.btnThemeSystem
|
||||
})
|
||||
binding.themeToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
if (!isChecked) return@addOnButtonCheckedListener
|
||||
val (key, mode) = when (checkedId) {
|
||||
R.id.btnThemeLight -> "light" to AppCompatDelegate.MODE_NIGHT_NO
|
||||
R.id.btnThemeDark -> "dark" to AppCompatDelegate.MODE_NIGHT_YES
|
||||
else -> "system" to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
prefs.edit().putString("theme", key).apply()
|
||||
AppCompatDelegate.setDefaultNightMode(mode)
|
||||
}
|
||||
|
||||
// Biometrics
|
||||
val canUseBiometrics = BiometricManager.from(requireContext())
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
if (canUseBiometrics) {
|
||||
val unlockEnabled = prefs.getBoolean("biometrics_enabled", false)
|
||||
binding.switchBiometrics.isChecked = unlockEnabled
|
||||
binding.switchBiometricsTransfer.isChecked = prefs.getBoolean("biometrics_transfer_confirm", false)
|
||||
binding.switchBiometricsTransfer.isEnabled = unlockEnabled
|
||||
|
||||
binding.switchBiometrics.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean("biometrics_enabled", isChecked).apply()
|
||||
binding.switchBiometricsTransfer.isEnabled = isChecked
|
||||
if (!isChecked) {
|
||||
binding.switchBiometricsTransfer.isChecked = false
|
||||
prefs.edit().putBoolean("biometrics_transfer_confirm", false).apply()
|
||||
}
|
||||
}
|
||||
|
||||
binding.switchBiometricsTransfer.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean("biometrics_transfer_confirm", isChecked).apply()
|
||||
}
|
||||
} else {
|
||||
binding.tvBiometricsHint.visibility = View.VISIBLE
|
||||
binding.switchBiometrics.isEnabled = false
|
||||
binding.switchBiometricsTransfer.isEnabled = false
|
||||
}
|
||||
|
||||
// Block screenshots — default on
|
||||
val blockScreenshots = prefs.getBoolean("block_screenshots", true)
|
||||
binding.switchBlockScreenshots.isChecked = blockScreenshots
|
||||
applyFlagSecure(blockScreenshots)
|
||||
binding.switchBlockScreenshots.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean("block_screenshots", isChecked).apply()
|
||||
applyFlagSecure(isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyFlagSecure(enabled: Boolean) {
|
||||
val win = activity?.window ?: return
|
||||
if (enabled) win.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else win.clearFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.ScrollView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import sh.sar.basedbank.databinding.FragmentOnboardingSlideBinding
|
||||
@@ -12,6 +14,7 @@ class OnboardingFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentOnboardingSlideBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var scrolledToBottom = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentOnboardingSlideBinding.inflate(inflater, container, false)
|
||||
@@ -19,17 +22,45 @@ class OnboardingFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val title = requireArguments().getString(ARG_TITLE, "")
|
||||
val desc = requireArguments().getString(ARG_DESC, "")
|
||||
val titleRes = requireArguments().getInt(ARG_TITLE)
|
||||
val descRes = requireArguments().getInt(ARG_DESC)
|
||||
val icon = requireArguments().getInt(ARG_ICON, 0)
|
||||
val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false)
|
||||
val isLast = requireArguments().getBoolean(ARG_IS_LAST, false)
|
||||
|
||||
binding.icon.visibility = if (isLast) View.GONE else View.VISIBLE
|
||||
binding.icon.setImageResource(icon)
|
||||
binding.title.text = title
|
||||
binding.description.text = desc
|
||||
binding.title.text = getString(titleRes)
|
||||
binding.description.text = getString(descRes)
|
||||
binding.description.gravity = if (isLast) android.view.Gravity.START else android.view.Gravity.CENTER
|
||||
|
||||
// On the first slide, show the two placeholder cards for upcoming banks
|
||||
binding.placeholderCards.visibility = if (isFirst) View.VISIBLE else View.GONE
|
||||
|
||||
if (isLast) setupScrollToBottomDetection()
|
||||
}
|
||||
|
||||
private fun setupScrollToBottomDetection() {
|
||||
val scrollView = binding.scrollView
|
||||
// If content fits without scrolling, fire immediately after layout
|
||||
scrollView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
scrollView.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
val child = scrollView.getChildAt(0) ?: return
|
||||
if (child.height <= scrollView.height) notifyScrolledToBottom()
|
||||
}
|
||||
})
|
||||
scrollView.setOnScrollChangeListener { v, _, scrollY, _, _ ->
|
||||
val sv = v as ScrollView
|
||||
val child = sv.getChildAt(0) ?: return@setOnScrollChangeListener
|
||||
if (scrollY + sv.height >= child.height) notifyScrolledToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyScrolledToBottom() {
|
||||
if (scrolledToBottom) return
|
||||
scrolledToBottom = true
|
||||
parentFragmentManager.setFragmentResult(RESULT_SCROLLED_TO_BOTTOM, Bundle.EMPTY)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -38,17 +69,20 @@ class OnboardingFragment : Fragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RESULT_SCROLLED_TO_BOTTOM = "scroll_to_bottom"
|
||||
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"
|
||||
private const val ARG_IS_LAST = "is_last"
|
||||
|
||||
fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply {
|
||||
arguments = bundleOf(
|
||||
ARG_TITLE to slide.title,
|
||||
ARG_DESC to slide.description,
|
||||
ARG_TITLE to slide.titleRes,
|
||||
ARG_DESC to slide.descRes,
|
||||
ARG_ICON to slide.iconRes,
|
||||
ARG_IS_FIRST to slide.isFirst
|
||||
ARG_IS_FIRST to slide.isFirst,
|
||||
ARG_IS_LAST to slide.isLast
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,36 +9,39 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
|
||||
|
||||
private val slides = listOf(
|
||||
OnboardingSlide(
|
||||
title = activity.getString(R.string.onboarding_title_1),
|
||||
description = activity.getString(R.string.onboarding_desc_1),
|
||||
titleRes = R.string.onboarding_title_1,
|
||||
descRes = 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),
|
||||
titleRes = R.string.onboarding_title_2,
|
||||
descRes = 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),
|
||||
titleRes = R.string.onboarding_title_3,
|
||||
descRes = R.string.onboarding_desc_3,
|
||||
iconRes = R.drawable.ic_launcher_foreground,
|
||||
isFirst = false
|
||||
isFirst = false,
|
||||
isLast = true
|
||||
)
|
||||
)
|
||||
|
||||
override fun getItemCount() = slides.size
|
||||
override fun getItemCount() = slides.size + 1 // +1 for OnboardingConfigureFragment at position 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
1 -> SecuritySetupFragment()
|
||||
else -> OnboardingFragment.newInstance(slides[position])
|
||||
2 -> OnboardingConfigureFragment()
|
||||
else -> OnboardingFragment.newInstance(slides[position - if (position > 2) 1 else 0])
|
||||
}
|
||||
}
|
||||
|
||||
data class OnboardingSlide(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val titleRes: Int,
|
||||
val descRes: Int,
|
||||
val iconRes: Int,
|
||||
val isFirst: Boolean
|
||||
val isFirst: Boolean,
|
||||
val isLast: Boolean = false
|
||||
)
|
||||
|
||||
@@ -86,17 +86,21 @@ class PatternView @JvmOverloads constructor(
|
||||
if (errorState) return false
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
recording = true; selected.clear()
|
||||
hit(event.x, event.y)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
touchX = event.x; touchY = event.y
|
||||
hit(event.x, event.y)
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
recording = false
|
||||
invalidate()
|
||||
onPatternComplete?.invoke(selected.map { it.index })
|
||||
if (event.action == MotionEvent.ACTION_UP)
|
||||
onPatternComplete?.invoke(selected.map { it.index })
|
||||
}
|
||||
}
|
||||
invalidate()
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import sh.sar.basedbank.R
|
||||
@@ -21,6 +20,7 @@ class SecuritySetupFragment : Fragment() {
|
||||
|
||||
interface Callback {
|
||||
fun onSecuritySetupComplete()
|
||||
fun onSecuritySetupReset()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -33,7 +33,7 @@ class SecuritySetupFragment : Fragment() {
|
||||
private var _b: FragmentSecuritySetupBinding? = null
|
||||
private val b get() = _b!!
|
||||
|
||||
private enum class Step { CHOOSE, PIN_ENTER, PIN_CONFIRM, PATTERN_ENTER, PATTERN_CONFIRM, BIOMETRIC }
|
||||
private enum class Step { CONFIGURED, CHOOSE, PIN_ENTER, PIN_CONFIRM, PATTERN_ENTER, PATTERN_CONFIRM }
|
||||
|
||||
private var step = Step.CHOOSE
|
||||
private val pinDigits = mutableListOf<Int>()
|
||||
@@ -48,9 +48,6 @@ class SecuritySetupFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
val changeMode = arguments?.getBoolean(ARG_CHANGE_MODE, false) ?: false
|
||||
if (!changeMode && prefs.getString("security_method", null) != null) {
|
||||
(activity as? Callback)?.onSecuritySetupComplete()
|
||||
}
|
||||
|
||||
b.cardPin.setOnClickListener { goTo(Step.PIN_ENTER) }
|
||||
b.cardPattern.setOnClickListener { goTo(Step.PATTERN_ENTER) }
|
||||
@@ -62,20 +59,21 @@ class SecuritySetupFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
b.btnPatternBack.setOnClickListener { goTo(Step.CHOOSE) }
|
||||
b.btnChangeLock.setOnClickListener {
|
||||
prefs.edit().remove("security_method").apply()
|
||||
(activity as? Callback)?.onSecuritySetupReset()
|
||||
goTo(Step.CHOOSE)
|
||||
}
|
||||
|
||||
b.patternView.onPatternComplete = { pattern -> handlePattern(pattern) }
|
||||
|
||||
b.btnEnableBiometrics.setOnClickListener {
|
||||
prefs.edit().putBoolean("biometrics_enabled", true).apply()
|
||||
finishSetup()
|
||||
}
|
||||
b.btnSkipBiometrics.setOnClickListener {
|
||||
prefs.edit().putBoolean("biometrics_enabled", false).apply()
|
||||
finishSetup()
|
||||
}
|
||||
|
||||
buildNumpad()
|
||||
goTo(Step.CHOOSE)
|
||||
|
||||
if (!changeMode && prefs.getString("security_method", null) != null) {
|
||||
goTo(Step.CONFIGURED)
|
||||
} else {
|
||||
goTo(Step.CHOOSE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNumpad() {
|
||||
@@ -144,7 +142,7 @@ class SecuritySetupFragment : Fragment() {
|
||||
Step.PIN_CONFIRM -> {
|
||||
if (entered == firstPin) {
|
||||
saveCredential("pin", entered)
|
||||
goToBiometricOrFinish()
|
||||
finishSetup()
|
||||
} else {
|
||||
b.tvPinDots.text = getString(R.string.pin_no_match)
|
||||
pinDigits.clear()
|
||||
@@ -172,7 +170,7 @@ class SecuritySetupFragment : Fragment() {
|
||||
Step.PATTERN_CONFIRM -> {
|
||||
if (pattern == firstPattern) {
|
||||
saveCredential("pattern", pattern.joinToString(""))
|
||||
goToBiometricOrFinish()
|
||||
finishSetup()
|
||||
} else {
|
||||
b.patternView.showError()
|
||||
b.tvPatternStatus.text = getString(R.string.pattern_no_match)
|
||||
@@ -187,29 +185,12 @@ class SecuritySetupFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToBiometricOrFinish() {
|
||||
// In change mode, biometrics is managed from Settings — skip this step
|
||||
if (arguments?.getBoolean(ARG_CHANGE_MODE, false) == true) {
|
||||
finishSetup()
|
||||
return
|
||||
}
|
||||
val canAuth = BiometricManager.from(requireContext())
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
|
||||
if (canAuth == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
goTo(Step.BIOMETRIC)
|
||||
} else {
|
||||
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
.edit().putBoolean("biometrics_enabled", false).apply()
|
||||
finishSetup()
|
||||
}
|
||||
}
|
||||
|
||||
private fun goTo(s: Step) {
|
||||
step = s
|
||||
b.viewConfigured.visibility = if (s == Step.CONFIGURED) View.VISIBLE else View.GONE
|
||||
b.viewChooseMethod.visibility = if (s == Step.CHOOSE) View.VISIBLE else View.GONE
|
||||
b.viewPinSetup.visibility = if (s == Step.PIN_ENTER || s == Step.PIN_CONFIRM) View.VISIBLE else View.GONE
|
||||
b.viewPatternSetup.visibility = if (s == Step.PATTERN_ENTER || s == Step.PATTERN_CONFIRM) View.VISIBLE else View.GONE
|
||||
b.viewBiometric.visibility = if (s == Step.BIOMETRIC) View.VISIBLE else View.GONE
|
||||
|
||||
when (s) {
|
||||
Step.PIN_ENTER -> {
|
||||
@@ -236,9 +217,6 @@ class SecuritySetupFragment : Fragment() {
|
||||
val hash = pbkdf2(input, salt)
|
||||
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
|
||||
.putString("security_method", method)
|
||||
// Remove legacy plaintext fields if they exist from an old install
|
||||
.remove("security_salt")
|
||||
.remove("security_hash")
|
||||
.apply()
|
||||
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
|
||||
}
|
||||
@@ -256,6 +234,7 @@ class SecuritySetupFragment : Fragment() {
|
||||
private fun finishSetup() {
|
||||
val cb = activity as? Callback
|
||||
if (cb != null) {
|
||||
goTo(Step.CONFIGURED)
|
||||
cb.onSecuritySetupComplete()
|
||||
} else {
|
||||
parentFragmentManager.popBackStack()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package sh.sar.basedbank.util
|
||||
|
||||
object BmlDashboardParser {
|
||||
|
||||
/**
|
||||
* Returns a display-ready product label for a BML dashboard account or card.
|
||||
* Known BML product names are mapped to short friendly labels.
|
||||
* Everything else is title-cased (first letter of each word capitalised).
|
||||
*/
|
||||
fun productLabel(raw: String): String {
|
||||
val u = raw.trim().uppercase()
|
||||
return when {
|
||||
u == "SAVINGS ACCOUNT" -> "Savings"
|
||||
u == "CURRENT ACCOUNT" ||
|
||||
u == "CURRENT ACCOUNT(PERSONAL)" ||
|
||||
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
|
||||
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
|
||||
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
|
||||
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
|
||||
else -> toTitleCase(raw)
|
||||
}
|
||||
}
|
||||
|
||||
fun toTitleCase(input: String): String =
|
||||
input.trim().lowercase().split(" ").joinToString(" ") { word ->
|
||||
word.replaceFirstChar { it.uppercaseChar() }
|
||||
}
|
||||
}
|
||||
@@ -93,32 +93,10 @@ class CredentialStore(context: Context) {
|
||||
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
|
||||
|
||||
fun getBmlLoginIds(): List<String> {
|
||||
val json = prefs.getString("bml_login_ids", null)
|
||||
if (json != null) {
|
||||
return try {
|
||||
val arr = org.json.JSONArray(json)
|
||||
(0 until arr.length()).map { arr.getString(it) }
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
// One-time migration from single-slot BML storage
|
||||
val oldEncUsername = prefs.getString("bml_enc_username", null) ?: return emptyList()
|
||||
val json = prefs.getString("bml_login_ids", null) ?: return emptyList()
|
||||
return try {
|
||||
val key = getOrCreateKey()
|
||||
val loginId = decrypt(oldEncUsername, key)
|
||||
val edit = prefs.edit()
|
||||
prefs.getString("bml_enc_password", null)?.let { edit.putString("bml_${loginId}_enc_password", it) }
|
||||
prefs.getString("bml_enc_otp_seed", null)?.let { edit.putString("bml_${loginId}_enc_otp_seed", it) }
|
||||
prefs.getString("bml_enc_token", null)?.let { edit.putString("bml_${loginId}_enc_token", it) }
|
||||
prefs.getString("bml_enc_device_id", null)?.let { edit.putString("bml_${loginId}_enc_device_id", it) }
|
||||
prefs.getString("bml_enc_profile", null)?.let { edit.putString("bml_${loginId}_enc_profile", it) }
|
||||
edit.putString("bml_${loginId}_enc_username", oldEncUsername)
|
||||
edit.remove("bml_enc_username").remove("bml_enc_password").remove("bml_enc_otp_seed")
|
||||
.remove("bml_enc_token").remove("bml_enc_device_id")
|
||||
.remove("bml_enc_profile").remove("bml_enc_full_name")
|
||||
val ids = org.json.JSONArray(listOf(loginId)).toString()
|
||||
edit.putString("bml_login_ids", ids)
|
||||
edit.apply()
|
||||
listOf(loginId)
|
||||
val arr = org.json.JSONArray(json)
|
||||
(0 until arr.length()).map { arr.getString(it) }
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
|
||||
18
app/src/main/java/sh/sar/basedbank/util/MibAccountParser.kt
Normal file
18
app/src/main/java/sh/sar/basedbank/util/MibAccountParser.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package sh.sar.basedbank.util
|
||||
|
||||
object MibAccountParser {
|
||||
|
||||
/**
|
||||
* Returns a display-ready product label for a MIB (Faisanet) account type name.
|
||||
* Known MIB accountTypeName values are mapped to short friendly labels.
|
||||
* Everything else is returned trimmed as-is.
|
||||
*/
|
||||
fun productLabel(raw: String): String {
|
||||
val u = raw.trim().uppercase()
|
||||
return when {
|
||||
u == "SAVING ACCOUNT" -> "Savings"
|
||||
u == "CURRENT ACCOUNT" -> "Current"
|
||||
else -> raw.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
10
app/src/main/res/drawable/americanexpress.xml
Normal file
10
app/src/main/res/drawable/americanexpress.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#016FD0"
|
||||
android:pathData="M16.015 14.378c0-.32-.135-.496-.344-.622-.21-.12-.464-.135-.81-.135h-1.543v2.82h.675v-1.027h.72c.24 0 .39.024.478.125.12.13.104.38.104.55v.35h.66v-.555c-.002-.25-.017-.376-.108-.516-.06-.08-.18-.18-.33-.234l.02-.008c.18-.072.48-.297.48-.747zm-.87.407l-.028-.002c-.09.053-.195.058-.33.058h-.81v-.63h.824c.12 0 .24 0 .33.05.098.048.156.147.15.255 0 .12-.045.215-.134.27zM20.297 15.837H19v.6h1.304c.676 0 1.05-.278 1.05-.884 0-.28-.066-.448-.187-.582-.153-.133-.392-.193-.73-.207l-.376-.015c-.104 0-.18 0-.255-.03-.09-.03-.15-.105-.15-.21 0-.09.017-.166.09-.21.083-.046.177-.066.272-.06h1.23v-.602h-1.35c-.704 0-.958.437-.958.84 0 .9.776.855 1.407.87.104 0 .18.015.225.06.046.03.082.106.082.18 0 .077-.035.15-.08.18-.06.053-.15.07-.277.07zM0 0v10.096L.81 8.22h1.75l.225.464V8.22h2.043l.45 1.02.437-1.013h6.502c.295 0 .56.057.756.236v-.23h1.787v.23c.307-.17.686-.23 1.12-.23h2.606l.24.466v-.466h1.918l.254.465v-.466h1.858v3.948H20.87l-.36-.6v.585h-2.353l-.256-.63h-.583l-.27.614h-1.213c-.48 0-.84-.104-1.08-.24v.24h-2.89v-.884c0-.12-.03-.12-.105-.135h-.105v1.036H6.067v-.48l-.21.48H4.69l-.202-.48v.465H2.235l-.256-.624H1.4l-.256.624H0V24h23.786v-7.108c-.27.135-.613.18-.973.18H21.09v-.255c-.21.165-.57.255-.914.255H14.71v-.9c0-.12-.018-.12-.12-.12h-.075v1.022h-1.8v-1.066c-.298.136-.643.15-.928.136h-.214v.915h-2.18l-.54-.617-.57.6H4.742v-3.93h3.61l.518.602.554-.6h2.412c.28 0 .74.03.942.225v-.24h2.177c.202 0 .644.045.903.225v-.24h3.265v.24c.163-.164.508-.24.803-.24h1.89v.24c.194-.15.464-.24.84-.24h1.176V0H0zM21.156 14.955c.004.005.006.012.01.016.01.01.024.01.032.02l-.042-.035zM23.828 13.082h.065v.555h-.065zM23.865 15.03v-.005c-.03-.025-.046-.048-.075-.07-.15-.153-.39-.215-.764-.225l-.36-.012c-.12 0-.194-.007-.27-.03-.09-.03-.15-.105-.15-.21 0-.09.03-.16.09-.204.076-.045.15-.05.27-.05h1.223v-.588h-1.283c-.69 0-.96.437-.96.84 0 .9.78.855 1.41.87.104 0 .18.015.224.06.046.03.076.106.076.18 0 .07-.034.138-.09.18-.045.056-.136.07-.27.07h-1.288v.605h1.287c.42 0 .734-.118.9-.36h.03c.09-.134.135-.3.135-.523 0-.24-.045-.39-.135-.526zM18.597 14.208v-.583h-2.235V16.458h2.235v-.585h-1.57v-.57h1.533v-.584h-1.532v-.51M13.51 8.787h.685V11.6h-.684zM13.126 9.543l-.007.006c0-.314-.13-.5-.34-.624-.217-.125-.47-.135-.81-.135H10.43v2.82h.674v-1.034h.72c.24 0 .39.03.487.12.122.136.107.378.107.548v.354h.677v-.553c0-.25-.016-.375-.11-.516-.09-.107-.202-.19-.33-.237.172-.07.472-.3.472-.75zm-.855.396h-.015c-.09.054-.195.056-.33.056H11.1v-.623h.825c.12 0 .24.004.33.05.09.04.15.128.15.25s-.047.22-.134.266zM15.92 9.373h.632v-.6h-.644c-.464 0-.804.105-1.02.33-.286.3-.362.69-.362 1.11 0 .512.123.833.36 1.074.232.238.645.31.97.31h.78l.255-.627h1.39l.262.627h1.36v-2.11l1.272 2.11h.95l.002.002V8.786h-.684v1.963l-1.18-1.96h-1.02V11.4L18.11 8.744h-1.004l-.943 2.22h-.3c-.177 0-.362-.03-.468-.134-.125-.15-.186-.36-.186-.662 0-.285.08-.51.194-.63.133-.135.272-.165.516-.165zm1.668-.108l.464 1.118v.002h-.93l.466-1.12zM2.38 10.97l.254.628H4V9.393l.972 2.205h.584l.973-2.202.015 2.202h.69v-2.81H6.118l-.807 1.904-.876-1.905H3.343v2.663L2.205 8.787h-.997L.01 11.597h.72l.26-.626h1.39zm-.688-1.705l.46 1.118-.003.002h-.915l.457-1.12zM11.856 13.62H9.714l-.85.923-.825-.922H5.346v2.82H8l.855-.932.824.93h1.302v-.94h.838c.6 0 1.17-.164 1.17-.945l-.006-.003c0-.78-.598-.93-1.128-.93zM7.67 15.853l-.014-.002H6.02v-.557h1.47v-.574H6.02v-.51H7.7l.733.82-.764.824zm2.642.33l-1.03-1.147 1.03-1.108v2.253zm1.553-1.258h-.885v-.717h.885c.24 0 .42.098.42.344 0 .243-.15.372-.42.372zM9.967 9.373v-.586H7.73V11.6h2.237v-.58H8.4v-.564h1.527V9.88H8.4v-.507" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_check_circle.xml
Normal file
11
app/src/main/res/drawable/ic_check_circle.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/mastercard.xml
Normal file
10
app/src/main/res/drawable/mastercard.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF5F00"
|
||||
android:pathData="M11.343 18.031c.058.049.12.098.181.146-1.177.783-2.59 1.238-4.107 1.238C3.32 19.416 0 16.096 0 12c0-4.095 3.32-7.416 7.416-7.416 1.518 0 2.931.456 4.105 1.238-.06.051-.12.098-.165.15C9.6 7.489 8.595 9.688 8.595 12c0 2.311 1.001 4.51 2.748 6.031zm5.241-13.447c-1.52 0-2.931.456-4.105 1.238.06.051.12.098.165.15C14.4 7.489 15.405 9.688 15.405 12c0 2.31-1.001 4.507-2.748 6.031-.058.049-.12.098-.181.146 1.177.783 2.588 1.238 4.107 1.238C20.68 19.416 24 16.096 24 12c0-4.094-3.32-7.416-7.416-7.416zM12 6.174c-.096.075-.189.15-.28.231C10.156 7.764 9.169 9.765 9.169 12c0 2.236.987 4.236 2.551 5.595.09.08.185.158.28.232.096-.074.189-.152.28-.232 1.563-1.359 2.551-3.359 2.551-5.595 0-2.235-.987-4.236-2.551-5.595-.09-.08-.184-.156-.28-.231z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/visa.xml
Normal file
10
app/src/main/res/drawable/visa.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#1A1F71"
|
||||
android:pathData="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-0.094,-0.368,-0.175,-0.503,-0.461,-0.658C1.447 8.864 0.677 8.627 0 8.479l0.046,-0.217h3.3a0.904,0.904,0,0,1,0.894,0.764l0.817 4.338 2.018,-5.102zM17.145 13.311c0.008,-1.979,-2.736,-2.088,-2.717,-2.972 0.006,-0.269 0.262,-0.555 0.822,-0.628a3.66,3.66,0,0,1,1.913,0.336l0.34,-1.59a5.207,5.207,0,0,0,-1.814,-0.333c-1.917 0,-3.266 1.02,-3.278 2.479,-0.012 1.079 0.963 1.68 1.698 2.04 0.756 0.367 1.01 0.603 1.006 0.931,-0.005 0.504,-0.602 0.725,-1.16 0.734,-0.975 0.015,-1.54,-0.263,-1.992,-0.473l-0.351 1.642c0.453 0.208 1.289 0.39 2.156 0.398 2.037 0 3.37,-1.006 3.377,-2.564zM22.206 15.758H24l-1.565,-7.496h-1.656a0.883,0.883,0,0,0,-0.826,0.55l-2.909 6.946h2.036l0.405,-1.12h2.488zM20.043 13.102l1.02,-2.815 0.588 2.815zM11.883 8.262l-1.603 7.496H8.34l1.605,-7.496z" />
|
||||
</vector>
|
||||
@@ -4,17 +4,20 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<!-- Main content -->
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
@@ -49,6 +52,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:fitsSystemWindows="true"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -61,6 +65,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:menu="@menu/drawer_menu" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/navHostFragment"
|
||||
|
||||
@@ -27,31 +27,49 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/languageChipGroup"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:id="@+id/languageSection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="false">
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipEnglish"
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="English"
|
||||
style="@style/Widget.Material3.Chip.Filter" />
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/select_language"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipDhivehi"
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/languageToggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="ދިވެހި"
|
||||
style="@style/Widget.Material3.Chip.Filter" />
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true">
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLangEnglish"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="English" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLangDhivehi"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="ދިވެހި" />
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/dotsIndicator"
|
||||
@@ -61,6 +79,8 @@
|
||||
android:background="@null"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
app:tabBackground="@drawable/tab_indicator_selector"
|
||||
app:tabGravity="center"
|
||||
app:tabIndicatorHeight="0dp"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/btnContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
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"
|
||||
@@ -116,7 +115,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone"
|
||||
android:visibility="invisible"
|
||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
218
app/src/main/res/layout/fragment_onboarding_configure.xml
Normal file
218
app/src/main/res/layout/fragment_onboarding_configure.xml
Normal file
@@ -0,0 +1,218 @@
|
||||
<?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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:paddingTop="40dp">
|
||||
|
||||
<!-- Appearance -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_appearance"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_navigation"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/navModeToggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnNavDrawer"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/settings_nav_drawer" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnNavBottom"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/settings_nav_bottom" />
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/theme"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/themeToggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnThemeSystem"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/theme_system" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnThemeLight"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/theme_light" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnThemeDark"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/theme_dark" />
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<!-- Privacy & Security -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_privacy_security"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Biometrics -->
|
||||
<LinearLayout
|
||||
android:id="@+id/rowBiometrics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_biometrics"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBiometricsHint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_biometrics_unavailable"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/settings_biometrics_unlock"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchBiometrics"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/settings_biometrics_transfer"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchBiometricsTransfer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Block screenshots -->
|
||||
<LinearLayout
|
||||
android:id="@+id/rowBlockScreenshots"
|
||||
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/settings_block_screenshots"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_block_screenshots_desc"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchBlockScreenshots"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -1,94 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
<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="match_parent"
|
||||
android:fillViewport="true">
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingHorizontal="32dp"
|
||||
android:paddingTop="64dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
<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:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingHorizontal="32dp"
|
||||
android:paddingTop="64dp"
|
||||
android:paddingBottom="16dp">
|
||||
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:contentDescription="@string/app_name" />
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:fillViewport="true">
|
||||
|
||||
<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" />
|
||||
|
||||
<!-- Bank logo 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">
|
||||
android:orientation="vertical">
|
||||
|
||||
<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">
|
||||
<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" />
|
||||
|
||||
<!-- Supported services shown only on first slide -->
|
||||
<LinearLayout
|
||||
android:id="@+id/placeholderCards"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="40dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/onboarding_supported_services"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/mib_faisanet_logo"
|
||||
android:scaleType="centerInside"
|
||||
android:padding="12dp"
|
||||
android:contentDescription="@string/mib_name" />
|
||||
|
||||
</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">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:src="@drawable/bml_logo_vector"
|
||||
android:scaleType="centerInside"
|
||||
android:padding="8dp"
|
||||
android:contentDescription="@string/bml_name" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:src="@drawable/fahipay_logo_long"
|
||||
android:scaleType="centerInside"
|
||||
android:contentDescription="@string/fahipay_name" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="#FFFFFF">
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<!-- Renderable receipt card -->
|
||||
@@ -206,12 +206,6 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Pushes buttons to bottom of screen -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<!-- Action buttons — outside renderable area -->
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
@@ -219,7 +213,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="#FFFFFF"
|
||||
android:background="?attr/colorSurface"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
android:paddingBottom="24dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔒"
|
||||
android:textSize="56sp"
|
||||
<ImageView
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:src="@drawable/ic_lock"
|
||||
android:tint="?attr/colorPrimary"
|
||||
android:contentDescription="@null"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
@@ -255,73 +256,49 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Step: Biometric prompt -->
|
||||
|
||||
<!-- Step: Already configured -->
|
||||
<LinearLayout
|
||||
android:id="@+id/viewBiometric"
|
||||
android:id="@+id/viewConfigured"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingTop="40dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:src="@drawable/ic_check_circle"
|
||||
android:tint="?attr/colorPrimary"
|
||||
android:contentDescription="@null"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔐"
|
||||
android:textSize="72sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/biometric_title"
|
||||
android:text="@string/security_already_configured"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="12dp" />
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/biometric_desc"
|
||||
android:text="@string/security_already_configured_desc"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="14dp"
|
||||
android:text="@string/biometric_security_note"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSecondaryContainer"
|
||||
android:gravity="center" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
android:layout_marginBottom="32dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnEnableBiometrics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:text="@string/enable_biometrics"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSkipBiometrics"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:id="@+id/btnChangeLock"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/skip_biometrics" />
|
||||
android:text="@string/settings_change_lock" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone">
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -40,7 +39,17 @@
|
||||
android:text="@string/settings_biometrics"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="8dp" />
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBiometricsHint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_biometrics_unavailable"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -14,18 +14,14 @@
|
||||
android:gravity="center_vertical"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
|
||||
<!-- Brand chip -->
|
||||
<TextView
|
||||
android:id="@+id/tvCardBrand"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="6dp"
|
||||
<!-- Brand logo -->
|
||||
<ImageView
|
||||
android:id="@+id/ivCardBrand"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="@android:color/white"
|
||||
android:fontFamily="monospace"
|
||||
android:background="@drawable/chip_background" />
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="Card brand" />
|
||||
|
||||
<!-- Card name + number -->
|
||||
<LinearLayout
|
||||
@@ -50,6 +46,14 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="monospace" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardProduct"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Status pill + balance (prepaid only) -->
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
<string name="app_name">BasedBank</string>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<string name="onboarding_supported_services">ހިދުމަތްތައް</string>
|
||||
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</string>
|
||||
<string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string>
|
||||
<string name="onboarding_desc_1">BasedBank ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
|
||||
<string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string>
|
||||
<string name="onboarding_desc_2">އިތުރު ބޭންކްތަކަށް ސަޕޯޓް ލިބޭ ގޮތަށް ތައްޔާރުވަމުން ދަނީ. ދިވެހިރާއްޖޭގެ ބޭންކްތަކަށް ސަޕޯޓް ފަހި ވަމުން ދިޔަ ވަރަކަށް ހިމަނެމުން ދޭ.</string>
|
||||
<string name="onboarding_title_3">ފެށޭ ގޮތަށް ތައްޔާރު</string>
|
||||
<string name="onboarding_desc_3">ތިޔަ ބޭންކު ކްރެޑެންޝަލް ޖެހި، ތިޔަ އެކައުންޓްތައް ބަލާ. ތިޔަ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.</string>
|
||||
<string name="onboarding_desc_3">ތިޔަ ބޭންކު ކްރެޑެންޝަލް ޖެހި، ތިޔަ އެކައުންޓްތައް ބަލާ. ތިޔަ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.\n\nDhiraagu އާއި Ooredoo ގެ API ބޭނުންކޮށްގެން ފޯން ނަންބަރުގެ ތަފްސީލު ބެލޭ.\n\nމި އެޕް ތިޔައާ ބެހޭ ތަފްސީލެއް ނެހެދޭ، ޑިވެލޮޕަރަށް ވެސް ނުފޮނުވާ. ހުރިހާ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.</string>
|
||||
<string name="coming_soon">ފަހުން ލިބޭ</string>
|
||||
<string name="next">ދެން</string>
|
||||
<string name="get_started">ފަށާ</string>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
<string name="app_name">BasedBank</string>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<string name="onboarding_supported_services">Supported services</string>
|
||||
<string name="select_language">Select Language</string>
|
||||
<string name="onboarding_title_1">Your Banks, One App</string>
|
||||
<string name="onboarding_desc_1">BasedBank brings all your Maldivian bank accounts together in one place. Check balances, view accounts, and more — without switching between apps.</string>
|
||||
<string name="onboarding_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="onboarding_title_3">Before You Begin</string>
|
||||
<string name="onboarding_desc_3">BasedBank is an independent, third-party app. It is not affiliated with, endorsed by, or officially supported by any bank or financial institution.\n\nThis app works by logging into your internet banking services directly using your credentials and communicating with bank APIs. It is built using techniques derived from reverse-engineered official bank apps. Behaviour may change or break without notice if banks update their systems.\n\nDhiraagu and Ooredoo APIs are used to look up details about phone numbers.\n\nThis app does not collect any analytics, telemetry, or personal data, and does not transmit any information about you or your usage to the developer. Everything stays entirely on your device.\n\nBy tapping Get Started, you acknowledge and accept that:\n\n• Errors, failures, or service interruptions may occur at any time\n• Your bank may detect third-party access and apply restrictions or take other actions against your account\n• The developer of this app is not liable for any loss, damage, or consequences arising from your use of this app\n• You use this app entirely at your own risk</string>
|
||||
<string name="coming_soon">Coming Soon</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="get_started">Get Started</string>
|
||||
@@ -49,6 +51,9 @@
|
||||
<!-- Security setup -->
|
||||
<string name="security_setup">Secure Your App</string>
|
||||
<string name="security_setup_desc">Choose how you want to lock BasedBank when you\'re away.</string>
|
||||
<string name="security_already_configured">App Lock Configured</string>
|
||||
<string name="security_already_configured_desc">Your app lock is set up.</string>
|
||||
|
||||
<string name="method_pin">PIN Code</string>
|
||||
<string name="method_pin_desc">4–8 digit numeric PIN</string>
|
||||
<string name="method_pattern">Draw Pattern</string>
|
||||
@@ -104,6 +109,7 @@
|
||||
<string name="settings_security">Security</string>
|
||||
<string name="settings_change_lock">Change PIN / Pattern</string>
|
||||
<string name="settings_biometrics">Use biometrics</string>
|
||||
<string name="settings_biometrics_unavailable">No biometrics enrolled on this device</string>
|
||||
<string name="settings_biometrics_unlock">To unlock app</string>
|
||||
<string name="settings_biometrics_transfer">Confirm transfer</string>
|
||||
<string name="biometric_transfer_title">Confirm Transfer</string>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.9.1"
|
||||
agp = "8.7.3"
|
||||
kotlin = "2.1.21"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
|
||||
Reference in New Issue
Block a user