Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
94b280a177
|
|||
|
88c9f153e5
|
|||
|
eb7da01b2e
|
|||
|
27270f1b7a
|
|||
|
fd7fcb41a6
|
|||
|
c9ae614fc7
|
|||
|
b784085605
|
|||
|
01e5c17284
|
|||
|
6d3c7036b5
|
|||
|
804712d22d
|
|||
|
f208ee6ad1
|
|||
|
51dbed94d4
|
|||
|
0b5a452046
|
|||
|
00297da71e
|
|||
|
1602d061c1
|
|||
|
ddd64e8624
|
|||
|
77f367844d
|
|||
|
e2729b1d1a
|
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@@ -3,11 +3,11 @@
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-05-18T20:24:18.550107339Z">
|
||||
<option name="selectionMode" value="DIALOG" />
|
||||
<DropdownSelection timestamp="2026-05-22T00:11:32.873305232Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=683a9830" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
@@ -15,7 +15,7 @@
|
||||
<targets>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" />
|
||||
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=67d022c2" />
|
||||
</handle>
|
||||
</Target>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "sh.sar.basedbank"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 5
|
||||
versionName = "1.0.6"
|
||||
versionCode = 6
|
||||
versionName = "1.0.7"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
BIN
app/src/main/assets/cards/mib/visa_black_platinum.jpg
Normal file
BIN
app/src/main/assets/cards/mib/visa_black_platinum.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
BIN
app/src/main/assets/cards/mib/visa_blue_everyday.jpg
Normal file
BIN
app/src/main/assets/cards/mib/visa_blue_everyday.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
BIN
app/src/main/assets/cards/mib/visa_business.jpg
Normal file
BIN
app/src/main/assets/cards/mib/visa_business.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 633 KiB |
@@ -32,6 +32,8 @@ class LockActivity : AppCompatActivity() {
|
||||
private lateinit var salt: String
|
||||
private lateinit var storedHash: String
|
||||
private var biometricsEnabled = false
|
||||
private var autoUnlockPin = false
|
||||
private var pinLength = 4
|
||||
private var isVerifying = false
|
||||
|
||||
private val lockPrefs get() = getSharedPreferences("lock_attempts", MODE_PRIVATE)
|
||||
@@ -61,6 +63,8 @@ class LockActivity : AppCompatActivity() {
|
||||
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||
method = prefs.getString("security_method", "pin") ?: "pin"
|
||||
biometricsEnabled = prefs.getBoolean("biometrics_enabled", false)
|
||||
autoUnlockPin = prefs.getBoolean("auto_unlock_pin", false)
|
||||
pinLength = prefs.getInt("pin_length", 4)
|
||||
|
||||
val stored = CredentialStore(this).loadSecurityHash() ?: run { finish(); return }
|
||||
salt = stored.first
|
||||
@@ -134,13 +138,18 @@ class LockActivity : AppCompatActivity() {
|
||||
when (key) {
|
||||
"⌫" -> if (pinDigits.isNotEmpty()) { pinDigits.removeLast(); updateDots() }
|
||||
"✓" -> if (pinDigits.size >= 4) verifyPin()
|
||||
else -> if (pinDigits.size < 8) { pinDigits.add(key.toInt()); updateDots() }
|
||||
else -> if (pinDigits.size < 8) {
|
||||
pinDigits.add(key.toInt())
|
||||
updateDots()
|
||||
if (autoUnlockPin && pinDigits.size == pinLength) verifyPin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDots() {
|
||||
val n = pinDigits.size
|
||||
binding.tvLockPinDots.text = "●".repeat(n) + "○".repeat(maxOf(4 - n, 0))
|
||||
val total = if (autoUnlockPin) pinLength else maxOf(n, 4)
|
||||
binding.tvLockPinDots.text = "●".repeat(n) + "○".repeat(maxOf(total - n, 0))
|
||||
}
|
||||
|
||||
private fun verifyPin() {
|
||||
|
||||
@@ -30,6 +30,14 @@ class BmlAccountClient {
|
||||
return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId)
|
||||
}
|
||||
|
||||
/** Lightweight call to verify the session is alive. Throws [AuthExpiredException] on 401/419. */
|
||||
fun checkProfile(session: BmlSession) {
|
||||
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/profile")).execute()
|
||||
val code = resp.code
|
||||
resp.close()
|
||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||
}
|
||||
|
||||
fun fetchUserInfo(session: BmlSession): BmlUserInfo? {
|
||||
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/userinfo")).execute()
|
||||
val json = resp.body?.string() ?: return null
|
||||
@@ -73,6 +81,27 @@ class BmlAccountClient {
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun fetchTransferChannels(session: BmlSession): List<BmlOtpChannel> {
|
||||
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/transfer")).execute()
|
||||
val json = resp.body?.string() ?: run { resp.close(); return emptyList() }
|
||||
resp.close()
|
||||
return try {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return emptyList()
|
||||
val arr = root.optJSONObject("payload")
|
||||
?.optJSONObject("transfer")
|
||||
?.optJSONArray("otpChannel") ?: return emptyList()
|
||||
(0 until arr.length()).map { i ->
|
||||
val ch = arr.getJSONObject(i)
|
||||
BmlOtpChannel(
|
||||
channel = ch.optString("channel"),
|
||||
description = ch.optString("description"),
|
||||
masked = ch.optString("masked")
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
private fun parseDashboard(
|
||||
json: String,
|
||||
loginTag: String,
|
||||
|
||||
@@ -310,13 +310,47 @@ class BmlLoginFlow {
|
||||
val tokenJson = tokenResp.body?.string() ?: throw Exception("Empty token response")
|
||||
tokenResp.close()
|
||||
|
||||
val accessToken = JSONObject(tokenJson).optString("access_token")
|
||||
val tokenObj = JSONObject(tokenJson)
|
||||
val accessToken = tokenObj.optString("access_token")
|
||||
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
|
||||
val refreshToken = tokenObj.optString("refresh_token", "")
|
||||
val expiresIn = tokenObj.optLong("expires_in", 0L)
|
||||
val expiresAt = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000L else 0L
|
||||
|
||||
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
|
||||
val session = BmlSession(accessToken = accessToken, deviceId = deviceId, refreshToken = refreshToken, expiresAt = expiresAt)
|
||||
val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profileName, profileId)
|
||||
return Pair(session, accounts)
|
||||
}
|
||||
// ─── Token refresh ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Uses the saved refresh token to obtain a new access token without re-login.
|
||||
* Returns a new [BmlSession] with updated tokens.
|
||||
*/
|
||||
fun refreshSession(session: BmlSession): BmlSession {
|
||||
val body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("refresh_token", session.refreshToken)
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("Device-ID", session.deviceId)
|
||||
.add("User-Agent", APP_USER_AGENT)
|
||||
.add("x-app-version", APP_VERSION)
|
||||
.build()
|
||||
val resp = newBmlApiClient().newCall(
|
||||
Request.Builder().url("$BASE_URL/oauth/token").post(body)
|
||||
.header("User-Agent", WEB_USER_AGENT).build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: throw Exception("Empty refresh response")
|
||||
resp.close()
|
||||
val obj = JSONObject(json)
|
||||
val newAccess = obj.optString("access_token").takeIf { it.isNotBlank() }
|
||||
?: throw Exception("Token refresh failed")
|
||||
val newRefresh = obj.optString("refresh_token", "").ifBlank { session.refreshToken }
|
||||
val expiresIn = obj.optLong("expires_in", 0L)
|
||||
val expiresAt = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000L else 0L
|
||||
return BmlSession(accessToken = newAccess, deviceId = session.deviceId, refreshToken = newRefresh, expiresAt = expiresAt)
|
||||
}
|
||||
|
||||
// ─── Parsing ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,8 +4,12 @@ import sh.sar.basedbank.api.models.BankAccount
|
||||
|
||||
data class BmlSession(
|
||||
val accessToken: String,
|
||||
val deviceId: String
|
||||
)
|
||||
val deviceId: String,
|
||||
val refreshToken: String = "",
|
||||
val expiresAt: Long = 0L // Unix millis; 0 = unknown
|
||||
) {
|
||||
fun isExpired() = expiresAt > 0L && System.currentTimeMillis() >= expiresAt
|
||||
}
|
||||
|
||||
data class BmlProfile(
|
||||
val profileId: String,
|
||||
|
||||
@@ -17,7 +17,8 @@ class BmlTransferClient {
|
||||
amount: Double,
|
||||
transferType: String,
|
||||
currency: String,
|
||||
bank: String? = null
|
||||
bank: String? = null,
|
||||
channel: String = "token"
|
||||
): Boolean {
|
||||
val jo = JSONObject().apply {
|
||||
put("debitAccount", debitAccount)
|
||||
@@ -25,7 +26,7 @@ class BmlTransferClient {
|
||||
put("debitAmount", amount)
|
||||
put("transfertype", transferType)
|
||||
put("currency", currency)
|
||||
put("channel", "token")
|
||||
put("channel", channel)
|
||||
if (bank != null) put("bank", bank)
|
||||
}
|
||||
val request = Request.Builder()
|
||||
@@ -55,7 +56,8 @@ class BmlTransferClient {
|
||||
currency: String,
|
||||
otp: String,
|
||||
remarks: String = "",
|
||||
bank: String? = null
|
||||
bank: String? = null,
|
||||
channel: String = "token"
|
||||
): BmlTransferResult {
|
||||
val jo = JSONObject().apply {
|
||||
put("debitAccount", debitAccount)
|
||||
@@ -63,7 +65,7 @@ class BmlTransferClient {
|
||||
put("debitAmount", amount)
|
||||
put("transfertype", transferType)
|
||||
put("currency", currency)
|
||||
put("channel", "token")
|
||||
put("channel", channel)
|
||||
put("otp", otp)
|
||||
if (remarks.isNotBlank()) put("remarks", remarks)
|
||||
if (bank != null) put("bank", bank)
|
||||
|
||||
62
app/src/main/java/sh/sar/basedbank/api/mib/MibCardsClient.kt
Normal file
62
app/src/main/java/sh/sar/basedbank/api/mib/MibCardsClient.kt
Normal file
@@ -0,0 +1,62 @@
|
||||
package sh.sar.basedbank.api.mib
|
||||
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MibCardsClient {
|
||||
|
||||
private val BASE_WV_URL = "https://faisamobilex-wv.mib.com.mv"
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private fun cookieHeader(session: MibSession) =
|
||||
"mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " +
|
||||
"mbnonce=${session.nonceGenerator}; time-tracker=597"
|
||||
|
||||
fun fetchCards(session: MibSession, loginTag: String): List<MibCard> {
|
||||
val body = FormBody.Builder()
|
||||
.add("name", "")
|
||||
.add("start", "1")
|
||||
.add("end", "50")
|
||||
.add("includeCount", "1")
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_WV_URL/ajaxDebitCard/fetchCardInfos")
|
||||
.post(body)
|
||||
.header("Cookie", cookieHeader(session))
|
||||
.header("User-Agent", "Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36")
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.header("Accept", "*/*")
|
||||
.header("Origin", BASE_WV_URL)
|
||||
.header("Referer", "$BASE_WV_URL//debitCards?dashurl=1")
|
||||
.build()
|
||||
|
||||
return client.newCall(request).execute().use { response ->
|
||||
val bodyStr = response.body?.string() ?: return emptyList()
|
||||
val json = try { JSONObject(bodyStr) } catch (_: Exception) { return emptyList() }
|
||||
if (!json.optBoolean("success")) return emptyList()
|
||||
val data = json.optJSONArray("data") ?: return emptyList()
|
||||
(0 until data.length()).map { i ->
|
||||
val item = data.getJSONObject(i)
|
||||
MibCard(
|
||||
cardId = item.optString("cardId"),
|
||||
maskedCardNumber = item.optString("maskedCardNumber"),
|
||||
cardStatus = item.optString("cardStatus"),
|
||||
cardType = item.optString("cardType"),
|
||||
cardTypeDesc = item.optString("cardTypeDesc"),
|
||||
customerId = item.optString("customerId"),
|
||||
phoneNumber = item.optString("phoneNumber"),
|
||||
cardHolderName = item.optString("cardHolderName"),
|
||||
loginTag = loginTag
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,18 @@ data class MibIpsAccountInfo(
|
||||
)
|
||||
|
||||
|
||||
data class MibCard(
|
||||
val cardId: String,
|
||||
val maskedCardNumber: String,
|
||||
val cardStatus: String,
|
||||
val cardType: String,
|
||||
val cardTypeDesc: String,
|
||||
val customerId: String,
|
||||
val phoneNumber: String,
|
||||
val cardHolderName: String,
|
||||
val loginTag: String
|
||||
)
|
||||
|
||||
data class MibFinanceDeal(
|
||||
val dealNo: String,
|
||||
val productDesc: String,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.databinding.FragmentCardSettingsBinding
|
||||
|
||||
class CardSettingsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentCardSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentCardSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = CardSettingsAdapter(emptyList(), requireContext())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||
insets
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards == null) return@observe
|
||||
adapter.update(cards)
|
||||
binding.loadingView.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
if (viewModel.mibCards.value == null) {
|
||||
binding.loadingView.visibility = View.VISIBLE
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = getString(R.string.nav_card_settings)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class CardSettingsAdapter(
|
||||
private var cards: List<MibCard>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(context).inflate(R.layout.item_card_settings_entry, parent, false))
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
|
||||
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
|
||||
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
|
||||
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
|
||||
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
|
||||
private val btnChangePin: View = view.findViewById(R.id.btnChangePin)
|
||||
private val btnFreeze: View = view.findViewById(R.id.btnFreeze)
|
||||
private val btnBlock: View = view.findViewById(R.id.btnBlock)
|
||||
|
||||
fun bind(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
|
||||
tvCardType.text = card.cardTypeDesc
|
||||
val assetPath = PayWithCardFragment.cardImageAsset(card)
|
||||
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
val wip = View.OnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
btnChangePin.setOnClickListener(wip)
|
||||
btnFreeze.setOnClickListener(wip)
|
||||
btnBlock.setOnClickListener(wip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,21 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||
import kotlin.math.abs
|
||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||
@@ -45,6 +52,17 @@ class DashboardFragment : Fragment() {
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
|
||||
val cardAdapter = DashboardCardAdapter()
|
||||
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.rvCards.adapter = cardAdapter
|
||||
LinearSnapHelper().attachToRecyclerView(binding.rvCards)
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards.isNullOrEmpty()) return@observe
|
||||
cardAdapter.update(cards)
|
||||
binding.sectionCards.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.buttonBar) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
@@ -78,7 +96,7 @@ class DashboardFragment : Fragment() {
|
||||
private fun updateBalances(accounts: List<BankAccount>) {
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
|
||||
val nonCreditAccounts = accounts.filter { it.profileType != "BML_CREDIT" }
|
||||
val nonCreditAccounts = accounts.filter { it.profileType != "BML_CREDIT" && it.profileType != "BML_LOAN" }
|
||||
val creditAccounts = accounts.filter { it.profileType == "BML_CREDIT" }
|
||||
|
||||
if (hide) {
|
||||
@@ -209,4 +227,51 @@ class DashboardFragment : Fragment() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class DashboardCardAdapter : RecyclerView.Adapter<DashboardCardAdapter.VH>() {
|
||||
private var cards: List<MibCard> = emptyList()
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_card_dashboard, parent, false)
|
||||
return VH(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
|
||||
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
|
||||
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
|
||||
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
|
||||
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
|
||||
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
|
||||
|
||||
fun bind(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
|
||||
val assetPath = PayWithCardFragment.cardImageAsset(card)
|
||||
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
btnPayQr.setOnClickListener {
|
||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val nfcAdapter = android.nfc.NfcAdapter.getDefaultAdapter(requireContext())
|
||||
val nfcSupported = nfcAdapter != null
|
||||
btnPayNfc.isEnabled = nfcSupported
|
||||
if (nfcSupported) {
|
||||
btnPayNfc.setOnClickListener {
|
||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
btnPayNfc.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import android.widget.Toast
|
||||
import sh.sar.basedbank.ui.home.NavCustomization
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
@@ -37,7 +39,6 @@ import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.AuthExpiredException
|
||||
import sh.sar.basedbank.api.bml.BmlAccountClient
|
||||
import sh.sar.basedbank.api.bml.BmlActivationResult
|
||||
import sh.sar.basedbank.api.bml.BmlContactsClient
|
||||
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
|
||||
import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||
@@ -54,6 +55,7 @@ import sh.sar.basedbank.ui.login.LoginActivity
|
||||
import sh.sar.basedbank.api.models.BankContact
|
||||
import sh.sar.basedbank.api.models.BankContactCategory
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.api.mib.MibCardsClient
|
||||
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||
import sh.sar.basedbank.api.mib.MibProfile
|
||||
import sh.sar.basedbank.api.mib.MibSession
|
||||
@@ -61,6 +63,7 @@ import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||
import sh.sar.basedbank.util.AccountCache
|
||||
import sh.sar.basedbank.util.ContactsCache
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.util.CardsCache
|
||||
import sh.sar.basedbank.util.FinancingCache
|
||||
import sh.sar.basedbank.util.ForeignLimitsCache
|
||||
|
||||
@@ -71,6 +74,10 @@ class HomeActivity : AppCompatActivity() {
|
||||
private lateinit var toggle: ActionBarDrawerToggle
|
||||
private var suppressBottomNavCallback = false
|
||||
|
||||
private var backPressedOnce = false
|
||||
private val backPressHandler = Handler(Looper.getMainLooper())
|
||||
private val resetBackPress = Runnable { backPressedOnce = false }
|
||||
|
||||
private val autolockHandler = Handler(Looper.getMainLooper())
|
||||
private var warningDialog: AlertDialog? = null
|
||||
private var countdownTimer: CountDownTimer? = null
|
||||
@@ -138,6 +145,8 @@ class HomeActivity : AppCompatActivity() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
else -> null
|
||||
}
|
||||
if (frag != null) show(frag)
|
||||
@@ -169,6 +178,8 @@ class HomeActivity : AppCompatActivity() {
|
||||
byLoginId.forEach { (loginId, accs) -> AccountCache.saveFahipay(this, loginId, accs) }
|
||||
}
|
||||
|
||||
val cachedCards = CardsCache.load(this)
|
||||
if (cachedCards.isNotEmpty()) viewModel.mibCards.value = cachedCards
|
||||
val cachedFinancing = FinancingCache.load(this)
|
||||
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
|
||||
@@ -182,6 +193,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
}
|
||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||
refreshBmlLoanDetails()
|
||||
triggerRefreshCards()
|
||||
} else {
|
||||
// Came from lock screen — show caches immediately, refresh everything in background
|
||||
val store = CredentialStore(this)
|
||||
@@ -190,6 +202,8 @@ class HomeActivity : AppCompatActivity() {
|
||||
val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds())
|
||||
val merged = cachedMib + cachedBml + cachedFahipay
|
||||
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
||||
val cachedCards = CardsCache.load(this)
|
||||
if (cachedCards.isNotEmpty()) viewModel.mibCards.value = cachedCards
|
||||
val cachedFinancing = FinancingCache.load(this)
|
||||
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
|
||||
@@ -208,6 +222,42 @@ class HomeActivity : AppCompatActivity() {
|
||||
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
// Close drawer if open (drawer-nav mode)
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawers()
|
||||
return
|
||||
}
|
||||
// Pop fragment back stack if there's anything on it (e.g. showWithBackStack)
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
return
|
||||
}
|
||||
// In bottom nav mode, pressing back navigates up the hierarchy
|
||||
val isBottomNav = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
if (isBottomNav && binding.bottomNavigation.selectedItemId != R.id.nav_dashboard) {
|
||||
val currentFrag = supportFragmentManager.findFragmentById(R.id.contentFrame)
|
||||
// Sub-page reached via More (e.g. Settings, Activities) — go back to More
|
||||
if (binding.bottomNavigation.selectedItemId == R.id.nav_more && currentFrag !is MoreFragment) {
|
||||
show(MoreFragment())
|
||||
return
|
||||
}
|
||||
binding.bottomNavigation.selectedItemId = R.id.nav_dashboard
|
||||
return
|
||||
}
|
||||
// At top level — require double-tap to exit
|
||||
if (backPressedOnce) {
|
||||
backPressHandler.removeCallbacks(resetBackPress)
|
||||
finish()
|
||||
} else {
|
||||
backPressedOnce = true
|
||||
Toast.makeText(this@HomeActivity, R.string.press_back_to_exit, Toast.LENGTH_SHORT).show()
|
||||
backPressHandler.postDelayed(resetBackPress, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Keep all MIB sessions alive every 25 seconds while the app is in the foreground
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
@@ -307,6 +357,8 @@ fun applyNavLabelVisibility() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
|
||||
}
|
||||
show(dest)
|
||||
@@ -521,34 +573,63 @@ fun applyNavLabelVisibility() {
|
||||
}
|
||||
|
||||
// One async job per BML login, all run in parallel
|
||||
val bmlJobs = bmlLoginIds.mapNotNull { loginId ->
|
||||
val creds = store.loadBmlCredentials(loginId) ?: return@mapNotNull null
|
||||
val bmlJobs = bmlLoginIds.map { loginId ->
|
||||
loginId to async(Dispatchers.IO) {
|
||||
val loginTag = "bml_$loginId"
|
||||
val app = application as BasedBankApp
|
||||
val savedProfiles = store.loadBmlProfiles(loginId)
|
||||
val allAccounts = mutableListOf<BankAccount>()
|
||||
var anyExpired = savedProfiles.isEmpty()
|
||||
|
||||
// Try each saved profile's cached session
|
||||
for (profile in savedProfiles) {
|
||||
val saved = store.loadBmlProfileSession(profile.profileId)
|
||||
if (saved != null) {
|
||||
try {
|
||||
val session = BmlSession(saved.first, saved.second)
|
||||
val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profile.name, profile.profileId)
|
||||
app.bmlSessions[profile.profileId] = session
|
||||
allAccounts += accounts
|
||||
} catch (_: AuthExpiredException) { anyExpired = true
|
||||
} catch (_: Exception) { anyExpired = true }
|
||||
} else {
|
||||
anyExpired = true
|
||||
}
|
||||
}
|
||||
|
||||
if (savedProfiles.isNotEmpty()) app.bmlProfilesMap[loginId] = savedProfiles
|
||||
|
||||
// Also try legacy single-profile session token (pre-multi-profile installs)
|
||||
val bmlClient = BmlAccountClient()
|
||||
for (profile in savedProfiles) {
|
||||
val saved = store.loadBmlProfileSession(profile.profileId)
|
||||
val refreshToken = store.loadBmlProfileRefreshToken(profile.profileId)
|
||||
if (saved == null) {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
continue
|
||||
}
|
||||
val expiresAt = store.loadBmlProfileExpiresAt(profile.profileId)
|
||||
val tokenKnownExpired = expiresAt > 0L && System.currentTimeMillis() >= expiresAt
|
||||
|
||||
suspend fun fetchWithSession(session: BmlSession) {
|
||||
bmlClient.checkProfile(session)
|
||||
val accounts = bmlClient.fetchAccounts(session, loginTag, profile.name, profile.profileId)
|
||||
app.bmlSessions[profile.profileId] = session
|
||||
allAccounts += accounts
|
||||
}
|
||||
|
||||
suspend fun tryRefresh() {
|
||||
if (refreshToken == null) throw Exception("No refresh token")
|
||||
val oldSession = BmlSession(saved.first, saved.second, refreshToken)
|
||||
val newSession = app.bmlFlowFor(loginId).refreshSession(oldSession)
|
||||
store.saveBmlProfileSession(profile.profileId, newSession.accessToken, newSession.deviceId)
|
||||
if (newSession.refreshToken.isNotBlank())
|
||||
store.saveBmlProfileRefreshToken(profile.profileId, newSession.refreshToken)
|
||||
if (newSession.expiresAt > 0)
|
||||
store.saveBmlProfileExpiresAt(profile.profileId, newSession.expiresAt)
|
||||
fetchWithSession(newSession)
|
||||
}
|
||||
|
||||
try {
|
||||
if (tokenKnownExpired) {
|
||||
tryRefresh()
|
||||
} else {
|
||||
try {
|
||||
fetchWithSession(BmlSession(saved.first, saved.second))
|
||||
} catch (_: AuthExpiredException) {
|
||||
tryRefresh()
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy single-profile session (pre-multi-profile installs)
|
||||
if (savedProfiles.isEmpty()) {
|
||||
val legacyToken = store.loadBmlSession(loginId)
|
||||
if (legacyToken != null) {
|
||||
@@ -557,47 +638,11 @@ fun applyNavLabelVisibility() {
|
||||
val accounts = BmlAccountClient().fetchAccounts(session, loginTag)
|
||||
app.bmlSessions[loginId] = session
|
||||
allAccounts += accounts
|
||||
anyExpired = false
|
||||
} catch (_: AuthExpiredException) { anyExpired = true
|
||||
} catch (_: Exception) { anyExpired = true }
|
||||
}
|
||||
}
|
||||
|
||||
if (anyExpired || allAccounts.isEmpty()) {
|
||||
// Re-authenticate to refresh personal profile sessions
|
||||
try {
|
||||
val flow = app.bmlFlowFor(loginId)
|
||||
val profiles = flow.login(creds.username, creds.password, creds.otpSeed)
|
||||
store.saveBmlProfiles(loginId, profiles)
|
||||
app.bmlProfilesMap[loginId] = profiles
|
||||
|
||||
for (profile in profiles) {
|
||||
if (profile.profileType == "business") {
|
||||
// Can't activate business profiles without user OTP — use cached
|
||||
val cached = AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
if (allAccounts.none { it.profileId == profile.profileId })
|
||||
allAccounts += cached
|
||||
continue
|
||||
}
|
||||
try {
|
||||
val result = flow.activateProfile(profile, loginTag)
|
||||
if (result is BmlActivationResult.Success) {
|
||||
store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId)
|
||||
app.bmlSessions[profile.profileId] = result.session
|
||||
allAccounts.removeAll { it.profileId == profile.profileId }
|
||||
allAccounts += result.accounts
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
if (allAccounts.none { it.profileId == profile.profileId }) {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
if (allAccounts.isEmpty())
|
||||
} catch (_: Exception) {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
}
|
||||
} else {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,6 +714,10 @@ fun applyNavLabelVisibility() {
|
||||
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||
}
|
||||
refreshBmlLoanDetails()
|
||||
for ((loginId, session) in app.mibSessions) {
|
||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||
refreshMibCards(loginId, session, profiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,6 +981,44 @@ fun applyNavLabelVisibility() {
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerRefreshCards() {
|
||||
val app = application as BasedBankApp
|
||||
for ((loginId, session) in app.mibSessions) {
|
||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||
refreshMibCards(loginId, session, profiles)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshMibCards(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||
if (profiles.isEmpty()) return
|
||||
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||
val client = MibCardsClient()
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val cards = withContext(Dispatchers.IO) {
|
||||
val result = mutableListOf<sh.sar.basedbank.api.mib.MibCard>()
|
||||
val seen = mutableSetOf<String>()
|
||||
for (profile in profiles) {
|
||||
try {
|
||||
flow.switchProfile(session, profile)
|
||||
for (card in client.fetchCards(session, "mib_$loginId")) {
|
||||
if (seen.add(card.cardId)) result += card
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
result
|
||||
}
|
||||
if (cards.isNotEmpty()) {
|
||||
val existing = viewModel.mibCards.value?.toMutableList() ?: mutableListOf()
|
||||
existing.removeAll { it.loginTag == "mib_$loginId" }
|
||||
existing += cards
|
||||
viewModel.mibCards.postValue(existing)
|
||||
CardsCache.save(this@HomeActivity, existing)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||
if (profiles.isEmpty()) return
|
||||
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||
|
||||
@@ -7,6 +7,7 @@ import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.api.models.BankContact
|
||||
import sh.sar.basedbank.api.models.BankContactCategory
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
@@ -20,5 +21,7 @@ class HomeViewModel : ViewModel() {
|
||||
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
||||
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
||||
|
||||
val mibCards = MutableLiveData<List<MibCard>?>(null)
|
||||
|
||||
val hideAmounts = MutableLiveData<Boolean>(false)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class MoreFragment : Fragment() {
|
||||
val row = inflater.inflate(R.layout.item_more_nav, list, false)
|
||||
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
|
||||
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
|
||||
row.findViewById<TextView>(R.id.tvDescription).setText(item.descriptionRes)
|
||||
row.setOnClickListener { (requireActivity() as HomeActivity).navigateTo(item.id) }
|
||||
list.addView(row)
|
||||
}
|
||||
|
||||
@@ -10,21 +10,23 @@ object NavCustomization {
|
||||
data class NavItemDef(
|
||||
val id: Int,
|
||||
@DrawableRes val iconRes: Int,
|
||||
@StringRes val titleRes: Int
|
||||
@StringRes val titleRes: Int,
|
||||
@StringRes val descriptionRes: Int
|
||||
)
|
||||
|
||||
/** All items that can occupy either a bottom nav slot or the "More" screen. */
|
||||
val ALL_SWAPPABLE = listOf(
|
||||
NavItemDef(R.id.nav_accounts, R.drawable.ic_nav_accounts, R.string.nav_accounts),
|
||||
NavItemDef(R.id.nav_contacts, R.drawable.ic_contacts, R.string.nav_contacts),
|
||||
NavItemDef(R.id.nav_transfer, R.drawable.ic_send, R.string.transfer),
|
||||
NavItemDef(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr),
|
||||
NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities),
|
||||
NavItemDef(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history),
|
||||
NavItemDef(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances),
|
||||
NavItemDef(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings),
|
||||
NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp),
|
||||
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings),
|
||||
NavItemDef(R.id.nav_accounts, R.drawable.ic_nav_accounts, R.string.nav_accounts, R.string.nav_desc_accounts),
|
||||
NavItemDef(R.id.nav_contacts, R.drawable.ic_contacts, R.string.nav_contacts, R.string.nav_desc_contacts),
|
||||
NavItemDef(R.id.nav_transfer, R.drawable.ic_send, R.string.transfer, R.string.nav_desc_transfer),
|
||||
NavItemDef(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr, R.string.nav_desc_pay_mv_qr),
|
||||
NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities, R.string.nav_desc_activities),
|
||||
NavItemDef(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history, R.string.nav_desc_transfer_history),
|
||||
NavItemDef(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances, R.string.nav_desc_finances),
|
||||
NavItemDef(R.id.nav_pay_with_card, R.drawable.ic_nav_card, R.string.nav_pay_with_card, R.string.nav_desc_pay_with_card),
|
||||
NavItemDef(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings, R.string.nav_desc_card_settings),
|
||||
NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp, R.string.nav_desc_otp),
|
||||
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings, R.string.nav_desc_settings),
|
||||
)
|
||||
|
||||
fun getSlots(prefs: SharedPreferences): List<Int> = listOf(
|
||||
|
||||
@@ -77,8 +77,10 @@ class PayMvQrFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val basePaddingBottom = view.paddingBottom
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.updatePadding(bottom = basePaddingBottom + navBar.bottom)
|
||||
val bottomNav = activity?.findViewById<View>(R.id.bottomNavigation)
|
||||
val navBarBottom = if (bottomNav?.visibility == View.VISIBLE) 0
|
||||
else insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||
v.updatePadding(bottom = basePaddingBottom + navBarBottom)
|
||||
insets
|
||||
}
|
||||
setupDropdown()
|
||||
@@ -95,7 +97,7 @@ class PayMvQrFragment : Fragment() {
|
||||
private fun setupDropdown() {
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||
val eligible = accounts.filter {
|
||||
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT"
|
||||
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_LOAN"
|
||||
}
|
||||
val adapter = QrAccountAdapter(requireContext(), eligible)
|
||||
binding.actvAccount.setAdapter(adapter)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding
|
||||
import sh.sar.basedbank.util.CardsCache
|
||||
|
||||
class PayWithCardFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentPayWithCardBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentPayWithCardBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = CardWalletAdapter(emptyList(), requireContext())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||
insets
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards == null) return@observe
|
||||
adapter.update(cards)
|
||||
binding.loadingView.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
val cached = CardsCache.load(requireContext())
|
||||
if (cached.isNotEmpty()) {
|
||||
viewModel.mibCards.value = cached
|
||||
} else {
|
||||
binding.loadingView.visibility = View.VISIBLE
|
||||
}
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = getString(R.string.nav_pay_with_card)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class CardWalletAdapter(
|
||||
private var cards: List<MibCard>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(context).inflate(R.layout.item_card_wallet, parent, false))
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
|
||||
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
|
||||
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
|
||||
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
|
||||
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
|
||||
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
|
||||
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
|
||||
|
||||
fun bind(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = formatMasked(card.maskedCardNumber)
|
||||
tvCardType.text = card.cardTypeDesc
|
||||
val assetPath = cardImageAsset(card)
|
||||
if (assetPath != null) loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
btnPayQr.setOnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val nfcAdapter = android.nfc.NfcAdapter.getDefaultAdapter(context)
|
||||
val nfcSupported = nfcAdapter != null
|
||||
btnPayNfc.isEnabled = nfcSupported
|
||||
if (nfcSupported) {
|
||||
btnPayNfc.setOnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
btnPayNfc.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
|
||||
"53" -> "cards/mib/visa_black_platinum.jpg"
|
||||
"57" -> "cards/mib/visa_blue_everyday.jpg"
|
||||
"70" -> "cards/mib/visa_business.jpg"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun loadCardImage(imageView: ImageView, assetPath: String) {
|
||||
try {
|
||||
val bitmap = imageView.context.assets.open(assetPath).use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
}
|
||||
imageView.setImageBitmap(bitmap)
|
||||
} catch (_: Exception) {
|
||||
imageView.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatMasked(masked: String): String {
|
||||
if (masked.length < 4) return masked
|
||||
return "\u2022\u2022\u2022\u2022 ${masked.takeLast(4)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,15 @@ class SettingsFragment : Fragment() {
|
||||
private data class SettingsItem(
|
||||
@DrawableRes val icon: Int,
|
||||
@StringRes val title: Int,
|
||||
@StringRes val description: Int,
|
||||
val dest: () -> Fragment
|
||||
)
|
||||
|
||||
private val items = listOf(
|
||||
SettingsItem(R.drawable.ic_contacts, R.string.settings_logins) { SettingsLoginsFragment() },
|
||||
SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance) { SettingsAppearanceFragment() },
|
||||
SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security) { SettingsSecurityFragment() },
|
||||
SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage) { SettingsStorageFragment() },
|
||||
SettingsItem(R.drawable.ic_contacts, R.string.settings_logins, R.string.settings_desc_logins) { SettingsLoginsFragment() },
|
||||
SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance, R.string.settings_desc_appearance) { SettingsAppearanceFragment() },
|
||||
SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security, R.string.settings_desc_privacy_security) { SettingsSecurityFragment() },
|
||||
SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage, R.string.settings_desc_storage) { SettingsStorageFragment() },
|
||||
)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
@@ -37,6 +38,7 @@ class SettingsFragment : Fragment() {
|
||||
val row = inflater.inflate(R.layout.item_more_nav, list, false)
|
||||
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.icon)
|
||||
row.findViewById<TextView>(R.id.tvLabel).setText(item.title)
|
||||
row.findViewById<TextView>(R.id.tvDescription).setText(item.description)
|
||||
row.setOnClickListener {
|
||||
(requireActivity() as HomeActivity).showWithBackStack(item.dest())
|
||||
}
|
||||
|
||||
@@ -433,6 +433,10 @@ class SettingsLoginsFragment : Fragment() {
|
||||
return when (activationResult) {
|
||||
is BmlActivationResult.Success -> {
|
||||
store.saveBmlProfileSession(profile.profileId, activationResult.session.accessToken, activationResult.session.deviceId)
|
||||
if (activationResult.session.refreshToken.isNotBlank())
|
||||
store.saveBmlProfileRefreshToken(profile.profileId, activationResult.session.refreshToken)
|
||||
if (activationResult.session.expiresAt > 0)
|
||||
store.saveBmlProfileExpiresAt(profile.profileId, activationResult.session.expiresAt)
|
||||
true
|
||||
}
|
||||
is BmlActivationResult.NeedsBusinessOtp ->
|
||||
@@ -475,6 +479,10 @@ class SettingsLoginsFragment : Fragment() {
|
||||
}
|
||||
verifyProgress.dismiss()
|
||||
store.saveBmlProfileSession(profile.profileId, session.accessToken, session.deviceId)
|
||||
if (session.refreshToken.isNotBlank())
|
||||
store.saveBmlProfileRefreshToken(profile.profileId, session.refreshToken)
|
||||
if (session.expiresAt > 0)
|
||||
store.saveBmlProfileExpiresAt(profile.profileId, session.expiresAt)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
verifyProgress.dismiss()
|
||||
|
||||
@@ -31,6 +31,15 @@ class SettingsSecurityFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
// Auto unlock on correct PIN (only for pin method)
|
||||
if (prefs.getString("security_method", null) == "pin") {
|
||||
binding.rowAutoUnlockPin.visibility = View.VISIBLE
|
||||
binding.switchAutoUnlockPin.isChecked = prefs.getBoolean("auto_unlock_pin", false)
|
||||
binding.switchAutoUnlockPin.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean("auto_unlock_pin", isChecked).apply()
|
||||
}
|
||||
}
|
||||
|
||||
// Biometrics
|
||||
val canUseBiometrics = BiometricManager.from(requireContext())
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|
||||
@@ -15,6 +15,8 @@ import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.graphics.Typeface
|
||||
import android.view.Gravity
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@@ -36,6 +38,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlAccountClient
|
||||
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
||||
import sh.sar.basedbank.api.bml.BmlTransferClient
|
||||
import sh.sar.basedbank.api.bml.BmlTransferResult
|
||||
import sh.sar.basedbank.api.bml.BmlValidateClient
|
||||
@@ -83,6 +87,28 @@ class TransferFragment : Fragment() {
|
||||
// Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL"
|
||||
private var selectedFahipayService: String? = null
|
||||
|
||||
// BML business profile OTP flow state
|
||||
private enum class BmlOtpState { NONE, SELECTING_CHANNEL, AWAITING_OTP }
|
||||
private var bmlOtpState = BmlOtpState.NONE
|
||||
private var bmlOtpChannel: String? = null
|
||||
|
||||
private data class PendingBmlTransfer(
|
||||
val src: BankAccount,
|
||||
val debitAccount: String,
|
||||
val creditAccount: String,
|
||||
val amount: Double,
|
||||
val amountStr: String,
|
||||
val remarks: String,
|
||||
val transferType: String,
|
||||
val currency: String,
|
||||
val bank: String?,
|
||||
val destDisplay: String,
|
||||
val destAccount: String,
|
||||
val toBank: String,
|
||||
val toAvatar: Bitmap?
|
||||
)
|
||||
private var pendingBmlTransfer: PendingBmlTransfer? = null
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
@@ -171,7 +197,10 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.btnTransfer.isEnabled = false
|
||||
binding.btnTransfer.setOnClickListener { initiateTransfer() }
|
||||
binding.btnTransfer.setOnClickListener {
|
||||
if (bmlOtpState == BmlOtpState.AWAITING_OTP) verifyBmlOtp()
|
||||
else initiateTransfer()
|
||||
}
|
||||
|
||||
binding.etAmount.addTextChangedListener { updateTransferButton() }
|
||||
|
||||
@@ -602,6 +631,7 @@ class TransferFragment : Fragment() {
|
||||
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
||||
|
||||
val isSrcBml = src.bank == "BML"
|
||||
val isBmlBusiness = isSrcBml && isBusinessProfile(src) // to test on personal accounts: use `isSrcBml`
|
||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||
val currency = src.currencyName.ifBlank { "MVR" }
|
||||
@@ -636,26 +666,34 @@ class TransferFragment : Fragment() {
|
||||
val mainMsg = "Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}"
|
||||
|
||||
val doTransfer: () -> Unit = {
|
||||
binding.btnTransfer.isEnabled = false
|
||||
(activity as? HomeActivity)?.setRefreshing(true)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||
if (!isSrcBml) {
|
||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
||||
} else {
|
||||
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||
if (isBmlBusiness) {
|
||||
// Business profile: async OTP channel selection flow
|
||||
startBmlBusinessOtpFlow(
|
||||
src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks,
|
||||
isSrcCard, isDestMib, currency, allAccounts, allContacts, capturedToAvatar
|
||||
)
|
||||
} else {
|
||||
binding.btnTransfer.isEnabled = false
|
||||
(activity as? HomeActivity)?.setRefreshing(true)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||
if (!isSrcBml) {
|
||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
||||
} else {
|
||||
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||
}
|
||||
}
|
||||
binding.btnTransfer.isEnabled = true
|
||||
(activity as? HomeActivity)?.setRefreshing(false)
|
||||
if (ok && receipt != null) {
|
||||
ReceiptStore.save(requireContext(), receipt)
|
||||
clearForm()
|
||||
val activity = requireActivity() as HomeActivity
|
||||
activity.triggerRefresh()
|
||||
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
||||
} else if (!ok) {
|
||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
binding.btnTransfer.isEnabled = true
|
||||
(activity as? HomeActivity)?.setRefreshing(false)
|
||||
if (ok && receipt != null) {
|
||||
ReceiptStore.save(requireContext(), receipt)
|
||||
clearForm()
|
||||
val activity = requireActivity() as HomeActivity
|
||||
activity.triggerRefresh()
|
||||
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
||||
} else if (!ok) {
|
||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -884,12 +922,303 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── BML business profile OTP flow ─────────────────────────────────────────
|
||||
|
||||
private fun isBusinessProfile(account: BankAccount): Boolean {
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
val loginId = account.loginTag.removePrefix("bml_")
|
||||
val profiles = app.bmlProfilesMap[loginId] ?: return false
|
||||
return profiles.firstOrNull { it.profileId == account.profileId }?.profileType == "business"
|
||||
}
|
||||
|
||||
private fun startBmlBusinessOtpFlow(
|
||||
src: BankAccount,
|
||||
destAccount: String,
|
||||
destDisplay: String,
|
||||
amount: Double,
|
||||
amountStr: String,
|
||||
remarks: String,
|
||||
isSrcCard: Boolean,
|
||||
isDestMib: Boolean,
|
||||
currency: String,
|
||||
allAccounts: List<BankAccount>,
|
||||
allContacts: List<BankContact>,
|
||||
toAvatar: Bitmap?
|
||||
) {
|
||||
val debitAccount = src.internalId.ifBlank {
|
||||
Toast.makeText(requireContext(), getString(R.string.transfer_missing_internal_id), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
val isDestMyCard = allAccounts.any {
|
||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount
|
||||
}
|
||||
val (transferType, creditAccount, bank) = when {
|
||||
isSrcCard -> {
|
||||
val destBml = allAccounts.firstOrNull { it.accountNumber == destAccount && it.profileType == "BML" }
|
||||
Triple("CAD", destBml?.internalId?.ifBlank { destAccount } ?: destAccount, null as String?)
|
||||
}
|
||||
isDestMyCard -> {
|
||||
val card = allAccounts.first {
|
||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount
|
||||
}
|
||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
||||
}
|
||||
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
||||
isDestMib -> {
|
||||
val contact = allContacts.firstOrNull { it.benefCategoryId == "BML" && it.benefAccount == destAccount }
|
||||
if (contact == null) {
|
||||
Toast.makeText(requireContext(), "BML contact not found for this account", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
Triple("DOT", contact.benefNo.removePrefix("bml_"), null as String?)
|
||||
}
|
||||
else -> Triple("IAT", destAccount, null as String?)
|
||||
}
|
||||
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
||||
|
||||
pendingBmlTransfer = PendingBmlTransfer(
|
||||
src = src,
|
||||
debitAccount = debitAccount,
|
||||
creditAccount = creditAccount,
|
||||
amount = amount,
|
||||
amountStr = amountStr,
|
||||
remarks = remarks,
|
||||
transferType = transferType,
|
||||
currency = currency,
|
||||
bank = bank,
|
||||
destDisplay = destDisplay,
|
||||
destAccount = destAccount,
|
||||
toBank = toBank,
|
||||
toAvatar = toAvatar
|
||||
)
|
||||
|
||||
bmlOtpState = BmlOtpState.SELECTING_CHANNEL
|
||||
binding.btnTransfer.isEnabled = false
|
||||
(activity as? HomeActivity)?.setRefreshing(true)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val sess = bmlSessionFor(src)
|
||||
val channels = if (sess != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try { BmlAccountClient().fetchTransferChannels(sess) }
|
||||
catch (_: Exception) { emptyList() }
|
||||
}
|
||||
} else emptyList<BmlOtpChannel>()
|
||||
|
||||
(activity as? HomeActivity)?.setRefreshing(false)
|
||||
|
||||
if (channels.isEmpty()) {
|
||||
Toast.makeText(requireContext(), "Could not load OTP channels", Toast.LENGTH_SHORT).show()
|
||||
resetBmlOtpState()
|
||||
updateTransferButton()
|
||||
return@launch
|
||||
}
|
||||
|
||||
showBmlChannelSelection(channels)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBmlChannelSelection(channels: List<BmlOtpChannel>) {
|
||||
val ctx = requireContext()
|
||||
val dp = ctx.resources.displayMetrics.density
|
||||
binding.containerBmlChannels.removeAllViews()
|
||||
|
||||
for (channel in channels) {
|
||||
val iconRes = when (channel.channel) {
|
||||
"email" -> R.drawable.ic_channel_email
|
||||
"mobile" -> R.drawable.ic_channel_sms
|
||||
else -> R.drawable.ic_channel_sms
|
||||
}
|
||||
val iconSize = (24 * dp).toInt()
|
||||
|
||||
val textCol = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
|
||||
marginStart = (12 * dp).toInt()
|
||||
}
|
||||
}
|
||||
textCol.addView(TextView(ctx).apply {
|
||||
text = channel.description
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge)
|
||||
})
|
||||
textCol.addView(TextView(ctx).apply {
|
||||
text = channel.masked
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall)
|
||||
alpha = 0.6f
|
||||
})
|
||||
|
||||
val row = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
|
||||
background = ta.getDrawable(0); ta.recycle()
|
||||
isClickable = true; isFocusable = true
|
||||
val hp = (16 * dp).toInt(); val vp = (12 * dp).toInt()
|
||||
setPadding(hp, vp, hp, vp)
|
||||
}
|
||||
row.addView(ImageView(ctx).apply { setImageResource(iconRes) },
|
||||
LinearLayout.LayoutParams(iconSize, iconSize))
|
||||
row.addView(textCol)
|
||||
row.setOnClickListener { selectBmlOtpChannel(channel) }
|
||||
binding.containerBmlChannels.addView(row)
|
||||
}
|
||||
|
||||
disableTransferFields()
|
||||
binding.layoutBmlChannelSelection.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun selectBmlOtpChannel(channel: BmlOtpChannel) {
|
||||
bmlOtpChannel = channel.channel
|
||||
binding.layoutBmlChannelSelection.visibility = View.GONE
|
||||
|
||||
val pending = pendingBmlTransfer ?: return
|
||||
val sess = bmlSessionFor(pending.src) ?: run {
|
||||
Toast.makeText(requireContext(), getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
|
||||
resetBmlOtpState()
|
||||
updateTransferButton()
|
||||
return
|
||||
}
|
||||
|
||||
binding.btnTransfer.isEnabled = false
|
||||
(activity as? HomeActivity)?.setRefreshing(true)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val initiated = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
BmlTransferClient().initiateTransfer(
|
||||
sess, pending.debitAccount, pending.creditAccount,
|
||||
pending.amount, pending.transferType, pending.currency,
|
||||
pending.bank, channel.channel
|
||||
)
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
(activity as? HomeActivity)?.setRefreshing(false)
|
||||
|
||||
if (!initiated) {
|
||||
Toast.makeText(requireContext(), "Failed to initiate transfer — check your session", Toast.LENGTH_SHORT).show()
|
||||
resetBmlOtpState()
|
||||
updateTransferButton()
|
||||
return@launch
|
||||
}
|
||||
|
||||
bmlOtpState = BmlOtpState.AWAITING_OTP
|
||||
binding.tvBmlOtpSentVia.text = "OTP code sent via: ${channel.description} (${channel.masked})"
|
||||
binding.tvBmlOtpSentVia.visibility = View.VISIBLE
|
||||
binding.tilBmlOtp.visibility = View.VISIBLE
|
||||
binding.etBmlOtp.requestFocus()
|
||||
binding.btnTransfer.text = getString(R.string.transfer_verify_payment)
|
||||
binding.btnTransfer.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyBmlOtp() {
|
||||
val otp = binding.etBmlOtp.text?.toString()?.trim() ?: ""
|
||||
if (otp.isEmpty()) {
|
||||
binding.tilBmlOtp.error = "Enter the verification code"
|
||||
return
|
||||
}
|
||||
binding.tilBmlOtp.error = null
|
||||
val pending = pendingBmlTransfer ?: return
|
||||
val channel = bmlOtpChannel ?: return
|
||||
val sess = bmlSessionFor(pending.src) ?: run {
|
||||
Toast.makeText(requireContext(), getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
binding.btnTransfer.isEnabled = false
|
||||
(activity as? HomeActivity)?.setRefreshing(true)
|
||||
|
||||
val capturedToAvatar = pending.toAvatar
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val result = BmlTransferClient().confirmTransfer(
|
||||
sess, pending.debitAccount, pending.creditAccount,
|
||||
pending.amount, pending.transferType, pending.currency,
|
||||
otp, pending.remarks, pending.bank, channel
|
||||
)
|
||||
if (result.success) {
|
||||
val r = TransferReceiptData(
|
||||
bank = "BML",
|
||||
amount = "%.2f".format(pending.amount),
|
||||
currency = pending.currency,
|
||||
fromLabel = pending.src.accountBriefName,
|
||||
fromColorHex = "#0066A1",
|
||||
toLabel = pending.destDisplay.ifBlank { pending.destAccount },
|
||||
toAccount = pending.destAccount,
|
||||
toBank = pending.toBank,
|
||||
remarks = pending.remarks,
|
||||
bmlFromName = pending.src.accountBriefName,
|
||||
bmlReference = result.reference,
|
||||
bmlTimestamp = result.timestamp,
|
||||
bmlMessage = result.message
|
||||
)
|
||||
Triple(true, "", r)
|
||||
} else {
|
||||
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null as TransferReceiptData?)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Triple(false, e.message ?: "Transfer failed", null as TransferReceiptData?)
|
||||
}
|
||||
}
|
||||
(activity as? HomeActivity)?.setRefreshing(false)
|
||||
|
||||
if (ok && receipt != null) {
|
||||
ReceiptStore.save(requireContext(), receipt)
|
||||
resetBmlOtpState()
|
||||
clearForm()
|
||||
val activity = requireActivity() as HomeActivity
|
||||
activity.triggerRefresh()
|
||||
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
||||
} else {
|
||||
binding.btnTransfer.isEnabled = true
|
||||
binding.tilBmlOtp.error = msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableTransferFields() {
|
||||
binding.tilAmount.isEnabled = false
|
||||
binding.tilRemarks.isEnabled = false
|
||||
binding.cardFromInfo.alpha = 0.5f
|
||||
binding.btnClearFromInfo.isEnabled = false
|
||||
binding.cardToInfo.alpha = 0.5f
|
||||
binding.btnClearToInfo.isEnabled = false
|
||||
}
|
||||
|
||||
private fun enableTransferFields() {
|
||||
binding.tilAmount.isEnabled = true
|
||||
binding.tilRemarks.isEnabled = true
|
||||
binding.cardFromInfo.alpha = 1f
|
||||
binding.btnClearFromInfo.isEnabled = true
|
||||
binding.cardToInfo.alpha = 1f
|
||||
binding.btnClearToInfo.isEnabled = true
|
||||
}
|
||||
|
||||
private fun resetBmlOtpState() {
|
||||
bmlOtpState = BmlOtpState.NONE
|
||||
bmlOtpChannel = null
|
||||
pendingBmlTransfer = null
|
||||
val b = _binding ?: return
|
||||
b.layoutBmlChannelSelection.visibility = View.GONE
|
||||
b.tvBmlOtpSentVia.visibility = View.GONE
|
||||
b.tilBmlOtp.visibility = View.GONE
|
||||
b.etBmlOtp.setText("")
|
||||
b.tilBmlOtp.error = null
|
||||
enableTransferFields()
|
||||
b.btnTransfer.text = getString(R.string.transfer)
|
||||
}
|
||||
|
||||
private fun updateTransferButton() {
|
||||
if (bmlOtpState != BmlOtpState.NONE) return
|
||||
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
||||
binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
|
||||
}
|
||||
|
||||
private fun clearForm() {
|
||||
resetBmlOtpState()
|
||||
selectedAccount = null
|
||||
binding.actvFrom.setText("", false)
|
||||
binding.cardFromInfo.visibility = View.GONE
|
||||
@@ -1008,7 +1337,7 @@ class TransferFragment : Fragment() {
|
||||
) : BaseAdapter(), Filterable {
|
||||
|
||||
private val items: List<Any> = buildList {
|
||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_LOAN" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||
addAll(regular)
|
||||
if (cards.isNotEmpty()) {
|
||||
|
||||
@@ -267,6 +267,10 @@ class CredentialsFragment : Fragment() {
|
||||
bmlAccumulatedAccounts += result.accounts
|
||||
val store = CredentialStore(requireContext())
|
||||
store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId)
|
||||
if (result.session.refreshToken.isNotBlank())
|
||||
store.saveBmlProfileRefreshToken(profile.profileId, result.session.refreshToken)
|
||||
if (result.session.expiresAt > 0)
|
||||
store.saveBmlProfileExpiresAt(profile.profileId, result.session.expiresAt)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
app.bmlSessions[profile.profileId] = result.session
|
||||
}
|
||||
@@ -326,8 +330,16 @@ class CredentialsFragment : Fragment() {
|
||||
val session = app.bmlSessions.remove(oldId)
|
||||
if (session != null) {
|
||||
app.bmlSessions[customerId] = session
|
||||
val savedRefresh = store.loadBmlProfileRefreshToken(oldId)
|
||||
val savedExpiry = store.loadBmlProfileExpiresAt(oldId)
|
||||
store.clearBmlProfileSession(oldId)
|
||||
store.saveBmlProfileSession(customerId, session.accessToken, session.deviceId)
|
||||
if (session.refreshToken.isNotBlank())
|
||||
store.saveBmlProfileRefreshToken(customerId, session.refreshToken)
|
||||
else if (savedRefresh != null)
|
||||
store.saveBmlProfileRefreshToken(customerId, savedRefresh)
|
||||
val expiryToSave = if (session.expiresAt > 0) session.expiresAt else savedExpiry
|
||||
if (expiryToSave > 0) store.saveBmlProfileExpiresAt(customerId, expiryToSave)
|
||||
}
|
||||
// Update stored profile list with the real ID
|
||||
val updatedProfiles = profiles.map {
|
||||
|
||||
@@ -11,19 +11,19 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
|
||||
OnboardingSlide(
|
||||
titleRes = R.string.onboarding_title_1,
|
||||
descRes = R.string.onboarding_desc_1,
|
||||
iconRes = R.drawable.ic_launcher_foreground,
|
||||
iconRes = R.drawable.ic_logo,
|
||||
isFirst = true
|
||||
),
|
||||
OnboardingSlide(
|
||||
titleRes = R.string.onboarding_title_2,
|
||||
descRes = R.string.onboarding_desc_2,
|
||||
iconRes = R.drawable.ic_launcher_foreground,
|
||||
iconRes = R.drawable.ic_logo,
|
||||
isFirst = false
|
||||
),
|
||||
OnboardingSlide(
|
||||
titleRes = R.string.onboarding_title_3,
|
||||
descRes = R.string.onboarding_desc_3,
|
||||
iconRes = R.drawable.ic_launcher_foreground,
|
||||
iconRes = R.drawable.ic_logo,
|
||||
isFirst = false,
|
||||
isLast = true
|
||||
)
|
||||
|
||||
@@ -215,9 +215,10 @@ class SecuritySetupFragment : Fragment() {
|
||||
val salt = ByteArray(16).also { SecureRandom().nextBytes(it) }
|
||||
val saltB64 = Base64.encodeToString(salt, Base64.NO_WRAP)
|
||||
val hash = pbkdf2(input, salt)
|
||||
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
|
||||
val edit = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
|
||||
.putString("security_method", method)
|
||||
.apply()
|
||||
if (method == "pin") edit.putInt("pin_length", input.length)
|
||||
edit.apply()
|
||||
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
|
||||
}
|
||||
|
||||
|
||||
57
app/src/main/java/sh/sar/basedbank/util/CardsCache.kt
Normal file
57
app/src/main/java/sh/sar/basedbank/util/CardsCache.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
package sh.sar.basedbank.util
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
|
||||
object CardsCache {
|
||||
|
||||
private const val PREFS = "cards_cache"
|
||||
private const val KEY_MIB_CARDS = "mib_cards"
|
||||
|
||||
fun save(context: Context, cards: List<MibCard>) {
|
||||
val arr = JSONArray()
|
||||
for (c in cards) {
|
||||
arr.put(JSONObject().apply {
|
||||
put("cardId", c.cardId)
|
||||
put("maskedCardNumber", c.maskedCardNumber)
|
||||
put("cardStatus", c.cardStatus)
|
||||
put("cardType", c.cardType)
|
||||
put("cardTypeDesc", c.cardTypeDesc)
|
||||
put("customerId", c.customerId)
|
||||
put("phoneNumber", c.phoneNumber)
|
||||
put("cardHolderName", c.cardHolderName)
|
||||
put("loginTag", c.loginTag)
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.edit().putString(KEY_MIB_CARDS, CacheEncryption.encrypt(arr.toString())).apply()
|
||||
}
|
||||
|
||||
fun load(context: Context): List<MibCard> {
|
||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getString(KEY_MIB_CARDS, null) ?: return emptyList()
|
||||
return try {
|
||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||
(0 until arr.length()).map { i ->
|
||||
val o = arr.getJSONObject(i)
|
||||
MibCard(
|
||||
cardId = o.optString("cardId"),
|
||||
maskedCardNumber = o.optString("maskedCardNumber"),
|
||||
cardStatus = o.optString("cardStatus"),
|
||||
cardType = o.optString("cardType"),
|
||||
cardTypeDesc = o.optString("cardTypeDesc"),
|
||||
customerId = o.optString("customerId"),
|
||||
phoneNumber = o.optString("phoneNumber"),
|
||||
cardHolderName = o.optString("cardHolderName"),
|
||||
loginTag = o.optString("loginTag")
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
fun clear(context: Context) {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
||||
}
|
||||
}
|
||||
@@ -264,10 +264,30 @@ class CredentialStore(context: Context) {
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun saveBmlProfileExpiresAt(profileId: String, expiresAt: Long) {
|
||||
prefs.edit().putLong("bml_profile_${profileId}_expires_at", expiresAt).apply()
|
||||
}
|
||||
|
||||
fun loadBmlProfileExpiresAt(profileId: String): Long =
|
||||
prefs.getLong("bml_profile_${profileId}_expires_at", 0L)
|
||||
|
||||
fun saveBmlProfileRefreshToken(profileId: String, refreshToken: String) {
|
||||
val key = getOrCreateKey()
|
||||
prefs.edit().putString("bml_profile_${profileId}_enc_refresh_token", encrypt(refreshToken, key)).apply()
|
||||
}
|
||||
|
||||
fun loadBmlProfileRefreshToken(profileId: String): String? {
|
||||
val key = getOrCreateKey()
|
||||
val enc = prefs.getString("bml_profile_${profileId}_enc_refresh_token", null) ?: return null
|
||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun clearBmlProfileSession(profileId: String) {
|
||||
prefs.edit()
|
||||
.remove("bml_profile_${profileId}_enc_token")
|
||||
.remove("bml_profile_${profileId}_enc_device_id")
|
||||
.remove("bml_profile_${profileId}_enc_refresh_token")
|
||||
.remove("bml_profile_${profileId}_expires_at")
|
||||
.apply()
|
||||
}
|
||||
|
||||
|
||||
7
app/src/main/res/drawable/bg_card_overlay_gradient.xml
Normal file
7
app/src/main/res/drawable/bg_card_overlay_gradient.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:startColor="#00000000"
|
||||
android:endColor="#CC000000"
|
||||
android:angle="270"/>
|
||||
</shape>
|
||||
10
app/src/main/res/drawable/ic_block.xml
Normal file
10
app/src/main/res/drawable/ic_block.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.68L5.68,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.68L18.32,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_freeze.xml
Normal file
10
app/src/main/res/drawable/ic_freeze.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M22,11h-4.17l3.24,-3.24 -1.41,-1.42L15,11h-2V9l4.66,-4.66 -1.42,-1.41L13,6.17V2h-2v4.17L7.76,2.93 6.34,4.34 11,9v2H9L4.34,6.34 2.93,7.76 6.17,11H2v2h4.17l-3.24,3.24 1.41,1.42L9,13h2v2l-4.66,4.66 1.42,1.41L11,17.83V22h2v-4.17l3.24,3.24 1.42,-1.41L13,15v-2h2l4.66,4.66 1.41,-1.42L17.83,13H22z"/>
|
||||
</vector>
|
||||
@@ -5,166 +5,6 @@
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:fillColor="#E8B547"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
android:viewportWidth="432"
|
||||
android:viewportHeight="432">
|
||||
|
||||
<!--
|
||||
Rufiyaa symbol centered on canvas.
|
||||
Original SVG bounding box: 276.85 × 175.60
|
||||
Scale 0.85 → 235.3 × 149.3, centered at (216, 216):
|
||||
translateX = 216 − 235.3/2 = 98
|
||||
translateY = 216 − 149.3/2 = 141
|
||||
-->
|
||||
<group
|
||||
android:scaleX="0.85"
|
||||
android:scaleY="0.85"
|
||||
android:translateX="98"
|
||||
android:translateY="141">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m 0.01444349,146.48136 c 0.292039,-6.44722 3.89308401,-9.12968 9.18909001,-9.48871 1.2523995,-0.0849 2.7807995,-0.0849 4.8186995,0 7.677319,0.0719 15.575535,2.04677 23.196,0.32719 3.328697,-0.83215 38.545925,-17.3522 71.890297,-40.525298 -6.38484,-3.92558 -9.558207,-9.227296 -9.06617,-16.8414 0.1618,-1.45617 2.26605,-16.30132 20.6841,-14.74007 2.82949,0.2398 11.06017,2.89394 24.6766,7.89527 4.8434,-4.011342 9.86993,-7.792775 14.86,-11.6179 -8.69961,-3.530822 -11.46541,-11.291021 -10.04177,-20.1135 1.42405,-8.825035 8.66169,-13.705029 19.0329,-11.88805 9.23153,1.617223 19.00984,5.215984 22.8004,6.7845 20.70075,-11.189327 54.77683,-28.8770828 65.3541,-34.0729998 5.70626,-3.35254 12.71464,-3.19857098 16.5862,2.73179 4.21557,6.6454798 3.9892,13.2813598 -2.19151,17.9821998 -8.64301,6.573585 -19.44036,9.920412 -29.2398,14.5974 -6.93317,3.490262 -13.93101,6.864024 -20.6089,10.8298 21.25669,6.267982 27.73865,10.456642 28.27166,22.5229 0.0826,1.868798 -0.20263,3.745 -0.64544,5.3661 -1.21907,4.462958 -5.06847,10.963331 -17.1941,9.7415 -5.09154,-0.513098 -25.58805,-6.353594 -42.9433,-12.0531 -4.15228,3.73688 -8.78178,6.907135 -13.2389,10.267 9.60154,3.066049 21.58993,6.56711 24.4216,17.486898 0.24324,1.08936 0.96989,5.15913 -0.58539,9.4564 -2.62403,7.6132 -9.24819,10.55389 -16.7664,10.1919 -3.54102,-0.12568 -18.38167,-3.16212 -42.9144,-12.18824 -26.82393,18.68149 -64.679357,43.44745 -92.643497,57.009 -5.389878,2.61389 -21.064916,10.09114 -30.0955,9.4114 -5.029255,-0.37856 -10.4541355,-5.68673 -13.6177995,-11.67795 -4.32270801,-8.18616 -4.01566901,-16.80012 -3.98877001,-17.39403 z" />
|
||||
</group>
|
||||
|
||||
</vector>
|
||||
|
||||
9
app/src/main/res/drawable/ic_logo.xml
Normal file
9
app/src/main/res/drawable/ic_logo.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#E8B547" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</layer-list>
|
||||
10
app/src/main/res/drawable/ic_nfc.xml
Normal file
10
app/src/main/res/drawable/ic_nfc.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M20,2L4,2C2.9,2 2,2.9 2,4l0,16c0,1.1 0.9,2 2,2l16,0c1.1,0 2,-0.9 2,-2L22,4C22,2.9 21.1,2 20,2zM13,18l-2,0 0,-1c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5l-2,0c0,-1.65 -1.35,-3 -3,-3s-3,1.35 -3,3 1.35,3 3,3l0,-2 2,3zM19,12l-2,0c0,-2.76 -2.24,-5 -5,-5l0,-2C15.87,5 19,8.13 19,12z"/>
|
||||
</vector>
|
||||
@@ -19,19 +19,26 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/refreshIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:trackCornerRadius="0dp" />
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/refreshIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:trackCornerRadius="0dp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
48
app/src/main/res/layout/fragment_card_settings.xml
Normal file
48
app/src/main/res/layout/fragment_card_settings.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:clipToPadding="false"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loadingView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/cards_empty"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
@@ -216,41 +216,31 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<!-- Card support WIP -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
<!-- Card carousel (hidden when no cards loaded) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/sectionCards"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardElevation="1dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant">
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/nav_pay_with_card"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvCards"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="24dp">
|
||||
android:clipToPadding="false"
|
||||
android:paddingEnd="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/card_support_wip"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/coming_soon"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOutline" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
48
app/src/main/res/layout/fragment_pay_with_card.xml
Normal file
48
app/src/main/res/layout/fragment_pay_with_card.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:clipToPadding="false"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loadingView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/cards_empty"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
@@ -96,6 +96,46 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/rowAutoUnlockPin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_auto_unlock_pin"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_auto_unlock_pin_desc"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchAutoUnlockPin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -336,6 +336,61 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- BML business OTP: channel selection (shown after confirmation, before OTP entry) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutBmlChannelSelection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/transfer_send_otp_via"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/containerBmlChannels"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- BML business OTP: sent-via label (shown after channel selection) -->
|
||||
<TextView
|
||||
android:id="@+id/tvBmlOtpSentVia"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- BML business OTP: verification code input (shown after channel selection) -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilBmlOtp"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/transfer_otp_code_hint"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etBmlOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:maxLines="1"
|
||||
android:maxLength="6" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnTransfer"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
109
app/src/main/res/layout/item_card_dashboard.xml
Normal file
109
app/src/main/res/layout/item_card_dashboard.xml
Normal file
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="280dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivCardImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/nav_card_settings"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/bg_card_overlay_gradient"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardOwner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#80000000"
|
||||
android:shadowDx="1"
|
||||
android:shadowDy="1"
|
||||
android:shadowRadius="3"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="#CCFFFFFF"
|
||||
android:fontFamily="monospace"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPayQr"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_pay_qr"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_qr_scan"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="4dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPayNfc"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_pay_nfc"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_nfc"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
139
app/src/main/res/layout/item_card_settings_entry.xml
Normal file
139
app/src/main/res/layout/item_card_settings_entry.xml
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:cardCornerRadius="20dp"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivCardImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/nav_card_settings"/>
|
||||
|
||||
<!-- Bottom gradient for text legibility -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="90dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/bg_card_overlay_gradient"/>
|
||||
|
||||
<!-- Bottom-left: card owner name + masked number -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardOwner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#80000000"
|
||||
android:shadowDx="1"
|
||||
android:shadowDy="1"
|
||||
android:shadowRadius="3"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="#CCFFFFFF"
|
||||
android:fontFamily="monospace"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Card type label -->
|
||||
<TextView
|
||||
android:id="@+id/tvCardType"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="10dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"/>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChangePin"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_action_change_pin"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_edit"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="4dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnFreeze"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_action_freeze"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_freeze"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="4dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBlock"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_action_block"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_block"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
123
app/src/main/res/layout/item_card_wallet.xml
Normal file
123
app/src/main/res/layout/item_card_wallet.xml
Normal file
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:cardCornerRadius="20dp"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivCardImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/nav_card_settings"/>
|
||||
|
||||
<!-- Bottom gradient for text legibility -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="90dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/bg_card_overlay_gradient"/>
|
||||
|
||||
<!-- Bottom-left: card owner name + masked number -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardOwner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#80000000"
|
||||
android:shadowDx="1"
|
||||
android:shadowDy="1"
|
||||
android:shadowRadius="3"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="#CCFFFFFF"
|
||||
android:fontFamily="monospace"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Card type label -->
|
||||
<TextView
|
||||
android:id="@+id/tvCardType"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="10dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"/>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPayQr"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_pay_qr"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_qr_scan"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="4dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPayNfc"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_pay_nfc"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_nfc"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -15,13 +15,26 @@
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLabel"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<item android:id="@+id/nav_finances"
|
||||
android:icon="@drawable/ic_nav_finances"
|
||||
android:title="@string/nav_finances" />
|
||||
<item android:id="@+id/nav_pay_with_card"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_pay_with_card" />
|
||||
<item android:id="@+id/nav_card_settings"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_card_settings" />
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<item android:id="@+id/nav_finances"
|
||||
android:icon="@drawable/ic_nav_finances"
|
||||
android:title="@string/nav_finances" />
|
||||
<item android:id="@+id/nav_pay_with_card"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_pay_with_card" />
|
||||
<item android:id="@+id/nav_card_settings"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_card_settings" />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">BasedBank</string>
|
||||
<string name="app_name">ތިޖޫރީ</string>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<string name="onboarding_supported_services">ހިދުމަތްތައް</string>
|
||||
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</string>
|
||||
<string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string>
|
||||
<string name="onboarding_desc_1">BasedBank ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
|
||||
<string name="onboarding_desc_1">ތިޖޫރީ ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
|
||||
<string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string>
|
||||
<string name="onboarding_desc_2">އިތުރު ބޭންކްތަކަށް ސަޕޯޓް ލިބޭ ގޮތަށް ތައްޔާރުވަމުން ދަނީ. ދިވެހިރާއްޖޭގެ ބޭންކްތަކަށް ސަޕޯޓް ފަހި ވަމުން ދިޔަ ވަރަކަށް ހިމަނެމުން ދޭ.</string>
|
||||
<string name="onboarding_title_3">ފެށޭ ގޮތަށް ތައްޔާރު</string>
|
||||
@@ -31,7 +31,7 @@
|
||||
<string name="login">ލޮގިން</string>
|
||||
|
||||
<!-- Lock screen -->
|
||||
<string name="unlock_app">BasedBank ހުޅުވާ</string>
|
||||
<string name="unlock_app">ތިޖޫރީ ހުޅުވާ</string>
|
||||
<string name="unlock_pin_subtitle">PIN ޖަހާ</string>
|
||||
<string name="unlock_pattern_subtitle">ހުޅުވާ ޕެޓަން ކަހާ</string>
|
||||
<string name="use_biometrics">ބަޔޮމެޓްރިކް ބޭނުން ކުރޭ</string>
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<!-- Security setup -->
|
||||
<string name="security_setup">އެޕް ރައްކާތެރި ކުރޭ</string>
|
||||
<string name="security_setup_desc">BasedBank ހުޅުވަން ބޭނުންވާ ގޮތެއް ހިޔާރު ކުރޭ.</string>
|
||||
<string name="security_setup_desc">ތިޖޫރީ ހުޅުވަން ބޭނުންވާ ގޮތެއް ހިޔާރު ކުރޭ.</string>
|
||||
<string name="method_pin">PIN ކޯޑް</string>
|
||||
<string name="method_pin_desc">4–8 ރިޔަލެއްގެ ނަންބަރު PIN</string>
|
||||
<string name="method_pattern">ޕެޓަން ކަހާ</string>
|
||||
@@ -74,6 +74,17 @@
|
||||
<string name="nav_finances">ފައިނޭންސް</string>
|
||||
<string name="nav_card_settings">ކާޑް ސެޓިންގ</string>
|
||||
<string name="nav_settings">ސެޓިންގ</string>
|
||||
<string name="nav_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string>
|
||||
<string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string>
|
||||
<string name="nav_desc_transfer">ކޮންޓެކްޓަކަށް ފައިސާ ފޮނުވާ</string>
|
||||
<string name="nav_desc_pay_mv_qr">PayMV QR ކޯޑް ސްކޭން ނުވަތަ ތައްޔާރު ކުރޭ</string>
|
||||
<string name="nav_desc_activities">ފަހުގެ ޓްރާންސްފަތައް ބަލާ</string>
|
||||
<string name="nav_desc_transfer_history">އެކައުންޓް ތަކުގެ ޓްރާންސެކްޝަން ތާރީހް</string>
|
||||
<string name="nav_desc_finances">ލޯން އަދި ފައިނޭންސިންގ</string>
|
||||
<string name="nav_desc_pay_with_card">ކާޑް ބޭނުންކޮށް ފައިސާ ދައްކާ</string>
|
||||
<string name="nav_desc_card_settings">ކާޑް ސެޓިންގ މެނޭޖް ކުރޭ</string>
|
||||
<string name="nav_desc_otp">OTP ކޯޑް ތައްޔާރު ކުރޭ</string>
|
||||
<string name="nav_desc_settings">އެޕްލިކޭޝަންގެ ތަރުތީބު</string>
|
||||
<string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string>
|
||||
<string name="nav_close_drawer">ނެވިގޭޝަން ލައްޕާ</string>
|
||||
<string name="work_in_progress">ތައްޔާރުވަމުން ދަނީ</string>
|
||||
@@ -100,12 +111,18 @@
|
||||
<string name="lang_english">English</string>
|
||||
<string name="lang_dhivehi">ދިވެހި</string>
|
||||
<string name="settings_privacy">ޕްރައިވެސީ</string>
|
||||
<string name="settings_auto_unlock_pin">ރަނގަޅު ޕިން އެޅުމުން ހުޅުވޭ</string>
|
||||
<string name="settings_auto_unlock_pin_desc">ޕިންގެ ދިގުމިނާ އެއްވަރަށް ޑިޖިޓް ލިޔުމުން ހުޅުވިދާ</string>
|
||||
<string name="settings_block_screenshots">ސްކްރީންޝޮޓް ބްލޮކްކުރޭ</string>
|
||||
<string name="settings_block_screenshots_desc">ރިސެންޓްސް ސްކްރީނުންނާއި ސްކްރީން ކެޕްޗާ ހުއްޓުވައިދޭ</string>
|
||||
<string name="settings_cache">ކޭޝް</string>
|
||||
<string name="settings_clear_cache">ކޭޝް ސާފުކުރޭ</string>
|
||||
<string name="settings_cache_cleared">ކޭޝް ސާފުކުރެވިއްޖެ</string>
|
||||
<string name="settings_logins">ލޮގިންތައް</string>
|
||||
<string name="settings_desc_logins">ބޭންކް ލޮގިންތައް މެނޭޖް ކުރޭ</string>
|
||||
<string name="settings_desc_appearance">ތީމް، ބަސް، އަދި ދައްކުވާ ގޮތް</string>
|
||||
<string name="settings_desc_privacy_security">އެޕް ލޮކް، ޕިން، އަދި ސަލާމަތީ ސެޓިންގ</string>
|
||||
<string name="settings_desc_storage">ކޭޝް ޑޭޓާ އަދި ސްޓޯރޭޖް</string>
|
||||
<string name="settings_logout">ލޮގްއައުޓް</string>
|
||||
<string name="settings_logout_confirm_title">%s އިން ލޮގްއައުޓް ވަންތަ؟</string>
|
||||
<string name="settings_logout_confirm_message">ހުރިހާ ކޭޝް ޑޭޓާ ސާފުވެ، ބާކީ ހުރި އެކައުންޓްތައް އަލުން ލޯޑްވާނެ.</string>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<resources>
|
||||
<string name="app_name">BasedBank</string>
|
||||
<string name="app_name">Thijooree</string>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<string name="onboarding_supported_services">Supported services</string>
|
||||
<string name="select_language">Select Language</string>
|
||||
<string name="onboarding_title_1">Your Banks, One App</string>
|
||||
<string name="onboarding_desc_1">BasedBank brings all your Maldivian bank accounts together in one place. Check balances, view accounts, and more — without switching between apps.</string>
|
||||
<string name="onboarding_desc_1">Thijooree brings all your Maldivian bank accounts together in one place. Check balances, view accounts, and more — without switching between apps.</string>
|
||||
<string name="onboarding_title_2">More Banks Coming</string>
|
||||
<string name="onboarding_desc_2">Support for additional banks is on the way. Stay tuned as we expand coverage across the Maldives.</string>
|
||||
<string name="onboarding_title_3">Before You Begin</string>
|
||||
<string name="onboarding_desc_3">BasedBank is an independent, third-party app. It is not affiliated with, endorsed by, or officially supported by any bank or financial institution.\n\nThis app works by logging into your internet banking services directly using your credentials and communicating with bank APIs. It is built using techniques derived from reverse-engineered official bank apps. Behaviour may change or break without notice if banks update their systems.\n\nDhiraagu and Ooredoo APIs are used to look up details about phone numbers.\n\nThis app does not collect any analytics, telemetry, or personal data, and does not transmit any information about you or your usage to the developer. Everything stays entirely on your device.\n\nBy tapping Get Started, you acknowledge and accept that:\n\n• Errors, failures, or service interruptions may occur at any time\n• Your bank may detect third-party access and apply restrictions or take other actions against your account\n• The developer of this app is not liable for any loss, damage, or consequences arising from your use of this app\n• You use this app entirely at your own risk</string>
|
||||
<string name="onboarding_desc_3">Thijooree is an independent, third-party app. It is not affiliated with, endorsed by, or officially supported by any bank or financial institution.\n\nThis app works by logging into your internet banking services directly using your credentials and communicating with bank APIs. It is built using techniques derived from reverse-engineered official bank apps. Behaviour may change or break without notice if banks update their systems.\n\nDhiraagu and Ooredoo APIs are used to look up details about phone numbers.\n\nThis app does not collect any analytics, telemetry, or personal data, and does not transmit any information about you or your usage to the developer. Everything stays entirely on your device.\n\nBy tapping Get Started, you acknowledge and accept that:\n\n• Errors, failures, or service interruptions may occur at any time\n• Your bank may detect third-party access and apply restrictions or take other actions against your account\n• The developer of this app is not liable for any loss, damage, or consequences arising from your use of this app\n• You use this app entirely at your own risk</string>
|
||||
<string name="coming_soon">Coming Soon</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="get_started">Get Started</string>
|
||||
@@ -38,7 +38,7 @@
|
||||
<string name="login">Login</string>
|
||||
|
||||
<!-- Lock screen -->
|
||||
<string name="unlock_app">Unlock BasedBank</string>
|
||||
<string name="unlock_app">Unlock Thijooree</string>
|
||||
<string name="unlock_pin_subtitle">Enter your PIN</string>
|
||||
<string name="unlock_pattern_subtitle">Draw your unlock pattern</string>
|
||||
<string name="use_biometrics">Use Biometrics</string>
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
<!-- Security setup -->
|
||||
<string name="security_setup">Secure Your App</string>
|
||||
<string name="security_setup_desc">Choose how you want to lock BasedBank when you\'re away.</string>
|
||||
<string name="security_setup_desc">Choose how you want to lock Thijooree when you\'re away.</string>
|
||||
<string name="security_already_configured">App Lock Configured</string>
|
||||
<string name="security_already_configured_desc">Your app lock is set up.</string>
|
||||
|
||||
@@ -86,9 +86,21 @@
|
||||
<string name="nav_otp">OTP Codes</string>
|
||||
<string name="nav_settings">Settings</string>
|
||||
<string name="nav_more">More</string>
|
||||
<string name="nav_desc_accounts">View all your bank accounts</string>
|
||||
<string name="nav_desc_contacts">Manage your transfer contacts</string>
|
||||
<string name="nav_desc_transfer">Send money to a contact</string>
|
||||
<string name="nav_desc_pay_mv_qr">Scan or generate a PayMV QR code</string>
|
||||
<string name="nav_desc_activities">View your recent transfers</string>
|
||||
<string name="nav_desc_transfer_history">Full transaction history by account</string>
|
||||
<string name="nav_desc_finances">Loans and financing overview</string>
|
||||
<string name="nav_desc_pay_with_card">Make a payment using your card</string>
|
||||
<string name="nav_desc_card_settings">Manage your card preferences</string>
|
||||
<string name="nav_desc_otp">Generate OTP codes for authentication</string>
|
||||
<string name="nav_desc_settings">App preferences and configuration</string>
|
||||
<string name="nav_open_drawer">Open navigation</string>
|
||||
<string name="nav_close_drawer">Close navigation</string>
|
||||
<string name="work_in_progress">Work in progress</string>
|
||||
<string name="press_back_to_exit">Press back again to exit</string>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<string name="dashboard_pending_finances">Pending Finances</string>
|
||||
@@ -143,6 +155,8 @@
|
||||
<string name="settings_privacy">Privacy</string>
|
||||
<string name="settings_hide_amounts">Hide sensitive information</string>
|
||||
<string name="settings_hide_amounts_desc">Masks account balances and financial figures across the app</string>
|
||||
<string name="settings_auto_unlock_pin">Auto unlock on correct PIN</string>
|
||||
<string name="settings_auto_unlock_pin_desc">Unlock automatically when the entered digits match your PIN length</string>
|
||||
<string name="settings_block_screenshots">Block Screenshots</string>
|
||||
<string name="settings_block_screenshots_desc">Prevents the app from appearing in the recents screen and blocks screen capture</string>
|
||||
<string name="settings_cache">Cache</string>
|
||||
@@ -161,6 +175,10 @@
|
||||
<string name="settings_privacy_security">Privacy & Security</string>
|
||||
<string name="settings_storage">Storage</string>
|
||||
<string name="settings_logins">Logins</string>
|
||||
<string name="settings_desc_logins">Manage your bank account logins</string>
|
||||
<string name="settings_desc_appearance">Theme, language, and display options</string>
|
||||
<string name="settings_desc_privacy_security">App lock, PIN, and security preferences</string>
|
||||
<string name="settings_desc_storage">Manage cached data and storage usage</string>
|
||||
<string name="settings_logout">Log out</string>
|
||||
<string name="settings_logout_confirm_title">Log out of %s?</string>
|
||||
<string name="settings_logout_confirm_message">All cached data will be cleared and remaining accounts will be refreshed.</string>
|
||||
@@ -216,6 +234,9 @@
|
||||
<string name="transfer_bml_contact_required_title">Contact Required</string>
|
||||
<string name="transfer_bml_contact_required_msg">To send USD to a MIB account from BML, the recipient must be saved as a BML contact first. This is required by BML\'s API.\n\nPlease add this account as a BML contact, then try again.</string>
|
||||
<string name="transfer_missing_internal_id">Account data is incomplete — please re-login to refresh.</string>
|
||||
<string name="transfer_verify_payment">Verify Payment</string>
|
||||
<string name="transfer_send_otp_via">Send verification code via</string>
|
||||
<string name="transfer_otp_code_hint">Verification code</string>
|
||||
|
||||
<!-- Contacts -->
|
||||
<string name="contacts_empty">No contacts found</string>
|
||||
@@ -272,4 +293,13 @@
|
||||
<string name="loan_end_date">End Date</string>
|
||||
<string name="loan_overdue_payments">Overdue Payments</string>
|
||||
<string name="loan_rate_fmt">%.2f%%</string>
|
||||
|
||||
<!-- Cards -->
|
||||
<string name="nav_pay_with_card">Pay with Card</string>
|
||||
<string name="card_pay_qr">QR Pay</string>
|
||||
<string name="card_pay_nfc">NFC Pay</string>
|
||||
<string name="card_action_change_pin">Change PIN</string>
|
||||
<string name="card_action_freeze">Freeze</string>
|
||||
<string name="card_action_block">Block</string>
|
||||
<string name="cards_empty">No cards found</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user