From 66568a464aabe601980ad5b3cd7d2e004e62e862 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Fri, 25 Sep 2026 06:53:15 +0500 Subject: [PATCH] share contact account numbers and PayMV QR for contacts --- .../ui/home/ContactDetailsSheetFragment.kt | 51 ++++++++++++-- .../sar/basedbank/ui/home/PayMvQrFragment.kt | 67 +++++++++++++----- app/src/main/res/drawable/ic_qr_code.xml | 10 +++ .../main/res/layout/fragment_pay_mv_qr.xml | 1 + .../main/res/layout/sheet_contact_details.xml | 70 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 app/src/main/res/drawable/ic_qr_code.xml 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