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:
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,6 +2,5 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/BinaryEye" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -54,6 +55,16 @@
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable/bg_mib_receipt_header.xml
Normal file
9
app/src/main/res/drawable/bg_mib_receipt_header.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:startColor="#2E7D32"
|
||||
android:centerColor="#43A047"
|
||||
android:endColor="#66BB6A"
|
||||
android:angle="315"
|
||||
android:type="linear" />
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/bml_icon.jpg
Normal file
BIN
app/src/main/res/drawable/bml_icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
app/src/main/res/drawable/bottom_receipt_wave.jpg
Normal file
BIN
app/src/main/res/drawable/bottom_receipt_wave.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
21
app/src/main/res/drawable/ic_receipt_check.xml
Normal file
21
app/src/main/res/drawable/ic_receipt_check.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="72dp"
|
||||
android:height="72dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<!-- Circle outline -->
|
||||
<path
|
||||
android:pathData="M50,50 m-45,0 a45,45 0 1,0 90,0 a45,45 0 1,0 -90,0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="4" />
|
||||
<!-- Checkmark -->
|
||||
<path
|
||||
android:pathData="M27,50 L41,64 L73,33"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="6"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/receiptwave.png
Normal file
BIN
app/src/main/res/drawable/receiptwave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
app/src/main/res/drawable/trx_success_bg.png
Normal file
BIN
app/src/main/res/drawable/trx_success_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
BIN
app/src/main/res/font/nunito_sans.ttf
Normal file
BIN
app/src/main/res/font/nunito_sans.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/sofia_pro.ttf
Normal file
BIN
app/src/main/res/font/sofia_pro.ttf
Normal file
Binary file not shown.
261
app/src/main/res/layout/fragment_receipt_bml.xml
Normal file
261
app/src/main/res/layout/fragment_receipt_bml.xml
Normal file
@@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="#FFFFFF">
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<!-- Renderable receipt card -->
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<LinearLayout
|
||||
android:id="@+id/receiptCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="#FFFFFF">
|
||||
|
||||
<!-- BML icon (bmlicon.jpg, centered, ~52dp ≈ 146/1080*360dp) -->
|
||||
<ImageView
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="20dp"
|
||||
android:src="@drawable/bml_icon"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<!-- Success message — NunitoSans 27.5px → 14sp, #2D2D2D -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:id="@+id/tvMessage"
|
||||
android:text="Thank you. Transfer transaction is successful."
|
||||
android:textSize="14sp"
|
||||
android:textColor="#2D2D2D"
|
||||
android:fontFamily="@font/nunito_sans"
|
||||
android:gravity="center" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<!-- Amount / wave section -->
|
||||
<!-- receiptwave.png fills this FrameLayout; text overlays center. -->
|
||||
<!-- Matches: drawImage(receiptwave, 0, 368, 1080, 155) clipped to -->
|
||||
<!-- y=379–488 visible; amount at y=355 (above wave), ccy at y=438. -->
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/receiptwave"
|
||||
android:scaleType="fitXY"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="14dp">
|
||||
|
||||
<!-- Amount — SofiaPro 68px → 42sp, #242424 -->
|
||||
<TextView
|
||||
android:id="@+id/tvAmountValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="42sp"
|
||||
android:textColor="#242424"
|
||||
android:fontFamily="@font/sofia_pro"
|
||||
android:gravity="center" />
|
||||
|
||||
<!-- Currency — SofiaPro 40px → 20sp, #7E7E7E -->
|
||||
<TextView
|
||||
android:id="@+id/tvAmountCurrency"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textSize="20sp"
|
||||
android:textColor="#7E7E7E"
|
||||
android:fontFamily="@font/sofia_pro"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<!-- Body rows -->
|
||||
<!-- Labels: SofiaPro 28px → 13sp, #000000, left-aligned -->
|
||||
<!-- Values: SofiaPro 35px → 15sp, #808080, right-aligned -->
|
||||
<!-- Divider: #E9E9E9, 1dp, margins 20dp each side -->
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:paddingTop="6dp">
|
||||
|
||||
<!-- Status (value = green #8BC155) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Status" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvStatus" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="SUCCESS" android:textSize="15sp" android:textColor="#8BC155" android:fontFamily="@font/sofia_pro" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#E9E9E9" />
|
||||
|
||||
<!-- Message (value wraps if long) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Message" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvMessageRow" android:layout_width="0dp" android:layout_weight="1.4" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#808080" android:fontFamily="@font/sofia_pro" android:gravity="end" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#E9E9E9" />
|
||||
|
||||
<!-- Reference -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Reference" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvReference" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#808080" android:fontFamily="@font/sofia_pro" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#E9E9E9" />
|
||||
|
||||
<!-- Transaction date -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Transaction date" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvTransactionDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#808080" android:fontFamily="@font/sofia_pro" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#E9E9E9" />
|
||||
|
||||
<!-- From (value uppercase) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="From" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvFrom" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#808080" android:fontFamily="@font/sofia_pro" android:textAllCaps="false" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#E9E9E9" />
|
||||
|
||||
<!-- To (stacked name + account) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="To" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="end">
|
||||
<TextView android:id="@+id/tvToName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#808080" android:fontFamily="@font/sofia_pro" android:gravity="end" />
|
||||
<TextView android:id="@+id/tvToAccount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:textSize="13sp" android:textColor="#808080" android:fontFamily="@font/sofia_pro" android:gravity="end" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#E9E9E9" />
|
||||
|
||||
<!-- Amount (value = green #8BC155) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Amount" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvAmountRow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#8BC155" android:fontFamily="@font/sofia_pro" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Remarks (hidden when empty) -->
|
||||
<View android:id="@+id/remarksDivider" android:layout_width="match_parent" android:layout_height="1dp" android:background="#E9E9E9" android:visibility="gone" />
|
||||
<LinearLayout android:id="@+id/remarksRow" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingVertical="13dp" android:visibility="gone">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Remarks" android:textSize="13sp" android:textColor="#000000" android:fontFamily="@font/sofia_pro" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvRemarks" android:layout_width="0dp" android:layout_weight="1.4" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#808080" android:fontFamily="@font/sofia_pro" android:gravity="end" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<!-- Footer — bottom wave + #F5F5F5 bg + "Bank of Maldives" -->
|
||||
<!-- 113px gap ≈ 38dp, then wave overlaps, then footer rect 157px≈52dp-->
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- Gap between last row and footer wave (113px / 3 ≈ 38dp) -->
|
||||
<View android:layout_width="match_parent" android:layout_height="38dp" />
|
||||
|
||||
<!-- Bottom wave — sits on top of footer, extends slightly upward -->
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/bottom_receipt_wave"
|
||||
android:scaleType="fitXY"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<!-- Footer — #F5F5F5, "Bank of Maldives" centered -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:background="#F5F5F5"
|
||||
android:paddingVertical="22dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Bank of Maldives"
|
||||
android:textSize="13sp"
|
||||
android:textColor="#848484"
|
||||
android:fontFamily="@font/sofia_pro"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Pushes buttons to bottom of screen -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<!-- Action buttons — outside renderable area -->
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="#FFFFFF"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnShare"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="Share" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:text="Save" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDone"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="Done" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
282
app/src/main/res/layout/fragment_receipt_mib.xml
Normal file
282
app/src/main/res/layout/fragment_receipt_mib.xml
Normal file
@@ -0,0 +1,282 @@
|
||||
<?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="#FFFFFF">
|
||||
|
||||
<!-- Renderable receipt card (header grows to fill remaining space) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/receiptCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Green header — fills all space not taken by body -->
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/trx_success_bg">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TRANSACTION RECEIPT"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:letterSpacing="0.08"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@drawable/ic_receipt_check"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SUCCESSFUL"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.05"
|
||||
android:layout_marginTop="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- White body — fixed size content -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="#FFFFFF"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<!-- Avatars row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivFromAvatar"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearance.Circle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFromLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#1C1B1F"
|
||||
android:gravity="center"
|
||||
android:maxLines="2" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:text="→"
|
||||
android:textSize="20sp"
|
||||
android:textColor="#49454F" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivToAvatar"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearance.Circle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvToLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#1C1B1F"
|
||||
android:gravity="center"
|
||||
android:maxLines="2" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<!-- Total Amount -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TOTAL AMOUNT"
|
||||
android:textSize="11sp"
|
||||
android:textColor="#49454F"
|
||||
android:letterSpacing="0.08"
|
||||
android:gravity="center"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAmount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#E65100"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<!-- Reference # -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingHorizontal="20dp" android:paddingVertical="11dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Reference #" android:textSize="13sp" android:textColor="#49454F" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvReferenceNo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" android:textColor="#1C1B1F" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<!-- To Account -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingHorizontal="20dp" android:paddingVertical="11dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="To Account" android:textSize="13sp" android:textColor="#49454F" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvToAccount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" android:textColor="#1C1B1F" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<!-- To Bank -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingHorizontal="20dp" android:paddingVertical="11dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="To Bank" android:textSize="13sp" android:textColor="#49454F" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvToBank" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" android:textColor="#1C1B1F" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<!-- Transaction Date -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingHorizontal="20dp" android:paddingVertical="11dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Transaction Date" android:textSize="13sp" android:textColor="#49454F" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvTransactionDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" android:textColor="#1C1B1F" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<!-- Value Date -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingHorizontal="20dp" android:paddingVertical="11dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Value Date" android:textSize="13sp" android:textColor="#49454F" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvValueDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" android:textColor="#1C1B1F" />
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<!-- Purpose -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingHorizontal="20dp" android:paddingVertical="11dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Purpose" android:textSize="13sp" android:textColor="#49454F" />
|
||||
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
|
||||
<TextView android:id="@+id/tvPurpose" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" android:textColor="#1C1B1F" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- MIB footer -->
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="#CAC4D0" android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="28dp"
|
||||
android:src="@drawable/mib_logo"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="MALDIVES ISLAMIC BANK"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#49454F"
|
||||
android:letterSpacing="0.05" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Action buttons — outside renderable area -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="#FFFFFF"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnShare"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="Share" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:text="Save" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDone"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="Done" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
4
app/src/main/res/xml/file_paths.xml
Normal file
4
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="receipt_cache" path="receipts/" />
|
||||
</paths>
|
||||
BIN
docs/bmlapi/bottom-receipt-wave.jpg
Normal file
BIN
docs/bmlapi/bottom-receipt-wave.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
docs/bmlapi/receiptwave.png
Normal file
BIN
docs/bmlapi/receiptwave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
Reference in New Issue
Block a user