diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index f8b7ff5..94a25f7 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,6 +2,5 @@
-
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7ae31a6..b30fad2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
@@ -54,6 +55,16 @@
android:exported="false"
android:screenOrientation="portrait" />
+
+
+
+
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt
index 69d089b..df0d414 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt
@@ -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 {
- val sess = session ?: return Pair(false, getString(R.string.transfer_session_unavailable))
+ remarks: String,
+ bankName: String
+ ): Triple {
+ 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,
allContacts: List
- ): Pair {
- val sess = bmlSession ?: return Pair(false, getString(R.string.transfer_session_unavailable))
+ ): Triple {
+ 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
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptData.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptData.kt
new file mode 100644
index 0000000..b7829c6
--- /dev/null
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptData.kt
@@ -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 = "",
+)
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptFragment.kt
new file mode 100644
index 0000000..a76d46b
--- /dev/null
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptFragment.kt
@@ -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(R.id.btnDone).setOnClickListener {
+ parentFragmentManager.popBackStack()
+ }
+ view.findViewById(R.id.btnShare).setOnClickListener {
+ shareReceipt()
+ }
+ view.findViewById(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
+ }
+}
diff --git a/app/src/main/res/drawable/bg_mib_receipt_header.xml b/app/src/main/res/drawable/bg_mib_receipt_header.xml
new file mode 100644
index 0000000..261ae7b
--- /dev/null
+++ b/app/src/main/res/drawable/bg_mib_receipt_header.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bml_icon.jpg b/app/src/main/res/drawable/bml_icon.jpg
new file mode 100644
index 0000000..e35d50d
Binary files /dev/null and b/app/src/main/res/drawable/bml_icon.jpg differ
diff --git a/app/src/main/res/drawable/bottom_receipt_wave.jpg b/app/src/main/res/drawable/bottom_receipt_wave.jpg
new file mode 100644
index 0000000..06e069c
Binary files /dev/null and b/app/src/main/res/drawable/bottom_receipt_wave.jpg differ
diff --git a/app/src/main/res/drawable/ic_receipt_check.xml b/app/src/main/res/drawable/ic_receipt_check.xml
new file mode 100644
index 0000000..17f117b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_receipt_check.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/receiptwave.png b/app/src/main/res/drawable/receiptwave.png
new file mode 100644
index 0000000..d386802
Binary files /dev/null and b/app/src/main/res/drawable/receiptwave.png differ
diff --git a/app/src/main/res/drawable/trx_success_bg.png b/app/src/main/res/drawable/trx_success_bg.png
new file mode 100644
index 0000000..35db358
Binary files /dev/null and b/app/src/main/res/drawable/trx_success_bg.png differ
diff --git a/app/src/main/res/font/nunito_sans.ttf b/app/src/main/res/font/nunito_sans.ttf
new file mode 100644
index 0000000..89ee8de
Binary files /dev/null and b/app/src/main/res/font/nunito_sans.ttf differ
diff --git a/app/src/main/res/font/sofia_pro.ttf b/app/src/main/res/font/sofia_pro.ttf
new file mode 100644
index 0000000..e079693
Binary files /dev/null and b/app/src/main/res/font/sofia_pro.ttf differ
diff --git a/app/src/main/res/layout/fragment_receipt_bml.xml b/app/src/main/res/layout/fragment_receipt_bml.xml
new file mode 100644
index 0000000..8c9b043
--- /dev/null
+++ b/app/src/main/res/layout/fragment_receipt_bml.xml
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_receipt_mib.xml b/app/src/main/res/layout/fragment_receipt_mib.xml
new file mode 100644
index 0000000..f8a0da1
--- /dev/null
+++ b/app/src/main/res/layout/fragment_receipt_mib.xml
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..bf2868b
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/docs/bmlapi/bottom-receipt-wave.jpg b/docs/bmlapi/bottom-receipt-wave.jpg
new file mode 100644
index 0000000..06e069c
Binary files /dev/null and b/docs/bmlapi/bottom-receipt-wave.jpg differ
diff --git a/docs/bmlapi/receiptwave.png b/docs/bmlapi/receiptwave.png
new file mode 100644
index 0000000..d386802
Binary files /dev/null and b/docs/bmlapi/receiptwave.png differ