7 Commits
Author SHA1 Message Date
shihaam 173c02ab8f release version 1.0.13
Build and Release APK / build (push) Successful in 3m12s
Auto Tag on Version Change / check-version (push) Failing after 14m33s
2026-05-31 00:28:28 +05:00
shihaam b37b12996f Contacts after selecting account back button Behaviour Enchantment #30
Auto Tag on Version Change / check-version (push) Successful in 5s
2026-05-31 00:26:10 +05:00
shihaam 21203b39e7 rearrange buttons in contact page #28
Auto Tag on Version Change / check-version (push) Failing after 15m6s
2026-05-31 00:22:55 +05:00
shihaam 0be492ca18 remove custom logic for scan button in PayMV QR generate and just handoff to transfer page to handle auto select account
Auto Tag on Version Change / check-version (push) Failing after 15m1s
2026-05-31 00:18:01 +05:00
shihaam 973576cf6a save static BML QR scans to recents #31
Auto Tag on Version Change / check-version (push) Successful in 6s
2026-05-31 00:06:37 +05:00
shihaam 4523aed69e remove *** on customer amount set PayMV QRs, #29
Auto Tag on Version Change / check-version (push) Successful in 7s
2026-05-30 23:26:55 +05:00
shihaam f90d83b59e improve share to scan to pay flow: fetching merchant details
Auto Tag on Version Change / check-version (push) Failing after 10m51s
2026-05-30 23:22:10 +05:00
9 changed files with 64 additions and 48 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "sh.sar.basedbank" applicationId = "sh.sar.basedbank"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 11 versionCode = 12
versionName = "1.0.12" versionName = "1.0.13"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -33,6 +33,8 @@ import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.databinding.FragmentBmlQrPayBinding import sh.sar.basedbank.databinding.FragmentBmlQrPayBinding
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.RecentPick
import sh.sar.basedbank.util.RecentsCache
import sh.sar.basedbank.util.Totp import sh.sar.basedbank.util.Totp
class BmlQrPayFragment : Fragment() { class BmlQrPayFragment : Fragment() {
@@ -150,6 +152,19 @@ class BmlQrPayFragment : Fragment() {
return@launch return@launch
} }
merchantInfo = info merchantInfo = info
if (info.amount == 0.0) {
val qrUrl = arguments?.getString(ARG_QR_URL)
if (qrUrl != null) {
RecentsCache.save(requireContext(), RecentPick(
accountNumber = "bmlqr:$qrUrl",
displayName = info.merchantName,
subtitle = info.merchantAddress.ifBlank { "BML Merchant" },
colorHex = "#0066A1",
imageHash = null,
isProfileImage = false
))
}
}
populateMerchant(info) populateMerchant(info)
} }
} }
@@ -168,6 +168,10 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
val account = accounts.firstOrNull { it.accountNumber == accountNumber } val account = accounts.firstOrNull { it.accountNumber == accountNumber }
val bundle = bundleOf(KEY_ACCOUNT_NUMBER to accountNumber, KEY_LABEL to label) val bundle = bundleOf(KEY_ACCOUNT_NUMBER to accountNumber, KEY_LABEL to label)
when { when {
accountNumber.startsWith("bmlqr:") -> {
bundle.putString(KEY_SUBTITLE, "BML QR Merchant")
bundle.putString(KEY_COLOR, "#0066A1")
}
account != null -> { account != null -> {
bundle.putString(KEY_SUBTITLE, "${account.accountNumber} · ${account.currencyName} ${account.availableBalance}") bundle.putString(KEY_SUBTITLE, "${account.accountNumber} · ${account.currencyName} ${account.availableBalance}")
bundle.putString(KEY_COLOR, "#FE860E") bundle.putString(KEY_COLOR, "#FE860E")
@@ -178,7 +178,7 @@ class ContactsFragment : Fragment() {
colorHex = contact.bankColor, colorHex = contact.bankColor,
imageHash = contact.imageHash imageHash = contact.imageHash
) )
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, fragment) (requireActivity() as HomeActivity).showWithBackStack(fragment)
} }
private fun confirmDelete(contact: ContactDisplay) { private fun confirmDelete(contact: ContactDisplay) {
@@ -446,18 +446,20 @@ fun applyNavLabelVisibility() {
} }
private fun routeSharedQrText(text: String) { private fun routeSharedQrText(text: String) {
val store = CredentialStore(this)
val bmlUrl = sh.sar.basedbank.util.PaymvQrParser.extractBmlGatewayUrl(text) val bmlUrl = sh.sar.basedbank.util.PaymvQrParser.extractBmlGatewayUrl(text)
if (text.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != null) { if (text.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != null) {
navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(bmlUrl ?: text)) navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(bmlUrl ?: text, store.getDefaultCardAccountNumber()))
return return
} }
val qr = sh.sar.basedbank.util.PaymvQrParser.parse(text) val qr = sh.sar.basedbank.util.PaymvQrParser.parse(text)
if (qr?.accountNumber != null) { if (qr?.accountNumber != null) {
navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromQr( navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromQr(
accountNumber = qr.accountNumber, accountNumber = qr.accountNumber,
displayName = qr.merchantName ?: qr.accountNumber, displayName = qr.merchantName ?: qr.accountNumber,
amount = qr.amount, amount = qr.amount,
remarks = qr.purpose remarks = qr.purpose,
fromAccountNumber = store.getDefaultAccountNumber()
)) ))
} else { } else {
Toast.makeText(this, R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
@@ -7,13 +7,10 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -39,7 +36,6 @@ import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
import sh.sar.basedbank.util.AccountListParser import sh.sar.basedbank.util.AccountListParser
import sh.sar.basedbank.util.PaymvQrParser
import sh.sar.basedbank.util.bmlapi.BmlCardParser import sh.sar.basedbank.util.bmlapi.BmlCardParser
import sh.sar.basedbank.util.bmlapi.BmlDashboardParser import sh.sar.basedbank.util.bmlapi.BmlDashboardParser
import java.io.File import java.io.File
@@ -56,31 +52,6 @@ class PayMvQrFragment : Fragment() {
private var generateJob: Job? = null private var generateJob: Job? = null
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>() private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
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
// BML card/gateway QR — hand off to dedicated payment screen
val bmlUrl = PaymvQrParser.extractBmlGatewayUrl(raw)
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != null) {
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(bmlUrl ?: raw))
return@registerForActivityResult
}
val qr = PaymvQrParser.parse(raw)
if (qr == null || qr.accountNumber == null) {
Toast.makeText(requireContext(), R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
return@registerForActivityResult
}
val activity = requireActivity() as HomeActivity
activity.navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromQr(
accountNumber = qr.accountNumber,
displayName = qr.merchantName ?: qr.accountNumber,
amount = qr.amount,
remarks = qr.purpose
))
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View { ): View {
@@ -106,7 +77,7 @@ class PayMvQrFragment : Fragment() {
binding.btnShare.setOnClickListener { shareQr() } binding.btnShare.setOnClickListener { shareQr() }
binding.btnSave.setOnClickListener { saveQr() } binding.btnSave.setOnClickListener { saveQr() }
binding.btnScanQr.setOnClickListener { binding.btnScanQr.setOnClickListener {
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java)) (requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceWithAutoScan())
} }
} }
@@ -95,6 +95,7 @@ class TransferFragment : Fragment() {
// BML QR merchant payment mode (set when navigated from a card QR scan) // BML QR merchant payment mode (set when navigated from a card QR scan)
private var bmlQrInfo: BmlQrPayInfo? = null private var bmlQrInfo: BmlQrPayInfo? = null
private var bmlGatewayQr = false // true for pay.bml.com.mv QRs (requires pre-initiate step) private var bmlGatewayQr = false // true for pay.bml.com.mv QRs (requires pre-initiate step)
private var bmlQrLookupAttempted = false // prevents re-lookup after user clears the merchant
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>() private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
// BML business profile OTP flow state // BML business profile OTP flow state
@@ -245,6 +246,10 @@ class TransferFragment : Fragment() {
childFragmentManager.setFragmentResultListener(ContactPickerSheetFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle -> childFragmentManager.setFragmentResultListener(ContactPickerSheetFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
val accountNumber = bundle.getString(ContactPickerSheetFragment.KEY_ACCOUNT_NUMBER) ?: return@setFragmentResultListener val accountNumber = bundle.getString(ContactPickerSheetFragment.KEY_ACCOUNT_NUMBER) ?: return@setFragmentResultListener
if (accountNumber.startsWith("bmlqr:")) {
lookupBmlQrMerchant(accountNumber.removePrefix("bmlqr:"))
return@setFragmentResultListener
}
val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: "" val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: ""
val subtitle = bundle.getString(ContactPickerSheetFragment.KEY_SUBTITLE) ?: accountNumber val subtitle = bundle.getString(ContactPickerSheetFragment.KEY_SUBTITLE) ?: accountNumber
val colorHex = bundle.getString(ContactPickerSheetFragment.KEY_COLOR) ?: "#607D8B" val colorHex = bundle.getString(ContactPickerSheetFragment.KEY_COLOR) ?: "#607D8B"
@@ -292,6 +297,7 @@ class TransferFragment : Fragment() {
} }
private fun lookupBmlQrMerchant(qrUrl: String) { private fun lookupBmlQrMerchant(qrUrl: String) {
bmlQrLookupAttempted = true
bmlGatewayQr = qrUrl.startsWith("https://pay.bml.com.mv/app/") bmlGatewayQr = qrUrl.startsWith("https://pay.bml.com.mv/app/")
val base64Url = android.util.Base64.encodeToString( val base64Url = android.util.Base64.encodeToString(
qrUrl.toByteArray(Charsets.UTF_8), android.util.Base64.NO_WRAP) qrUrl.toByteArray(Charsets.UTF_8), android.util.Base64.NO_WRAP)
@@ -316,6 +322,16 @@ class TransferFragment : Fragment() {
return@launch return@launch
} }
bmlQrInfo = info bmlQrInfo = info
if (info.amount == 0.0) {
RecentsCache.save(requireContext(), RecentPick(
accountNumber = "bmlqr:$qrUrl",
displayName = info.merchantName,
subtitle = info.merchantAddress.ifBlank { "BML Merchant" },
colorHex = "#0066A1",
imageHash = null,
isProfileImage = false
))
}
// Auto-select the user's default BML card if no card was pre-selected // Auto-select the user's default BML card if no card was pre-selected
if (selectedAccount == null) { if (selectedAccount == null) {
@@ -422,6 +438,14 @@ class TransferFragment : Fragment() {
updateTransferButton() updateTransferButton()
} }
} }
// On a cold start (e.g. share intent), anyBmlSession() may be null when
// onViewCreated runs. Retry the lookup once sessions are available.
val pendingBmlQrUrl = arguments?.getString(ARG_BML_QR_URL)
if (pendingBmlQrUrl != null && !bmlQrLookupAttempted) {
val app = requireActivity().application as BasedBankApp
if (app.anyBmlSession() != null) lookupBmlQrMerchant(pendingBmlQrUrl)
}
} }
} }
@@ -41,7 +41,7 @@ object PaymvQrParser {
PaymvQrData( PaymvQrData(
accountNumber = merchantInfo?.get("03"), accountNumber = merchantInfo?.get("03"),
amount = root["54"], amount = root["54"]?.takeIf { it != "***" },
purpose = additionalData?.get("08"), purpose = additionalData?.get("08"),
merchantName = root["59"] merchantName = root["59"]
) )
+9 -9
View File
@@ -26,15 +26,6 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<ImageButton
android:id="@+id/btnTransferContact"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_send"
android:padding="6dp"
android:contentDescription="@string/transfer" />
<ImageButton <ImageButton
android:id="@+id/btnEditContact" android:id="@+id/btnEditContact"
android:layout_width="36dp" android:layout_width="36dp"
@@ -54,6 +45,15 @@
android:tint="?attr/colorError" android:tint="?attr/colorError"
android:contentDescription="@string/contact_delete" /> android:contentDescription="@string/contact_delete" />
<ImageButton
android:id="@+id/btnTransferContact"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_send"
android:padding="6dp"
android:contentDescription="@string/transfer" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout