Compare commits
7
Commits
a03b1b1682
...
v1.0.13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173c02ab8f
|
||
|
|
b37b12996f
|
||
|
|
21203b39e7
|
||
|
|
0be492ca18
|
||
|
|
973576cf6a
|
||
|
|
4523aed69e
|
||
|
|
f90d83b59e
|
@@ -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"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user