diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactDetailsSheetFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactDetailsSheetFragment.kt
index c5b2940..b0c756b 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactDetailsSheetFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactDetailsSheetFragment.kt
@@ -3,6 +3,7 @@ package sh.sar.basedbank.ui.home
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -56,16 +57,25 @@ class ContactDetailsSheetFragment : BottomSheetDialogFragment() {
binding.tvName.text = contact.name
binding.actionTransfer.visibility = if (contact.canTransfer) View.VISIBLE else View.GONE
+ // QR stays visible for every contact; greyed out where PayMV QR isn't supported yet
+ val canQr = canShowPayMvQr(contact)
+ binding.btnQr.isEnabled = canQr
+ binding.tvQrLabel.alpha = if (canQr) 1f else 0.38f
binding.actionEdit.visibility = if (contact.canEdit) View.VISIBLE else View.GONE
binding.actionDelete.visibility = if (contact.canDelete) View.VISIBLE else View.GONE
- binding.actionRow.visibility =
- if (contact.canTransfer || contact.canEdit || contact.canDelete) View.VISIBLE else View.GONE
binding.btnTransfer.setOnClickListener {
val parent = contactsFragment
dismiss()
parent?.openTransfer(contact)
}
+ binding.btnQr.setOnClickListener {
+ val activity = requireActivity() as HomeActivity
+ val bank = contactBank(contact) ?: return@setOnClickListener
+ val qrName = contact.realName.ifBlank { contact.name }
+ dismiss()
+ activity.showWithBackStack(PayMvQrFragment.forContact(contact.accountNumber, qrName, bank))
+ }
binding.btnEdit.setOnClickListener {
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
@@ -79,6 +89,9 @@ class ContactDetailsSheetFragment : BottomSheetDialogFragment() {
val copy = View.OnClickListener { copyAccount(contact.accountNumber) }
binding.rowAccount.setOnClickListener(copy)
binding.btnCopyAccount.setOnClickListener(copy)
+ binding.btnShareAccount.setOnClickListener {
+ shareAccount(contact.realName.ifBlank { contact.name }, contact.accountNumber)
+ }
val showRealName = contact.realName.isNotBlank() && contact.realName != contact.name
binding.tvRealName.text = contact.realName
@@ -108,14 +121,30 @@ class ContactDetailsSheetFragment : BottomSheetDialogFragment() {
}
}
- /** Logo of the contact's bank; null when we have no logo for it (only MIB and BML for now). */
- private fun bankLogoRes(contact: ContactDisplay): Int? = when {
- contact.network == TransferNetwork.MIB -> R.drawable.mib_logo
- contact.accountNumber.matches(Regex("^7\\d{12}$")) -> R.drawable.bml_logo_vector
- contact.accountNumber.matches(Regex("^9\\d{16}$")) -> R.drawable.mib_logo
+ /** Which bank the contact's account is at ("BML" / "MIB"); null when we can't tell. */
+ private fun contactBank(contact: ContactDisplay): String? = when {
+ contact.network == TransferNetwork.MIB -> "MIB"
+ contact.accountNumber.matches(Regex("^7\\d{12}$")) -> "BML"
+ contact.accountNumber.matches(Regex("^9\\d{16}$")) -> "MIB"
else -> null
}
+ /** Logo of the contact's bank; null when we have no logo for it (only MIB and BML for now). */
+ private fun bankLogoRes(contact: ContactDisplay): Int? = when (contactBank(contact)) {
+ "BML" -> R.drawable.bml_logo_vector
+ "MIB" -> R.drawable.mib_logo
+ else -> null
+ }
+
+ /**
+ * PayMV QR is only offered for contacts whose account is at BML for now.
+ * TODO: enable for MIB and other banks once their PayMV QR payloads are verified —
+ * PayMvQrFragment.forContact already takes the bank, so this check is the only gate.
+ */
+ private fun canShowPayMvQr(contact: ContactDisplay): Boolean =
+ contactBank(contact) == "BML" &&
+ contact.currency?.contains("USD", ignoreCase = true) != true // PayMV QR is MVR only
+
private data class SourceProfile(val logoRes: Int, val bankNameRes: Int, val name: String)
/** Which of the user's own bank profiles this contact is saved under. */
@@ -149,6 +178,14 @@ class ContactDetailsSheetFragment : BottomSheetDialogFragment() {
}
}
+ private fun shareAccount(accountName: String, account: String) {
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, "$accountName\n$account")
+ }
+ startActivity(Intent.createChooser(intent, getString(R.string.contact_share_account)))
+ }
+
private fun copyAccount(account: String) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt
index d601a88..650ea5f 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/PayMvQrFragment.kt
@@ -48,6 +48,8 @@ class PayMvQrFragment : Fragment() {
private val viewModel: HomeViewModel by activityViewModels()
private var selectedAccount: BankAccount? = null
+ /** Set when opened for a saved contact: the QR is for their account, not one of ours. */
+ private var contactTarget: QrTarget? = null
private var generatedBitmap: Bitmap? = null
private var generateJob: Job? = null
private val dropdownProfileImageCache = mutableMapOf()
@@ -59,7 +61,17 @@ class PayMvQrFragment : Fragment() {
return binding.root
}
+ /** Who the QR pays into — one of our own accounts, or a contact's. */
+ private data class QrTarget(val accountNumber: String, val name: String, val bank: String)
+
+ private fun currentTarget(): QrTarget? = contactTarget
+ ?: selectedAccount?.let { QrTarget(it.accountNumber, it.accountBriefName, it.bank) }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ contactTarget = arguments?.let { args ->
+ val number = args.getString(ARG_ACCOUNT_NUMBER) ?: return@let null
+ QrTarget(number, args.getString(ARG_ACCOUNT_NAME).orEmpty(), args.getString(ARG_BANK).orEmpty())
+ }
val basePaddingBottom = view.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val bottomNav = activity?.findViewById(R.id.bottomNavigation)
@@ -68,7 +80,15 @@ class PayMvQrFragment : Fragment() {
v.updatePadding(bottom = basePaddingBottom + navBarBottom)
insets
}
- setupDropdown()
+ if (contactTarget != null) {
+ // Not our account, so no account picker and our phone number doesn't belong on it
+ binding.tilAccount.visibility = View.GONE
+ binding.layoutIncludePhone.visibility = View.GONE
+ binding.switchIncludePhone.isChecked = false
+ scheduleGenerate()
+ } else {
+ setupDropdown()
+ }
binding.etAmount.addTextChangedListener { scheduleGenerate() }
binding.etReference.addTextChangedListener { scheduleGenerate() }
binding.switchIncludePhone.setOnCheckedChangeListener { _, _ -> scheduleGenerate() }
@@ -119,8 +139,8 @@ class PayMvQrFragment : Fragment() {
}
private suspend fun generateQr() {
- val account = selectedAccount ?: return
- val acquirer = when (account.bank) {
+ val target = currentTarget() ?: return
+ val acquirer = when (target.bank) {
"BML" -> "MALBMVMV"
"MIB" -> "MADVMVMV"
"FAHIPAY" -> "FAHIMVMV"
@@ -133,10 +153,10 @@ class PayMvQrFragment : Fragment() {
?.let { "%.2f".format(it) }
val ctx = requireContext()
- val includePhone = binding.switchIncludePhone.isChecked
- val loginId = sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(account.loginTag)
- val store = CredentialStore(ctx)
- val mobile = if (includePhone) {
+ val account = selectedAccount
+ val mobile = if (binding.switchIncludePhone.isChecked && contactTarget == null && account != null) {
+ val loginId = sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(account.loginTag)
+ val store = CredentialStore(ctx)
when (account.bank) {
"BML" -> store.loadBmlUserProfile(loginId)?.mobile
"FAHIPAY" -> store.loadFahipayUserProfile(loginId)?.mobile
@@ -154,8 +174,8 @@ class PayMvQrFragment : Fragment() {
?.takeIf { it.isNotBlank() } ?: getString(R.string.paymvqr_reference_default)
val bmp = withContext(Dispatchers.Default) {
- val payload = buildQrPayload(account.accountNumber, account.accountBriefName, acquirer, amountFormatted, mobile, purpose)
- renderQrCard(ctx, account, payload, amountFormatted)
+ val payload = buildQrPayload(target.accountNumber, target.name, acquirer, amountFormatted, mobile, purpose)
+ renderQrCard(ctx, target, payload, amountFormatted)
}
if (_binding == null) return
generatedBitmap = bmp
@@ -224,7 +244,7 @@ class PayMvQrFragment : Fragment() {
private fun renderQrCard(
ctx: Context,
- account: BankAccount,
+ target: QrTarget,
qrPayload: String,
amountStr: String?
): Bitmap {
@@ -246,7 +266,7 @@ class PayMvQrFragment : Fragment() {
canvas.drawColor(Color.WHITE)
// --- Bank logo top-left ---
- val logoRes = when (account.bank) {
+ val logoRes = when (target.bank) {
"BML" -> R.drawable.bml_logo_vector
"MIB" -> R.drawable.mib_faisanet_logo
else -> R.drawable.fahipay_logo_long
@@ -279,7 +299,7 @@ class PayMvQrFragment : Fragment() {
paint.color = Color.WHITE
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
paint.textAlign = Paint.Align.CENTER
- val nameText = account.accountBriefName.uppercase()
+ val nameText = target.name.uppercase()
paint.textSize = 36f
val maxNameW = boxR - boxL - 48f
if (paint.measureText(nameText) > maxNameW) {
@@ -338,13 +358,13 @@ class PayMvQrFragment : Fragment() {
private fun shareQr() {
val bmp = generatedBitmap ?: return
- val account = selectedAccount ?: return
+ val target = currentTarget() ?: return
lifecycleScope.launch {
val uri = withContext(Dispatchers.IO) {
try {
val dir = File(requireContext().cacheDir, "qr")
dir.mkdirs()
- val safeName = account.accountBriefName.replace(Regex("[^A-Za-z0-9_]"), "_")
+ val safeName = target.name.replace(Regex("[^A-Za-z0-9_]"), "_")
val file = File(dir, "${safeName}_paymv_qr.png")
FileOutputStream(file).use { bmp.compress(Bitmap.CompressFormat.PNG, 100, it) }
FileProvider.getUriForFile(
@@ -366,11 +386,11 @@ class PayMvQrFragment : Fragment() {
private fun saveQr() {
val bmp = generatedBitmap ?: return
- val account = selectedAccount ?: return
+ val target = currentTarget() ?: return
lifecycleScope.launch {
val saved = withContext(Dispatchers.IO) {
try {
- val safeName = account.accountBriefName.replace(Regex("[^A-Za-z0-9_]"), "_")
+ val safeName = target.name.replace(Regex("[^A-Za-z0-9_]"), "_")
val filename = "${safeName}_PayMV_QR.png"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = ContentValues().apply {
@@ -412,6 +432,21 @@ class PayMvQrFragment : Fragment() {
_binding = null
}
+ companion object {
+ private const val ARG_ACCOUNT_NUMBER = "account_number"
+ private const val ARG_ACCOUNT_NAME = "account_name"
+ private const val ARG_BANK = "bank"
+
+ /** QR for a contact's account. [bank] is "BML" / "MIB" / "FAHIPAY", as on [BankAccount.bank]. */
+ fun forContact(accountNumber: String, name: String, bank: String) = PayMvQrFragment().apply {
+ arguments = Bundle().apply {
+ putString(ARG_ACCOUNT_NUMBER, accountNumber)
+ putString(ARG_ACCOUNT_NAME, name)
+ putString(ARG_BANK, bank)
+ }
+ }
+ }
+
// ── Account dropdown adapter ──────────────────────────────────────────────
private inner class QrAccountAdapter(
diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml
new file mode 100644
index 0000000..9d1b39d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_qr_code.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_pay_mv_qr.xml b/app/src/main/res/layout/fragment_pay_mv_qr.xml
index dca2166..c34e90d 100644
--- a/app/src/main/res/layout/fragment_pay_mv_qr.xml
+++ b/app/src/main/res/layout/fragment_pay_mv_qr.xml
@@ -95,6 +95,7 @@
+
+
+
+
+
+
+
+
@@ -111,6 +163,13 @@
android:contentDescription="@string/contact_delete"
app:backgroundTint="?attr/colorErrorContainer"
app:icon="@drawable/ic_delete"
+ android:insetLeft="0dp"
+ android:insetTop="0dp"
+ android:insetRight="0dp"
+ android:insetBottom="0dp"
+ android:padding="0dp"
+ app:iconGravity="textStart"
+ app:iconPadding="0dp"
app:iconSize="24dp"
app:iconTint="?attr/colorOnErrorContainer" />
@@ -118,6 +177,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
+ android:gravity="center"
android:text="@string/contact_delete"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorError" />
@@ -174,6 +234,16 @@
+
+
Account number
Account name
Bank
+ QR
This can\'t be undone.
Copy account number
+ Share account details
Account number copied