Compare commits
5
Commits
v1.0.4
...
28682bba41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28682bba41
|
||
|
|
25484addfb
|
||
|
|
728c7d2aa3
|
||
|
|
b24949c117
|
||
|
|
28e5878668
|
@@ -73,6 +73,9 @@ dependencies {
|
|||||||
// Coroutines
|
// Coroutines
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||||
|
|
||||||
|
// ZXing core for QR code generation
|
||||||
|
implementation("com.google.zxing:core:3.5.3")
|
||||||
|
|
||||||
// QR scanning — CameraX + zxing-cpp (MIT, same stack as BinaryEye)
|
// QR scanning — CameraX + zxing-cpp (MIT, same stack as BinaryEye)
|
||||||
implementation("androidx.camera:camera-core:1.4.2")
|
implementation("androidx.camera:camera-core:1.4.2")
|
||||||
implementation("androidx.camera:camera-camera2:1.4.2")
|
implementation("androidx.camera:camera-camera2:1.4.2")
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||||
|
import sh.sar.basedbank.databinding.ItemTransactionBinding
|
||||||
|
import sh.sar.basedbank.util.ReceiptStore
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class ActivitiesAdapter(
|
||||||
|
private val onItemClick: (ReceiptStore.Entry) -> Unit
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
private sealed class Item {
|
||||||
|
data class DateHeader(val label: String) : Item()
|
||||||
|
data class ReceiptItem(val entry: ReceiptStore.Entry) : Item()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val displayItems = mutableListOf<Item>()
|
||||||
|
|
||||||
|
fun setEntries(entries: List<ReceiptStore.Entry>) {
|
||||||
|
displayItems.clear()
|
||||||
|
var lastDateKey = ""
|
||||||
|
for (entry in entries) {
|
||||||
|
val dateKey = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date(entry.savedAt))
|
||||||
|
if (dateKey != lastDateKey) {
|
||||||
|
displayItems.add(Item.DateHeader(formatDateHeader(entry.savedAt)))
|
||||||
|
lastDateKey = dateKey
|
||||||
|
}
|
||||||
|
displayItems.add(Item.ReceiptItem(entry))
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = displayItems.size
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int) =
|
||||||
|
if (displayItems[position] is Item.DateHeader) TYPE_DATE_HEADER else TYPE_RECEIPT
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
return if (viewType == TYPE_DATE_HEADER)
|
||||||
|
DateHeaderVH(ItemDateHeaderBinding.inflate(inflater, parent, false))
|
||||||
|
else
|
||||||
|
ReceiptVH(ItemTransactionBinding.inflate(inflater, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (holder) {
|
||||||
|
is DateHeaderVH -> holder.bind((displayItems[position] as Item.DateHeader).label)
|
||||||
|
is ReceiptVH -> holder.bind((displayItems[position] as Item.ReceiptItem).entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class DateHeaderVH(private val b: ItemDateHeaderBinding) :
|
||||||
|
RecyclerView.ViewHolder(b.root) {
|
||||||
|
fun bind(label: String) { b.tvDateHeader.text = label }
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ReceiptVH(private val b: ItemTransactionBinding) :
|
||||||
|
RecyclerView.ViewHolder(b.root) {
|
||||||
|
fun bind(entry: ReceiptStore.Entry) {
|
||||||
|
val d = entry.data
|
||||||
|
val colorHex = d.fromColorHex.takeIf { it.isNotBlank() } ?: "#607D8B"
|
||||||
|
val initial = d.toLabel.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
|
||||||
|
|
||||||
|
b.fvAvatar.background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.OVAL
|
||||||
|
setColor(try { Color.parseColor(colorHex) } catch (_: Exception) { Color.GRAY })
|
||||||
|
}
|
||||||
|
b.tvInitial.visibility = android.view.View.VISIBLE
|
||||||
|
b.tvInitial.text = initial
|
||||||
|
|
||||||
|
b.tvCounterparty.text = d.toLabel
|
||||||
|
b.tvCounterparty.visibility = android.view.View.VISIBLE
|
||||||
|
b.tvDescription.text = buildString {
|
||||||
|
append(d.fromLabel)
|
||||||
|
if (d.toBank.isNotBlank()) append(" · ${d.toBank}")
|
||||||
|
}
|
||||||
|
b.tvDate.text = formatTime(entry.savedAt)
|
||||||
|
|
||||||
|
b.tvAmount.text = "- ${d.currency} ${d.amount}"
|
||||||
|
b.tvAmount.setTextColor(Color.parseColor("#FF7043"))
|
||||||
|
|
||||||
|
b.root.setOnClickListener { onItemClick(entry) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDateHeader(millis: Long): String {
|
||||||
|
val sdf = SimpleDateFormat("EEEE, d MMMM yyyy", Locale.US)
|
||||||
|
return sdf.format(Date(millis))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatTime(millis: Long): String {
|
||||||
|
val sdf = SimpleDateFormat("HH:mm", Locale.US)
|
||||||
|
return sdf.format(Date(millis))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_DATE_HEADER = 0
|
||||||
|
private const val TYPE_RECEIPT = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.databinding.FragmentActivitiesBinding
|
||||||
|
import sh.sar.basedbank.util.ReceiptStore
|
||||||
|
|
||||||
|
class ActivitiesFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentActivitiesBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private lateinit var adapter: ActivitiesAdapter
|
||||||
|
private val allEntries = mutableListOf<ReceiptStore.Entry>()
|
||||||
|
private var searchQuery = ""
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
_binding = FragmentActivitiesBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
adapter = ActivitiesAdapter { entry ->
|
||||||
|
(activity as? HomeActivity)?.showWithBackStack(
|
||||||
|
TransferReceiptFragment.newInstance(entry.data, null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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.etSearch.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
searchQuery = s?.toString()?.trim() ?: ""
|
||||||
|
filterAndDisplay()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadEntries()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
requireActivity().title = getString(R.string.nav_activities)
|
||||||
|
// Reload in case a new receipt was added while we were away
|
||||||
|
loadEntries()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadEntries() {
|
||||||
|
allEntries.clear()
|
||||||
|
allEntries.addAll(ReceiptStore.loadAll(requireContext()))
|
||||||
|
filterAndDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterAndDisplay() {
|
||||||
|
val filtered = if (searchQuery.isBlank()) allEntries
|
||||||
|
else allEntries.filter { entry ->
|
||||||
|
entry.data.toLabel.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
entry.data.fromLabel.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
entry.data.toAccount.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
entry.data.toBank.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
entry.data.mibReferenceNo.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
entry.data.bmlReference.contains(searchQuery, ignoreCase = true)
|
||||||
|
}
|
||||||
|
adapter.setEntries(filtered)
|
||||||
|
binding.emptyView.visibility = if (filtered.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,7 +115,9 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
R.id.nav_accounts -> AccountsFragment()
|
R.id.nav_accounts -> AccountsFragment()
|
||||||
R.id.nav_contacts -> ContactsFragment()
|
R.id.nav_contacts -> ContactsFragment()
|
||||||
R.id.nav_transfer -> TransferFragment()
|
R.id.nav_transfer -> TransferFragment()
|
||||||
|
R.id.nav_pay_mv_qr -> PayMvQrFragment()
|
||||||
R.id.nav_more -> MoreFragment()
|
R.id.nav_more -> MoreFragment()
|
||||||
|
R.id.nav_activities -> ActivitiesFragment()
|
||||||
R.id.nav_transfer_history -> TransferHistoryFragment()
|
R.id.nav_transfer_history -> TransferHistoryFragment()
|
||||||
R.id.nav_finances -> FinancingFragment()
|
R.id.nav_finances -> FinancingFragment()
|
||||||
R.id.nav_otp -> OtpFragment()
|
R.id.nav_otp -> OtpFragment()
|
||||||
@@ -248,6 +250,19 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
menu.add(Menu.NONE, R.id.nav_more, 4, R.string.nav_more)
|
menu.add(Menu.NONE, R.id.nav_more, 4, R.string.nav_more)
|
||||||
.setIcon(R.drawable.ic_nav_more)
|
.setIcon(R.drawable.ic_nav_more)
|
||||||
|
// Restore selection to current destination after menu rebuild
|
||||||
|
val currentId = binding.navigationView.checkedItem?.itemId
|
||||||
|
if (currentId != null) {
|
||||||
|
val bottomNavIds = (0 until menu.size()).map { menu.getItem(it).itemId }.toSet()
|
||||||
|
val selectId = if (currentId in bottomNavIds) currentId
|
||||||
|
else if (R.id.nav_more in bottomNavIds) R.id.nav_more
|
||||||
|
else null
|
||||||
|
if (selectId != null) {
|
||||||
|
suppressBottomNavCallback = true
|
||||||
|
binding.bottomNavigation.selectedItemId = selectId
|
||||||
|
suppressBottomNavCallback = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyNavLabelVisibility() {
|
fun applyNavLabelVisibility() {
|
||||||
@@ -263,6 +278,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
R.id.nav_accounts -> AccountsFragment()
|
R.id.nav_accounts -> AccountsFragment()
|
||||||
R.id.nav_contacts -> ContactsFragment()
|
R.id.nav_contacts -> ContactsFragment()
|
||||||
R.id.nav_transfer -> TransferFragment()
|
R.id.nav_transfer -> TransferFragment()
|
||||||
|
R.id.nav_pay_mv_qr -> PayMvQrFragment()
|
||||||
|
R.id.nav_activities -> ActivitiesFragment()
|
||||||
R.id.nav_transfer_history -> TransferHistoryFragment()
|
R.id.nav_transfer_history -> TransferHistoryFragment()
|
||||||
R.id.nav_finances -> FinancingFragment()
|
R.id.nav_finances -> FinancingFragment()
|
||||||
R.id.nav_otp -> OtpFragment()
|
R.id.nav_otp -> OtpFragment()
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.*
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.EncodeHintType
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
|
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
|
||||||
|
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||||
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
class PayMvQrFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentPayMvQrBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var selectedAccount: MibAccount? = null
|
||||||
|
private var generatedBitmap: Bitmap? = null
|
||||||
|
private var generateJob: Job? = 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
|
||||||
|
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(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentPayMvQrBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
setupDropdown()
|
||||||
|
binding.etAmount.addTextChangedListener { scheduleGenerate() }
|
||||||
|
binding.btnShare.isEnabled = false
|
||||||
|
binding.btnSave.isEnabled = false
|
||||||
|
binding.btnShare.setOnClickListener { shareQr() }
|
||||||
|
binding.btnSave.setOnClickListener { saveQr() }
|
||||||
|
binding.btnScanQr.setOnClickListener {
|
||||||
|
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDropdown() {
|
||||||
|
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||||
|
val eligible = accounts.filter {
|
||||||
|
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT"
|
||||||
|
}
|
||||||
|
val adapter = QrAccountAdapter(requireContext(), eligible)
|
||||||
|
binding.actvAccount.setAdapter(adapter)
|
||||||
|
binding.actvAccount.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
val picked = adapter.getAccount(position) ?: return@setOnItemClickListener
|
||||||
|
selectedAccount = picked
|
||||||
|
scheduleGenerate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleGenerate() {
|
||||||
|
generateJob?.cancel()
|
||||||
|
generateJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
delay(300)
|
||||||
|
generateQr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun generateQr() {
|
||||||
|
val account = selectedAccount ?: return
|
||||||
|
val acquirer = when (account.bank) {
|
||||||
|
"BML" -> "MALBMVMV"
|
||||||
|
"MIB" -> "MADVMVMV"
|
||||||
|
"FAHIPAY" -> "FAHIMVMV"
|
||||||
|
else -> "MADVMVMV"
|
||||||
|
}
|
||||||
|
val amountFormatted = binding.etAmount.text?.toString()?.trim()
|
||||||
|
?.replace(",", "")
|
||||||
|
?.toDoubleOrNull()
|
||||||
|
?.takeIf { it > 0 }
|
||||||
|
?.let { "%.2f".format(it) }
|
||||||
|
|
||||||
|
val ctx = requireContext()
|
||||||
|
val bmp = withContext(Dispatchers.Default) {
|
||||||
|
val payload = buildQrPayload(account.accountNumber, account.accountBriefName, acquirer, amountFormatted)
|
||||||
|
renderQrCard(ctx, account, payload, amountFormatted)
|
||||||
|
}
|
||||||
|
if (_binding == null) return
|
||||||
|
generatedBitmap = bmp
|
||||||
|
binding.tvQrPlaceholder.visibility = View.GONE
|
||||||
|
binding.ivQrCard.setImageBitmap(bmp)
|
||||||
|
binding.ivQrCard.visibility = View.VISIBLE
|
||||||
|
binding.btnShare.isEnabled = true
|
||||||
|
binding.btnSave.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EMV MPQR payload ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun buildQrPayload(
|
||||||
|
accountNumber: String,
|
||||||
|
accountName: String,
|
||||||
|
acquirer: String,
|
||||||
|
amountStr: String?
|
||||||
|
): String {
|
||||||
|
fun tlv(tag: String, value: String): String {
|
||||||
|
val len = value.length
|
||||||
|
return tag + (if (len < 10) "0$len" else "$len") + value
|
||||||
|
}
|
||||||
|
val format = tlv("00", "01")
|
||||||
|
val poi = tlv("01", "11")
|
||||||
|
val sub00 = tlv("00", "mv.favara.mpqr")
|
||||||
|
val sub01 = tlv("01", acquirer)
|
||||||
|
val sub03 = tlv("03", accountNumber)
|
||||||
|
val sub10 = tlv("10", "IPAY")
|
||||||
|
val merchantAcct = tlv("26", sub00 + sub01 + sub03 + sub10)
|
||||||
|
val currency = tlv("53", "462")
|
||||||
|
val amountTLV = if (!amountStr.isNullOrBlank()) tlv("54", amountStr) else ""
|
||||||
|
val country = tlv("58", "MV")
|
||||||
|
val name = tlv("59", accountName.take(25))
|
||||||
|
val prefix = format + poi + merchantAcct + currency + amountTLV + country + name + "6304"
|
||||||
|
return prefix + crc16(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc16(data: String): String {
|
||||||
|
var crc = 0xFFFF
|
||||||
|
for (c in data) {
|
||||||
|
crc = crc xor ((c.code and 0xFF) shl 8)
|
||||||
|
repeat(8) {
|
||||||
|
crc = if (crc and 0x8000 != 0) ((crc shl 1) and 0xFFFF) xor 0x1021
|
||||||
|
else (crc shl 1) and 0xFFFF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc.toString(16).uppercase().padStart(4, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── QR card rendering ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun renderQrCard(
|
||||||
|
ctx: Context,
|
||||||
|
account: MibAccount,
|
||||||
|
qrPayload: String,
|
||||||
|
amountStr: String?
|
||||||
|
): Bitmap {
|
||||||
|
val W = 900
|
||||||
|
val H = 1080
|
||||||
|
val outerCorner = 48f
|
||||||
|
val boxBlue = Color.parseColor("#2272B7")
|
||||||
|
val footerBlue = Color.parseColor("#1A5799")
|
||||||
|
val boxL = 24f; val boxT = 110f; val boxR = 876f; val boxB = 962f
|
||||||
|
|
||||||
|
val bm = Bitmap.createBitmap(W, H, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bm)
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
|
// Clip to outer rounded card shape
|
||||||
|
val outerPath = Path()
|
||||||
|
outerPath.addRoundRect(RectF(0f, 0f, W.toFloat(), H.toFloat()), outerCorner, outerCorner, Path.Direction.CW)
|
||||||
|
canvas.clipPath(outerPath)
|
||||||
|
canvas.drawColor(Color.WHITE)
|
||||||
|
|
||||||
|
// --- Bank logo top-left ---
|
||||||
|
val logoRes = when (account.bank) {
|
||||||
|
"BML" -> R.drawable.bml_logo_vector
|
||||||
|
"MIB" -> R.drawable.mib_faisanet_logo
|
||||||
|
else -> R.drawable.fahipay_logo_long
|
||||||
|
}
|
||||||
|
AppCompatResources.getDrawable(ctx, logoRes)?.let { d ->
|
||||||
|
val nW = d.intrinsicWidth.coerceAtLeast(1)
|
||||||
|
val nH = d.intrinsicHeight.coerceAtLeast(1)
|
||||||
|
val maxW = 180f; val maxH = 76f
|
||||||
|
val scale = minOf(maxW / nW, maxH / nH)
|
||||||
|
val lW = (nW * scale).toInt()
|
||||||
|
val lH = (nH * scale).toInt()
|
||||||
|
val lTop = ((boxT - lH) / 2).toInt().coerceAtLeast(10)
|
||||||
|
d.setBounds(24, lTop, 24 + lW, lTop + lH)
|
||||||
|
d.draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- "PayMV QR" top-right ---
|
||||||
|
paint.color = Color.parseColor("#1A1A2E")
|
||||||
|
paint.textSize = 36f
|
||||||
|
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||||
|
paint.textAlign = Paint.Align.RIGHT
|
||||||
|
canvas.drawText("PayMV QR", W - 28f, 66f, paint)
|
||||||
|
|
||||||
|
// --- Blue rounded box ---
|
||||||
|
paint.color = boxBlue
|
||||||
|
paint.textAlign = Paint.Align.LEFT
|
||||||
|
canvas.drawRoundRect(RectF(boxL, boxT, boxR, boxB), 36f, 36f, paint)
|
||||||
|
|
||||||
|
// Account name (white, bold, uppercase, auto-scaled to fit)
|
||||||
|
paint.color = Color.WHITE
|
||||||
|
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
val nameText = account.accountBriefName.uppercase()
|
||||||
|
paint.textSize = 36f
|
||||||
|
val maxNameW = boxR - boxL - 48f
|
||||||
|
if (paint.measureText(nameText) > maxNameW) {
|
||||||
|
paint.textSize = 36f * maxNameW / paint.measureText(nameText)
|
||||||
|
}
|
||||||
|
val nameBaseline = boxT + 68f
|
||||||
|
canvas.drawText(nameText, W / 2f, nameBaseline, paint)
|
||||||
|
|
||||||
|
// Optional amount below name
|
||||||
|
val qrTopY: Float
|
||||||
|
if (!amountStr.isNullOrBlank()) {
|
||||||
|
paint.textSize = 28f
|
||||||
|
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||||
|
val amtBaseline = nameBaseline + 42f
|
||||||
|
canvas.drawText("MVR $amountStr", W / 2f, amtBaseline, paint)
|
||||||
|
qrTopY = amtBaseline + 20f
|
||||||
|
} else {
|
||||||
|
qrTopY = nameBaseline + 26f
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR code — white modules on the same blue as the box background
|
||||||
|
val availH = boxB - qrTopY - 24f
|
||||||
|
val qrPx = minOf(availH, boxR - boxL - 48f).toInt().coerceAtMost(700).coerceAtLeast(200)
|
||||||
|
val qrLeft = ((W - qrPx) / 2).toFloat()
|
||||||
|
try {
|
||||||
|
val hints = mapOf(
|
||||||
|
EncodeHintType.MARGIN to 0,
|
||||||
|
EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.M
|
||||||
|
)
|
||||||
|
val matrix = QRCodeWriter().encode(qrPayload, BarcodeFormat.QR_CODE, qrPx, qrPx, hints)
|
||||||
|
val pixels = IntArray(qrPx * qrPx)
|
||||||
|
for (y in 0 until qrPx) {
|
||||||
|
for (x in 0 until qrPx) {
|
||||||
|
pixels[y * qrPx + x] = if (matrix[x, y]) Color.WHITE else boxBlue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val qrBm = Bitmap.createBitmap(pixels, qrPx, qrPx, Bitmap.Config.ARGB_8888)
|
||||||
|
canvas.drawBitmap(qrBm, qrLeft, qrTopY, null)
|
||||||
|
qrBm.recycle()
|
||||||
|
} catch (_: Exception) { /* skip if encoding fails */ }
|
||||||
|
|
||||||
|
// --- Dark blue footer ---
|
||||||
|
paint.color = footerBlue
|
||||||
|
paint.textAlign = Paint.Align.LEFT
|
||||||
|
canvas.drawRect(RectF(0f, 970f, W.toFloat(), H.toFloat()), paint)
|
||||||
|
paint.color = Color.WHITE
|
||||||
|
paint.textSize = 32f
|
||||||
|
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
canvas.drawText("MALDIVES NATIONAL QR", W / 2f, 1038f, paint)
|
||||||
|
|
||||||
|
return bm
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Share / Save ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun shareQr() {
|
||||||
|
val bmp = generatedBitmap ?: return
|
||||||
|
val account = selectedAccount ?: 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 file = File(dir, "${safeName}_paymv_qr.png")
|
||||||
|
FileOutputStream(file).use { bmp.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
requireContext(),
|
||||||
|
"${requireContext().packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
if (uri == null || _binding == null) return@launch
|
||||||
|
val intent = android.content.Intent(android.content.Intent.ACTION_SEND).apply {
|
||||||
|
type = "image/png"
|
||||||
|
putExtra(android.content.Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
startActivity(android.content.Intent.createChooser(intent, getString(R.string.paymvqr_share)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveQr() {
|
||||||
|
val bmp = generatedBitmap ?: return
|
||||||
|
val account = selectedAccount ?: return
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val saved = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val safeName = account.accountBriefName.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 {
|
||||||
|
put(MediaStore.Images.Media.DISPLAY_NAME, filename)
|
||||||
|
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
|
||||||
|
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
|
||||||
|
}
|
||||||
|
val uri = requireContext().contentResolver.insert(
|
||||||
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
|
||||||
|
) ?: return@withContext false
|
||||||
|
requireContext().contentResolver.openOutputStream(uri)?.use {
|
||||||
|
bmp.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||||
|
dir.mkdirs()
|
||||||
|
FileOutputStream(File(dir, filename)).use { bmp.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
if (_binding == null) return@launch
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
if (saved) R.string.paymvqr_saved else R.string.paymvqr_save_failed,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
requireActivity().title = getString(R.string.pay_mv_qr)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Account dropdown adapter ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private inner class QrAccountAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val accounts: List<MibAccount>
|
||||||
|
) : BaseAdapter(), Filterable {
|
||||||
|
|
||||||
|
fun getAccount(position: Int): MibAccount? = accounts.getOrNull(position)
|
||||||
|
|
||||||
|
override fun getCount() = accounts.size
|
||||||
|
override fun getItem(position: Int) = accounts.getOrNull(position)
|
||||||
|
override fun getItemId(position: Int) = position.toLong()
|
||||||
|
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
|
||||||
|
getDropDownView(position, convertView, parent)
|
||||||
|
|
||||||
|
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val acc = accounts[position]
|
||||||
|
val b = if (convertView?.tag is ItemAccountDropdownBinding) {
|
||||||
|
convertView.tag as ItemAccountDropdownBinding
|
||||||
|
} else {
|
||||||
|
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
|
.also { it.root.tag = it }
|
||||||
|
}
|
||||||
|
val ownerPrefix = if (acc.bank == "BML" && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||||
|
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
||||||
|
b.tvDropdownAccountNumber.text = acc.accountNumber
|
||||||
|
b.tvDropdownBalance.text = ""
|
||||||
|
b.root.alpha = 1f
|
||||||
|
return b.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilter() = object : Filter() {
|
||||||
|
override fun performFiltering(c: CharSequence?) =
|
||||||
|
FilterResults().apply { values = accounts; count = accounts.size }
|
||||||
|
override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged()
|
||||||
|
override fun convertResultToString(r: Any?) =
|
||||||
|
(r as? MibAccount)?.let {
|
||||||
|
val prefix = if (it.bank == "BML" && it.profileName.isNotBlank()) "${it.profileName} · " else ""
|
||||||
|
"$prefix${it.accountBriefName}"
|
||||||
|
} ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,12 @@ import android.view.Gravity
|
|||||||
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.CheckBox
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.materialswitch.MaterialSwitch
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.mib.MibProfile
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
@@ -195,8 +195,8 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build checkbox rows — wired up after dialog.show() so we can reference the Save button
|
// Build toggle rows — wired up after dialog.show() so we can reference the Save button
|
||||||
val checkboxRows = mibProfiles.map { p ->
|
val toggleRows = mibProfiles.map { p ->
|
||||||
val row = LinearLayout(ctx).apply {
|
val row = LinearLayout(ctx).apply {
|
||||||
orientation = LinearLayout.HORIZONTAL
|
orientation = LinearLayout.HORIZONTAL
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
@@ -219,11 +219,20 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
alpha = 0.6f
|
alpha = 0.6f
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
val cb = CheckBox(ctx).apply { isChecked = p.profileId !in hidden }
|
val toggle = MaterialSwitch(ctx).apply { isChecked = p.profileId !in hidden }
|
||||||
row.addView(textCol)
|
row.addView(textCol)
|
||||||
row.addView(cb)
|
row.addView(toggle)
|
||||||
container.addView(row)
|
container.addView(row)
|
||||||
p to cb
|
p to toggle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateToggleStates(saveBtn: android.widget.Button) {
|
||||||
|
val visibleCount = mibProfiles.count { it.profileId !in hidden }
|
||||||
|
toggleRows.forEach { (p, toggle) ->
|
||||||
|
// Disable the sole remaining visible toggle so it can't be turned off
|
||||||
|
toggle.isEnabled = !(toggle.isChecked && visibleCount == 1)
|
||||||
|
}
|
||||||
|
saveBtn.isEnabled = hidden != originalHidden && visibleCount >= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(ctx)
|
val dialog = MaterialAlertDialogBuilder(ctx)
|
||||||
@@ -238,12 +247,12 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
|
|
||||||
val saveBtn = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
|
val saveBtn = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
|
||||||
saveBtn.isEnabled = false
|
saveBtn.isEnabled = false
|
||||||
|
updateToggleStates(saveBtn)
|
||||||
|
|
||||||
checkboxRows.forEach { (p, cb) ->
|
toggleRows.forEach { (p, toggle) ->
|
||||||
cb.setOnCheckedChangeListener { _, checked ->
|
toggle.setOnCheckedChangeListener { _, checked ->
|
||||||
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId)
|
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId)
|
||||||
val atLeastOneVisible = mibProfiles.any { it.profileId !in hidden }
|
updateToggleStates(saveBtn)
|
||||||
saveBtn.isEnabled = hidden != originalHidden && atLeastOneVisible
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
|||||||
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
|
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
|
||||||
import sh.sar.basedbank.databinding.ItemTransactionBinding
|
import sh.sar.basedbank.databinding.ItemTransactionBinding
|
||||||
|
|
||||||
/** Adapter for Transfer History — date-grouped, shows account name in secondary line. */
|
/** Adapter for Transaction History — date-grouped, shows account name in secondary line. */
|
||||||
class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private sealed class Item {
|
private sealed class Item {
|
||||||
@@ -134,7 +134,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
}
|
}
|
||||||
b.tvDescription.text = trx.description
|
b.tvDescription.text = trx.description
|
||||||
|
|
||||||
// Show account name in secondary line for Transfer History
|
// Show account name in secondary line for Transaction History
|
||||||
b.tvCounterparty.text = trx.accountDisplayName
|
b.tvCounterparty.text = trx.accountDisplayName
|
||||||
b.tvCounterparty.visibility = View.VISIBLE
|
b.tvCounterparty.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import sh.sar.basedbank.util.AccountInputParser
|
|||||||
import sh.sar.basedbank.util.PaymvQrParser
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
import sh.sar.basedbank.util.RecentPick
|
import sh.sar.basedbank.util.RecentPick
|
||||||
import sh.sar.basedbank.util.RecentsCache
|
import sh.sar.basedbank.util.RecentsCache
|
||||||
|
import sh.sar.basedbank.util.ReceiptStore
|
||||||
import sh.sar.basedbank.util.Totp
|
import sh.sar.basedbank.util.Totp
|
||||||
|
|
||||||
class TransferFragment : Fragment() {
|
class TransferFragment : Fragment() {
|
||||||
@@ -101,6 +102,8 @@ class TransferFragment : Fragment() {
|
|||||||
private const val ARG_COLOR = "contact_color"
|
private const val ARG_COLOR = "contact_color"
|
||||||
private const val ARG_IMAGE_HASH = "contact_image_hash"
|
private const val ARG_IMAGE_HASH = "contact_image_hash"
|
||||||
private const val ARG_FROM_ACCOUNT = "from_account"
|
private const val ARG_FROM_ACCOUNT = "from_account"
|
||||||
|
private const val ARG_AMOUNT_PREFILL = "amount_prefill"
|
||||||
|
private const val ARG_REMARKS_PREFILL = "remarks_prefill"
|
||||||
|
|
||||||
fun newInstanceFrom(account: MibAccount) = TransferFragment().apply {
|
fun newInstanceFrom(account: MibAccount) = TransferFragment().apply {
|
||||||
arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) }
|
arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) }
|
||||||
@@ -121,6 +124,22 @@ class TransferFragment : Fragment() {
|
|||||||
if (imageHash != null) putString(ARG_IMAGE_HASH, imageHash)
|
if (imageHash != null) putString(ARG_IMAGE_HASH, imageHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newInstanceFromQr(
|
||||||
|
accountNumber: String,
|
||||||
|
displayName: String,
|
||||||
|
amount: String?,
|
||||||
|
remarks: String?
|
||||||
|
) = TransferFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(ARG_ACCOUNT, accountNumber)
|
||||||
|
putString(ARG_NAME, displayName)
|
||||||
|
putString(ARG_SUBTITLE, accountNumber)
|
||||||
|
putString(ARG_COLOR, "#607D8B")
|
||||||
|
if (amount != null) putString(ARG_AMOUNT_PREFILL, amount)
|
||||||
|
if (remarks != null) putString(ARG_REMARKS_PREFILL, remarks)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
@@ -155,7 +174,7 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
binding.etAmount.addTextChangedListener { updateTransferButton() }
|
binding.etAmount.addTextChangedListener { updateTransferButton() }
|
||||||
|
|
||||||
// Pre-select contact if navigated from contacts page
|
// Pre-select contact if navigated from contacts page or QR scan
|
||||||
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
||||||
prefillToDirectly(
|
prefillToDirectly(
|
||||||
accountNumber = account,
|
accountNumber = account,
|
||||||
@@ -165,6 +184,8 @@ class TransferFragment : Fragment() {
|
|||||||
imageHash = arguments?.getString(ARG_IMAGE_HASH)
|
imageHash = arguments?.getString(ARG_IMAGE_HASH)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
arguments?.getString(ARG_AMOUNT_PREFILL)?.let { binding.etAmount.setText(it) }
|
||||||
|
arguments?.getString(ARG_REMARKS_PREFILL)?.let { binding.etRemarks.setText(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startLookupLoading() {
|
private fun startLookupLoading() {
|
||||||
@@ -628,6 +649,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.btnTransfer.isEnabled = true
|
binding.btnTransfer.isEnabled = true
|
||||||
(activity as? HomeActivity)?.setRefreshing(false)
|
(activity as? HomeActivity)?.setRefreshing(false)
|
||||||
if (ok && receipt != null) {
|
if (ok && receipt != null) {
|
||||||
|
ReceiptStore.save(requireContext(), receipt)
|
||||||
clearForm()
|
clearForm()
|
||||||
val activity = requireActivity() as HomeActivity
|
val activity = requireActivity() as HomeActivity
|
||||||
activity.refreshBalances(src)
|
activity.refreshBalances(src)
|
||||||
@@ -755,7 +777,7 @@ class TransferFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
val receipt = TransferReceiptData(
|
val receipt = TransferReceiptData(
|
||||||
isMib = true,
|
bank = "MIB",
|
||||||
amount = "%.2f".format(amount.toDoubleOrNull() ?: 0.0),
|
amount = "%.2f".format(amount.toDoubleOrNull() ?: 0.0),
|
||||||
currency = currency,
|
currency = currency,
|
||||||
fromLabel = src.accountBriefName,
|
fromLabel = src.accountBriefName,
|
||||||
@@ -839,7 +861,7 @@ class TransferFragment : Fragment() {
|
|||||||
val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
val receipt = TransferReceiptData(
|
val receipt = TransferReceiptData(
|
||||||
isMib = false,
|
bank = "BML",
|
||||||
amount = "%.2f".format(amount),
|
amount = "%.2f".format(amount),
|
||||||
currency = currency,
|
currency = currency,
|
||||||
fromLabel = src.accountBriefName,
|
fromLabel = src.accountBriefName,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
data class TransferReceiptData(
|
data class TransferReceiptData(
|
||||||
val isMib: Boolean,
|
val bank: String, // "MIB", "BML", etc.
|
||||||
val amount: String,
|
val amount: String,
|
||||||
val currency: String,
|
val currency: String,
|
||||||
val fromLabel: String,
|
val fromLabel: String,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
@@ -46,7 +50,7 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
private val receiptCard get() = _receiptCard!!
|
private val receiptCard get() = _receiptCard!!
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_IS_MIB = "is_mib"
|
private const val ARG_BANK = "bank"
|
||||||
private const val ARG_AMOUNT = "amount"
|
private const val ARG_AMOUNT = "amount"
|
||||||
private const val ARG_CURRENCY = "currency"
|
private const val ARG_CURRENCY = "currency"
|
||||||
private const val ARG_FROM_LABEL = "from_label"
|
private const val ARG_FROM_LABEL = "from_label"
|
||||||
@@ -69,7 +73,7 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
fun newInstance(data: TransferReceiptData, toAvatarBitmap: Bitmap?) = TransferReceiptFragment().apply {
|
fun newInstance(data: TransferReceiptData, toAvatarBitmap: Bitmap?) = TransferReceiptFragment().apply {
|
||||||
pendingToAvatarBitmap = toAvatarBitmap
|
pendingToAvatarBitmap = toAvatarBitmap
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putBoolean(ARG_IS_MIB, data.isMib)
|
putString(ARG_BANK, data.bank)
|
||||||
putString(ARG_AMOUNT, data.amount)
|
putString(ARG_AMOUNT, data.amount)
|
||||||
putString(ARG_CURRENCY, data.currency)
|
putString(ARG_CURRENCY, data.currency)
|
||||||
putString(ARG_FROM_LABEL, data.fromLabel)
|
putString(ARG_FROM_LABEL, data.fromLabel)
|
||||||
@@ -90,8 +94,8 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val isMib = arguments?.getBoolean(ARG_IS_MIB, true) ?: true
|
val bank = arguments?.getString(ARG_BANK, "MIB") ?: "MIB"
|
||||||
return if (isMib) {
|
return if (bank == "MIB") {
|
||||||
val binding = FragmentReceiptMibBinding.inflate(inflater, container, false)
|
val binding = FragmentReceiptMibBinding.inflate(inflater, container, false)
|
||||||
bindMib(binding)
|
bindMib(binding)
|
||||||
_receiptCard = binding.receiptCard
|
_receiptCard = binding.receiptCard
|
||||||
@@ -105,6 +109,8 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
receiptCard.setOnClickListener { showFullScreenReceipt() }
|
||||||
|
|
||||||
view.findViewById<MaterialButton>(R.id.btnDone).setOnClickListener {
|
view.findViewById<MaterialButton>(R.id.btnDone).setOnClickListener {
|
||||||
parentFragmentManager.popBackStack()
|
parentFragmentManager.popBackStack()
|
||||||
}
|
}
|
||||||
@@ -150,6 +156,12 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
binding.tvTransactionDate.text = args.getString(ARG_MIB_DATE, "")
|
binding.tvTransactionDate.text = args.getString(ARG_MIB_DATE, "")
|
||||||
binding.tvValueDate.text = args.getString(ARG_MIB_DATE, "")
|
binding.tvValueDate.text = args.getString(ARG_MIB_DATE, "")
|
||||||
binding.tvPurpose.text = args.getString(ARG_REMARKS, "")
|
binding.tvPurpose.text = args.getString(ARG_REMARKS, "")
|
||||||
|
|
||||||
|
copyOnLongClick(
|
||||||
|
binding.tvFromLabel, binding.tvToLabel, binding.tvAmount,
|
||||||
|
binding.tvReferenceNo, binding.tvToAccount, binding.tvToBank,
|
||||||
|
binding.tvTransactionDate, binding.tvValueDate, binding.tvPurpose
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) {
|
private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) {
|
||||||
@@ -201,6 +213,13 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
binding.remarksDivider.visibility = View.VISIBLE
|
binding.remarksDivider.visibility = View.VISIBLE
|
||||||
binding.remarksRow.visibility = View.VISIBLE
|
binding.remarksRow.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyOnLongClick(
|
||||||
|
binding.tvMessage, binding.tvMessageRow, binding.tvReference,
|
||||||
|
binding.tvTransactionDate, binding.tvFrom, binding.tvToName,
|
||||||
|
binding.tvToAccount, binding.tvAmountRow, binding.tvAmountValue,
|
||||||
|
binding.tvAmountCurrency, binding.tvRemarks
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Share / Save ──────────────────────────────────────────────────────────
|
// ── Share / Save ──────────────────────────────────────────────────────────
|
||||||
@@ -310,6 +329,54 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
return bm
|
return bm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showFullScreenReceipt() {
|
||||||
|
captureReceiptBitmap { bitmap ->
|
||||||
|
if (bitmap == null) return@captureReceiptBitmap
|
||||||
|
val ctx = requireContext()
|
||||||
|
val dialog = Dialog(ctx, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
|
||||||
|
val iv = android.widget.ImageView(ctx).apply {
|
||||||
|
setImageBitmap(bitmap)
|
||||||
|
scaleType = android.widget.ImageView.ScaleType.FIT_CENTER
|
||||||
|
setBackgroundColor(Color.BLACK)
|
||||||
|
}
|
||||||
|
iv.setOnClickListener { dialog.dismiss() }
|
||||||
|
dialog.setContentView(iv)
|
||||||
|
val actWin = requireActivity().window
|
||||||
|
val prevColor = actWin.statusBarColor
|
||||||
|
val insetsCtrl = androidx.core.view.WindowInsetsControllerCompat(actWin, actWin.decorView)
|
||||||
|
actWin.statusBarColor = Color.BLACK
|
||||||
|
insetsCtrl.isAppearanceLightStatusBars = false
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
actWin.statusBarColor = prevColor
|
||||||
|
val isLight = (resources.configuration.uiMode and
|
||||||
|
android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
|
||||||
|
android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
|
insetsCtrl.isAppearanceLightStatusBars = isLight
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
|
dialog.window?.let { win ->
|
||||||
|
androidx.core.view.WindowCompat.setDecorFitsSystemWindows(win, false)
|
||||||
|
androidx.core.view.WindowInsetsControllerCompat(win, iv).apply {
|
||||||
|
hide(androidx.core.view.WindowInsetsCompat.Type.systemBars())
|
||||||
|
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyOnLongClick(vararg views: android.widget.TextView) {
|
||||||
|
for (tv in views) {
|
||||||
|
tv.setOnLongClickListener {
|
||||||
|
val text = tv.text?.toString()?.trim() ?: return@setOnLongClickListener false
|
||||||
|
if (text.isBlank()) return@setOnLongClickListener false
|
||||||
|
val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
cm.setPrimaryClip(ClipData.newPlainText("receipt", text))
|
||||||
|
Toast.makeText(requireContext(), "Copied", Toast.LENGTH_SHORT).show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
requireActivity().title = "Receipt"
|
requireActivity().title = "Receipt"
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.ui.home.TransferReceiptData
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/** Persistent (non-cache) store for completed transfer receipts shown in Recent Transfers. */
|
||||||
|
object ReceiptStore {
|
||||||
|
|
||||||
|
private const val FILE_NAME = "activities.json"
|
||||||
|
|
||||||
|
data class Entry(val data: TransferReceiptData, val savedAt: Long)
|
||||||
|
|
||||||
|
fun save(context: Context, receipt: TransferReceiptData) {
|
||||||
|
val existing = loadAll(context).toMutableList()
|
||||||
|
existing.add(0, Entry(receipt, System.currentTimeMillis()))
|
||||||
|
writeAll(context, existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAll(context: Context): List<Entry> {
|
||||||
|
val file = File(context.filesDir, FILE_NAME)
|
||||||
|
if (!file.exists()) return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = JSONArray(CacheEncryption.decrypt(file.readText()))
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
Entry(
|
||||||
|
data = TransferReceiptData(
|
||||||
|
bank = o.optString("bank", "MIB"),
|
||||||
|
amount = o.optString("amount"),
|
||||||
|
currency = o.optString("currency"),
|
||||||
|
fromLabel = o.optString("fromLabel"),
|
||||||
|
fromColorHex = o.optString("fromColorHex"),
|
||||||
|
fromProfileImageHash = o.optString("fromProfileImageHash").takeIf { it.isNotBlank() },
|
||||||
|
toLabel = o.optString("toLabel"),
|
||||||
|
toAccount = o.optString("toAccount"),
|
||||||
|
toBank = o.optString("toBank"),
|
||||||
|
remarks = o.optString("remarks"),
|
||||||
|
mibReferenceNo = o.optString("mibReferenceNo"),
|
||||||
|
mibTransactionDate = o.optString("mibTransactionDate"),
|
||||||
|
bmlFromName = o.optString("bmlFromName"),
|
||||||
|
bmlReference = o.optString("bmlReference"),
|
||||||
|
bmlTimestamp = o.optString("bmlTimestamp"),
|
||||||
|
bmlMessage = o.optString("bmlMessage")
|
||||||
|
),
|
||||||
|
savedAt = o.optLong("savedAt", 0L)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAll(context: Context) {
|
||||||
|
File(context.filesDir, FILE_NAME).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeAll(context: Context, items: List<Entry>) {
|
||||||
|
try {
|
||||||
|
val arr = JSONArray()
|
||||||
|
for ((d, ts) in items) arr.put(JSONObject().apply {
|
||||||
|
put("bank", d.bank)
|
||||||
|
put("amount", d.amount)
|
||||||
|
put("currency", d.currency)
|
||||||
|
put("fromLabel", d.fromLabel)
|
||||||
|
put("fromColorHex", d.fromColorHex)
|
||||||
|
put("fromProfileImageHash", d.fromProfileImageHash ?: "")
|
||||||
|
put("toLabel", d.toLabel)
|
||||||
|
put("toAccount", d.toAccount)
|
||||||
|
put("toBank", d.toBank)
|
||||||
|
put("remarks", d.remarks)
|
||||||
|
put("mibReferenceNo", d.mibReferenceNo)
|
||||||
|
put("mibTransactionDate", d.mibTransactionDate)
|
||||||
|
put("bmlFromName", d.bmlFromName)
|
||||||
|
put("bmlReference", d.bmlReference)
|
||||||
|
put("bmlTimestamp", d.bmlTimestamp)
|
||||||
|
put("bmlMessage", d.bmlMessage)
|
||||||
|
put("savedAt", ts)
|
||||||
|
})
|
||||||
|
File(context.filesDir, FILE_NAME).writeText(CacheEncryption.encrypt(arr.toString()))
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
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="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||||
|
app:startIconDrawable="@android:drawable/ic_menu_search"
|
||||||
|
app:boxCornerRadiusTopStart="24dp"
|
||||||
|
app:boxCornerRadiusTopEnd="24dp"
|
||||||
|
app:boxCornerRadiusBottomStart="24dp"
|
||||||
|
app:boxCornerRadiusBottomEnd="24dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etSearch"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Search"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:imeOptions="actionSearch" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:clipToPadding="false" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/emptyView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="No recent transfers"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
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="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<!-- QR card fills all available space above the inputs -->
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvQrPlaceholder"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="Select an account"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivQrCard"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:contentDescription="@string/pay_mv_qr" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- Account dropdown -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilAccount"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:hint="@string/paymvqr_select_account">
|
||||||
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/actvAccount"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Amount (optional) -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilAmount"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:hint="@string/paymvqr_amount_hint"
|
||||||
|
app:helperText="@string/paymvqr_amount_helper"
|
||||||
|
app:prefixText="MVR ">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etAmount"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="numberDecimal"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Action buttons — always visible; share/save disabled until QR is rendered -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnShare"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="@string/paymvqr_share" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnSave"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="4dp"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="@string/paymvqr_save_image" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnScanQr"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:text="@string/transfer_scan_qr"
|
||||||
|
app:icon="@drawable/ic_qr_scan" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<string name="nav_accounts">އެކައުންޓްތައް</string>
|
<string name="nav_accounts">އެކައުންޓްތައް</string>
|
||||||
<string name="nav_contacts">ކޮންޓެކްޓްތައް</string>
|
<string name="nav_contacts">ކޮންޓެކްޓްތައް</string>
|
||||||
<string name="nav_activities">ހަރަކާތްތައް</string>
|
<string name="nav_activities">ހަރަކާތްތައް</string>
|
||||||
<string name="nav_transfer_history">ޓްރާންސްފަ ތާރީހް</string>
|
<string name="nav_transfer_history">ޓްރާންސެކްޝަން ތާރީހް</string>
|
||||||
<string name="nav_finances">ފައިނޭންސް</string>
|
<string name="nav_finances">ފައިނޭންސް</string>
|
||||||
<string name="nav_card_settings">ކާޑް ސެޓިންގ</string>
|
<string name="nav_card_settings">ކާޑް ސެޓިންގ</string>
|
||||||
<string name="nav_settings">ސެޓިންގ</string>
|
<string name="nav_settings">ސެޓިންގ</string>
|
||||||
|
|||||||
@@ -79,8 +79,8 @@
|
|||||||
<string name="nav_add_account">Add Login</string>
|
<string name="nav_add_account">Add Login</string>
|
||||||
<string name="nav_accounts">Accounts</string>
|
<string name="nav_accounts">Accounts</string>
|
||||||
<string name="nav_contacts">Contacts</string>
|
<string name="nav_contacts">Contacts</string>
|
||||||
<string name="nav_activities">Activities</string>
|
<string name="nav_activities">Recent Transfers</string>
|
||||||
<string name="nav_transfer_history">Transfer History</string>
|
<string name="nav_transfer_history">Transaction History</string>
|
||||||
<string name="nav_finances">Finances</string>
|
<string name="nav_finances">Finances</string>
|
||||||
<string name="nav_card_settings">Card Settings</string>
|
<string name="nav_card_settings">Card Settings</string>
|
||||||
<string name="nav_otp">OTP Codes</string>
|
<string name="nav_otp">OTP Codes</string>
|
||||||
@@ -99,6 +99,15 @@
|
|||||||
<string name="transfer">Transfer</string>
|
<string name="transfer">Transfer</string>
|
||||||
<string name="pay_mv_qr">PayMV QR</string>
|
<string name="pay_mv_qr">PayMV QR</string>
|
||||||
|
|
||||||
|
<!-- PayMV QR Generator -->
|
||||||
|
<string name="paymvqr_select_account">Select account</string>
|
||||||
|
<string name="paymvqr_amount_hint">Amount (optional)</string>
|
||||||
|
<string name="paymvqr_amount_helper">Leave empty to allow payer to enter any amount</string>
|
||||||
|
<string name="paymvqr_share">Share</string>
|
||||||
|
<string name="paymvqr_save_image">Save Image</string>
|
||||||
|
<string name="paymvqr_saved">QR saved to gallery</string>
|
||||||
|
<string name="paymvqr_save_failed">Failed to save image</string>
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<string name="action_lock">Lock app</string>
|
<string name="action_lock">Lock app</string>
|
||||||
<string name="autolock_warning_title">Locking soon</string>
|
<string name="autolock_warning_title">Locking soon</string>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths>
|
||||||
<cache-path name="receipt_cache" path="receipts/" />
|
<cache-path name="receipt_cache" path="receipts/" />
|
||||||
|
<cache-path name="qr_cache" path="qr/" />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
Reference in New Issue
Block a user