add transfer recipt for bml and mib
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
This commit is contained in:
@@ -59,6 +59,7 @@ class TransferFragment : Fragment() {
|
||||
// Resolved recipient info — set after successful lookup or prefill
|
||||
private var resolvedAccountNumber = ""
|
||||
private var resolvedRecipientName = ""
|
||||
private var resolvedBankName = ""
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
@@ -268,6 +269,7 @@ class TransferFragment : Fragment() {
|
||||
|
||||
resolvedAccountNumber = info.accountNumber
|
||||
resolvedRecipientName = info.accountName
|
||||
resolvedBankName = info.bankId
|
||||
|
||||
binding.tvToAccountName.text = displayName
|
||||
binding.tvToBankBic.text = "${info.accountNumber} · ${info.bankId}"
|
||||
@@ -299,6 +301,8 @@ class TransferFragment : Fragment() {
|
||||
) {
|
||||
resolvedAccountNumber = accountNumber
|
||||
resolvedRecipientName = displayName
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
resolvedBankName = contacts.firstOrNull { it.benefAccount == accountNumber }?.benefBankName ?: ""
|
||||
|
||||
binding.tvToAccountName.text = displayName
|
||||
binding.tvToBankBic.text = subtitle
|
||||
@@ -308,7 +312,6 @@ class TransferFragment : Fragment() {
|
||||
binding.btnScanQr.visibility = View.GONE
|
||||
binding.cardToInfo.visibility = View.VISIBLE
|
||||
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
||||
if (contact != null) {
|
||||
RecentsCache.save(requireContext(), RecentPick(
|
||||
@@ -376,29 +379,28 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
val destDisplay = binding.tvToAccountName.text?.toString() ?: resolvedAccountNumber
|
||||
val bankNameCapture = resolvedBankName
|
||||
val capturedToAvatar = (binding.ivToPhoto.drawable as? android.graphics.drawable.BitmapDrawable)?.bitmap
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.transfer)
|
||||
.setMessage("Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}")
|
||||
.setPositiveButton(R.string.transfer_confirm) { _, _ ->
|
||||
binding.btnTransfer.isEnabled = false
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val (ok, msg) = withContext(Dispatchers.IO) {
|
||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||
if (!isSrcBml) {
|
||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, amountStr, remarks)
|
||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
||||
} else {
|
||||
doBmlTransfer(src, resolvedAccountNumber, amount, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||
}
|
||||
}
|
||||
binding.btnTransfer.isEnabled = true
|
||||
if (ok) {
|
||||
if (ok && receipt != null) {
|
||||
clearForm()
|
||||
(requireActivity() as HomeActivity).refreshBalances(src)
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.transfer_success)
|
||||
.setMessage(msg)
|
||||
.setPositiveButton("OK", null)
|
||||
.show()
|
||||
} else {
|
||||
val activity = requireActivity() as HomeActivity
|
||||
activity.refreshBalances(src)
|
||||
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
||||
} else if (!ok) {
|
||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@@ -411,10 +413,12 @@ class TransferFragment : Fragment() {
|
||||
src: MibAccount,
|
||||
destAccount: String,
|
||||
destName: String,
|
||||
destDisplay: String,
|
||||
amount: String,
|
||||
remarks: String
|
||||
): Pair<Boolean, String> {
|
||||
val sess = session ?: return Pair(false, getString(R.string.transfer_session_unavailable))
|
||||
remarks: String,
|
||||
bankName: String
|
||||
): Triple<Boolean, String, TransferReceiptData?> {
|
||||
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
// Switch to the profile that owns the source account
|
||||
if (src.profileId.isNotBlank()) {
|
||||
@@ -423,10 +427,19 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
val otp = CredentialStore(requireContext()).loadMibCredentials()?.otpSeed
|
||||
?.let { Totp.generate(it) }
|
||||
?: return Pair(false, "OTP unavailable")
|
||||
?: return Triple(false, "OTP unavailable", null)
|
||||
val currencyCode = if (src.currencyName == "USD") "840" else "462"
|
||||
val currency = if (src.currencyName == "USD") "USD" else "MVR"
|
||||
val isDestMib = destAccount.matches(Regex("^9\\d{16}$"))
|
||||
val bankNo = if (isDestMib) 2 else 3
|
||||
val toBank = when {
|
||||
isDestMib -> "MIB"
|
||||
else -> when (bankName.uppercase()) {
|
||||
"MALBMVMV" -> "BML"
|
||||
"MADVMVMV" -> "MIB"
|
||||
else -> bankName.ifBlank { "LOCAL" }
|
||||
}
|
||||
}
|
||||
return try {
|
||||
val result = MibTransferClient().transfer(
|
||||
session = sess,
|
||||
@@ -440,32 +453,48 @@ class TransferFragment : Fragment() {
|
||||
otp = otp
|
||||
)
|
||||
if (result.success) {
|
||||
Pair(true, "Transaction ID: ${result.trxId}\n${result.date}")
|
||||
val receipt = TransferReceiptData(
|
||||
isMib = true,
|
||||
amount = "%.2f".format(amount.toDoubleOrNull() ?: 0.0),
|
||||
currency = currency,
|
||||
fromLabel = src.accountBriefName,
|
||||
fromColorHex = "#FE860E",
|
||||
fromProfileImageHash = src.profileImageHash,
|
||||
toLabel = destDisplay.ifBlank { destName },
|
||||
toAccount = destAccount,
|
||||
toBank = toBank,
|
||||
remarks = remarks,
|
||||
mibReferenceNo = result.trxId,
|
||||
mibTransactionDate = result.date
|
||||
)
|
||||
Triple(true, "Transaction ID: ${result.trxId}\n${result.date}", receipt)
|
||||
} else {
|
||||
Pair(false, result.errorMessage.ifBlank { "Transfer failed" })
|
||||
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Pair(false, e.message ?: "Transfer failed")
|
||||
Triple(false, e.message ?: "Transfer failed", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun doBmlTransfer(
|
||||
src: MibAccount,
|
||||
destAccount: String,
|
||||
destDisplay: String,
|
||||
amount: Double,
|
||||
amountStr: String,
|
||||
remarks: String,
|
||||
isSrcCard: Boolean,
|
||||
isDestMib: Boolean,
|
||||
currency: String,
|
||||
allAccounts: List<MibAccount>,
|
||||
allContacts: List<MibBeneficiary>
|
||||
): Pair<Boolean, String> {
|
||||
val sess = bmlSession ?: return Pair(false, getString(R.string.transfer_session_unavailable))
|
||||
): Triple<Boolean, String, TransferReceiptData?> {
|
||||
val sess = bmlSession ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||
val otp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
||||
?.let { Totp.generate(it) }
|
||||
?: return Pair(false, "OTP unavailable")
|
||||
?: return Triple(false, "OTP unavailable", null)
|
||||
val debitAccount = src.internalId.ifBlank {
|
||||
return Pair(false, getString(R.string.transfer_missing_internal_id))
|
||||
return Triple(false, getString(R.string.transfer_missing_internal_id), null)
|
||||
}
|
||||
|
||||
// Determine type + credit account
|
||||
@@ -485,19 +514,20 @@ class TransferFragment : Fragment() {
|
||||
isDestMib -> {
|
||||
// USD DOT: requires BML contact numeric ID
|
||||
val contact = allContacts.firstOrNull { it.benefCategoryId == "BML" && it.benefAccount == destAccount }
|
||||
?: return Pair(false, "BML contact not found for this account")
|
||||
?: return Triple(false, "BML contact not found for this account", null)
|
||||
Triple("DOT", contact.benefNo.removePrefix("bml_"), null as String?)
|
||||
}
|
||||
else -> Triple("IAT", destAccount, null as String?)
|
||||
}
|
||||
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
||||
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
// Step 1: initiate
|
||||
val initiated = try {
|
||||
bmlFlow.initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
|
||||
} catch (e: Exception) { return Pair(false, e.message ?: "Initiation failed") }
|
||||
} catch (e: Exception) { return Triple(false, e.message ?: "Initiation failed", null) }
|
||||
|
||||
if (!initiated) return Pair(false, "Failed to initiate transfer — check your session")
|
||||
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
||||
|
||||
// Step 2: confirm with fresh OTP
|
||||
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
||||
@@ -506,13 +536,27 @@ class TransferFragment : Fragment() {
|
||||
return try {
|
||||
val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
||||
if (result.success) {
|
||||
val receipt = TransferReceiptData(
|
||||
isMib = false,
|
||||
amount = "%.2f".format(amount),
|
||||
currency = currency,
|
||||
fromLabel = src.accountBriefName,
|
||||
fromColorHex = "#0066A1",
|
||||
toLabel = destDisplay.ifBlank { destAccount },
|
||||
toAccount = destAccount,
|
||||
toBank = toBank,
|
||||
remarks = remarks,
|
||||
bmlFromName = src.accountBriefName,
|
||||
bmlReference = result.reference,
|
||||
bmlTimestamp = result.timestamp
|
||||
)
|
||||
val time = result.timestamp.take(19).replace("T", " ")
|
||||
Pair(true, "Reference: ${result.reference}\n$time")
|
||||
Triple(true, "Reference: ${result.reference}\n$time", receipt)
|
||||
} else {
|
||||
Pair(false, result.errorMessage.ifBlank { "Transfer failed" })
|
||||
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Pair(false, e.message ?: "Transfer failed")
|
||||
Triple(false, e.message ?: "Transfer failed", null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,6 +568,7 @@ class TransferFragment : Fragment() {
|
||||
binding.etRemarks.setText("")
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
resolvedBankName = ""
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
data class TransferReceiptData(
|
||||
val isMib: Boolean,
|
||||
val amount: String,
|
||||
val currency: String,
|
||||
val fromLabel: String,
|
||||
val fromColorHex: String,
|
||||
val fromProfileImageHash: String? = null,
|
||||
val toLabel: String,
|
||||
val toAccount: String,
|
||||
val toBank: String,
|
||||
val remarks: String,
|
||||
// MIB receipt fields
|
||||
val mibReferenceNo: String = "",
|
||||
val mibTransactionDate: String = "",
|
||||
// BML receipt fields
|
||||
val bmlFromName: String = "",
|
||||
val bmlReference: String = "",
|
||||
val bmlTimestamp: String = "",
|
||||
)
|
||||
@@ -0,0 +1,319 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.PixelCopy
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.databinding.FragmentReceiptBmlBinding
|
||||
import sh.sar.basedbank.databinding.FragmentReceiptMibBinding
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.text.NumberFormat
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
class TransferReceiptFragment : Fragment() {
|
||||
|
||||
private var _receiptCard: View? = null
|
||||
private val receiptCard get() = _receiptCard!!
|
||||
|
||||
companion object {
|
||||
private const val ARG_IS_MIB = "is_mib"
|
||||
private const val ARG_AMOUNT = "amount"
|
||||
private const val ARG_CURRENCY = "currency"
|
||||
private const val ARG_FROM_LABEL = "from_label"
|
||||
private const val ARG_FROM_COLOR = "from_color"
|
||||
private const val ARG_FROM_PROFILE_HASH = "from_profile_hash"
|
||||
private const val ARG_TO_LABEL = "to_label"
|
||||
private const val ARG_TO_ACCOUNT = "to_account"
|
||||
private const val ARG_TO_BANK = "to_bank"
|
||||
private const val ARG_REMARKS = "remarks"
|
||||
private const val ARG_MIB_REF = "mib_ref"
|
||||
private const val ARG_MIB_DATE = "mib_date"
|
||||
private const val ARG_BML_FROM_NAME = "bml_from_name"
|
||||
private const val ARG_BML_REFERENCE = "bml_reference"
|
||||
private const val ARG_BML_TIMESTAMP = "bml_timestamp"
|
||||
|
||||
// Holds the already-rendered to-avatar bitmap from TransferFragment
|
||||
var pendingToAvatarBitmap: Bitmap? = null
|
||||
|
||||
fun newInstance(data: TransferReceiptData, toAvatarBitmap: Bitmap?) = TransferReceiptFragment().apply {
|
||||
pendingToAvatarBitmap = toAvatarBitmap
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(ARG_IS_MIB, data.isMib)
|
||||
putString(ARG_AMOUNT, data.amount)
|
||||
putString(ARG_CURRENCY, data.currency)
|
||||
putString(ARG_FROM_LABEL, data.fromLabel)
|
||||
putString(ARG_FROM_COLOR, data.fromColorHex)
|
||||
putString(ARG_FROM_PROFILE_HASH, data.fromProfileImageHash)
|
||||
putString(ARG_TO_LABEL, data.toLabel)
|
||||
putString(ARG_TO_ACCOUNT, data.toAccount)
|
||||
putString(ARG_TO_BANK, data.toBank)
|
||||
putString(ARG_REMARKS, data.remarks)
|
||||
putString(ARG_MIB_REF, data.mibReferenceNo)
|
||||
putString(ARG_MIB_DATE, data.mibTransactionDate)
|
||||
putString(ARG_BML_FROM_NAME, data.bmlFromName)
|
||||
putString(ARG_BML_REFERENCE, data.bmlReference)
|
||||
putString(ARG_BML_TIMESTAMP, data.bmlTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val isMib = arguments?.getBoolean(ARG_IS_MIB, true) ?: true
|
||||
return if (isMib) {
|
||||
val binding = FragmentReceiptMibBinding.inflate(inflater, container, false)
|
||||
bindMib(binding)
|
||||
_receiptCard = binding.receiptCard
|
||||
binding.root
|
||||
} else {
|
||||
val binding = FragmentReceiptBmlBinding.inflate(inflater, container, false)
|
||||
bindBml(binding)
|
||||
_receiptCard = binding.receiptCard
|
||||
binding.root
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
view.findViewById<MaterialButton>(R.id.btnDone).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
view.findViewById<MaterialButton>(R.id.btnShare).setOnClickListener {
|
||||
shareReceipt()
|
||||
}
|
||||
view.findViewById<MaterialButton>(R.id.btnSave).setOnClickListener {
|
||||
saveReceipt()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Data binding ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun bindMib(binding: FragmentReceiptMibBinding) {
|
||||
val args = requireArguments()
|
||||
val fromLabel = args.getString(ARG_FROM_LABEL, "")
|
||||
val fromColor = args.getString(ARG_FROM_COLOR, "#FE860E")
|
||||
val fromProfileHash = args.getString(ARG_FROM_PROFILE_HASH)
|
||||
val toLabel = args.getString(ARG_TO_LABEL, "")
|
||||
val currency = args.getString(ARG_CURRENCY, "MVR")
|
||||
val amount = args.getString(ARG_AMOUNT, "")
|
||||
|
||||
// From avatar: initials first, then load profile image if hash available
|
||||
binding.ivFromAvatar.setImageBitmap(makeInitialsBitmap(fromLabel, fromColor))
|
||||
binding.tvFromLabel.text = fromLabel
|
||||
if (fromProfileHash != null) {
|
||||
loadProfileImage(fromProfileHash, isProfile = true) { binding.ivFromAvatar.setImageBitmap(it) }
|
||||
}
|
||||
|
||||
// To avatar: use already-rendered bitmap from TransferFragment if available
|
||||
val toAvatar = pendingToAvatarBitmap
|
||||
if (toAvatar != null) {
|
||||
binding.ivToAvatar.setImageBitmap(toAvatar)
|
||||
} else {
|
||||
binding.ivToAvatar.setImageBitmap(makeInitialsBitmap(toLabel, "#607D8B"))
|
||||
}
|
||||
binding.tvToLabel.text = toLabel
|
||||
|
||||
binding.tvAmount.text = "$currency $amount"
|
||||
binding.tvReferenceNo.text = args.getString(ARG_MIB_REF, "")
|
||||
binding.tvToAccount.text = args.getString(ARG_TO_ACCOUNT, "")
|
||||
binding.tvToBank.text = args.getString(ARG_TO_BANK, "")
|
||||
binding.tvTransactionDate.text = args.getString(ARG_MIB_DATE, "")
|
||||
binding.tvValueDate.text = args.getString(ARG_MIB_DATE, "")
|
||||
binding.tvPurpose.text = args.getString(ARG_REMARKS, "").ifBlank { "-" }
|
||||
}
|
||||
|
||||
private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) {
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
val sess = app.mibSession ?: return
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val base64 = if (isProfile) {
|
||||
app.mibLoginFlow.fetchProfileImage(sess, hash)
|
||||
} else {
|
||||
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
||||
} ?: return@launch
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||
withContext(Dispatchers.Main) { if (_receiptCard != null) onLoaded(bitmap) }
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindBml(binding: FragmentReceiptBmlBinding) {
|
||||
val args = requireArguments()
|
||||
val currency = args.getString(ARG_CURRENCY, "MVR")
|
||||
val amountStr = args.getString(ARG_AMOUNT, "")
|
||||
|
||||
// Format with thousands separator: "1000.00" → "1,000.00"
|
||||
val formattedAmount = try {
|
||||
val d = amountStr.toDouble()
|
||||
val intFmt = NumberFormat.getNumberInstance(Locale.US).apply { maximumFractionDigits = 0 }
|
||||
intFmt.format(d.toLong()) + "%.2f".format(d).takeLast(3)
|
||||
} catch (_: Exception) { amountStr }
|
||||
|
||||
binding.tvAmountValue.text = formattedAmount
|
||||
binding.tvAmountCurrency.text = currency
|
||||
binding.tvMessageRow.text = "Thank you. Transfer transaction is successful."
|
||||
binding.tvReference.text = args.getString(ARG_BML_REFERENCE, "")
|
||||
binding.tvTransactionDate.text = formatBmlTimestamp(args.getString(ARG_BML_TIMESTAMP, ""))
|
||||
binding.tvFrom.text = args.getString(ARG_BML_FROM_NAME, "").ifBlank {
|
||||
args.getString(ARG_FROM_LABEL, "")
|
||||
}.uppercase(Locale.US)
|
||||
binding.tvToName.text = args.getString(ARG_TO_LABEL, "")
|
||||
binding.tvToAccount.text = args.getString(ARG_TO_ACCOUNT, "")
|
||||
binding.tvAmountRow.text = "$currency $formattedAmount"
|
||||
|
||||
val remarks = args.getString(ARG_REMARKS, "")
|
||||
if (!remarks.isNullOrBlank()) {
|
||||
binding.tvRemarks.text = remarks
|
||||
binding.remarksDivider.visibility = View.VISIBLE
|
||||
binding.remarksRow.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
// ── Share / Save ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun shareReceipt() {
|
||||
captureReceiptBitmap { bitmap ->
|
||||
if (bitmap == null) {
|
||||
Toast.makeText(requireContext(), "Failed to render receipt", Toast.LENGTH_SHORT).show()
|
||||
return@captureReceiptBitmap
|
||||
}
|
||||
try {
|
||||
val dir = File(requireContext().cacheDir, "receipts").also { it.mkdirs() }
|
||||
val file = File(dir, "receipt_${System.currentTimeMillis()}.png")
|
||||
FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||
val uri: Uri = FileProvider.getUriForFile(
|
||||
requireContext(), "${requireContext().packageName}.fileprovider", file
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "image/png"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, "Share Receipt"))
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Share failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveReceipt() {
|
||||
captureReceiptBitmap { bitmap ->
|
||||
if (bitmap == null) {
|
||||
Toast.makeText(requireContext(), "Failed to render receipt", Toast.LENGTH_SHORT).show()
|
||||
return@captureReceiptBitmap
|
||||
}
|
||||
val filename = "receipt_${System.currentTimeMillis()}.png"
|
||||
try {
|
||||
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 resolver = requireContext().contentResolver
|
||||
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
?: throw Exception("Could not create media entry")
|
||||
resolver.openOutputStream(uri)?.use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||
} else {
|
||||
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
dir.mkdirs()
|
||||
val file = File(dir, filename)
|
||||
FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||
android.media.MediaScannerConnection.scanFile(
|
||||
requireContext(), arrayOf(file.absolutePath), null, null
|
||||
)
|
||||
}
|
||||
Toast.makeText(requireContext(), "Receipt saved to Photos", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Save failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Captures the receipt card using PixelCopy, which correctly handles
|
||||
* hardware-accelerated views (avoids the black-square problem with view.draw()).
|
||||
*/
|
||||
private fun captureReceiptBitmap(callback: (Bitmap?) -> Unit) {
|
||||
val view = _receiptCard ?: run { callback(null); return }
|
||||
if (view.width == 0 || view.height == 0) { callback(null); return }
|
||||
|
||||
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
|
||||
val location = IntArray(2)
|
||||
view.getLocationInWindow(location)
|
||||
val srcRect = Rect(location[0], location[1], location[0] + view.width, location[1] + view.height)
|
||||
|
||||
PixelCopy.request(requireActivity().window, srcRect, bitmap, { result ->
|
||||
callback(if (result == PixelCopy.SUCCESS) bitmap else null)
|
||||
}, Handler(Looper.getMainLooper()))
|
||||
}
|
||||
|
||||
private fun formatBmlTimestamp(raw: String): String {
|
||||
if (raw.isBlank()) return ""
|
||||
return try {
|
||||
OffsetDateTime.parse(raw).format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"))
|
||||
} catch (_: Exception) {
|
||||
raw.take(16)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeInitialsBitmap(name: String, colorHex: String): Bitmap {
|
||||
val sizePx = (resources.displayMetrics.density * 52).toInt()
|
||||
val bgColor = try { Color.parseColor(colorHex) } catch (_: Exception) { Color.GRAY }
|
||||
val bm = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bm)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
paint.color = bgColor
|
||||
canvas.drawCircle(sizePx / 2f, sizePx / 2f, sizePx / 2f, paint)
|
||||
paint.color = Color.WHITE
|
||||
paint.textSize = sizePx * 0.42f
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
val letter = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
|
||||
val metrics = paint.fontMetrics
|
||||
canvas.drawText(letter, sizePx / 2f, sizePx / 2f - (metrics.ascent + metrics.descent) / 2f, paint)
|
||||
return bm
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = "Receipt"
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_receiptCard = null
|
||||
pendingToAvatarBitmap = null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user