BML account lookup and add new contacts
This commit is contained in:
@@ -203,6 +203,107 @@ class BmlLoginFlow {
|
||||
} catch (_: Exception) { "" }
|
||||
}
|
||||
|
||||
fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? {
|
||||
val resp = apiClient.newCall(
|
||||
Request.Builder().url("$BASE_URL/api/mobile/validate/account/$input")
|
||||
.header("Authorization", "Bearer ${session.accessToken}")
|
||||
.header("User-Agent", APP_USER_AGENT)
|
||||
.header("x-app-version", APP_VERSION)
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: return null
|
||||
resp.close()
|
||||
return try {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return null
|
||||
val payload = root.optJSONObject("payload") ?: return null
|
||||
val trnType = payload.optString("trnType", "")
|
||||
val validationType = payload.optString("validationType", "")
|
||||
if (validationType == "alias") {
|
||||
val cdtrAcct = payload.optJSONObject("CdtrAcct") ?: return null
|
||||
BmlAccountValidation(
|
||||
trnType = trnType,
|
||||
validationType = validationType,
|
||||
account = cdtrAcct.optString("Acct"),
|
||||
originalInput = input,
|
||||
name = payload.optString("contact_name").trim(),
|
||||
alias = null,
|
||||
currency = payload.optString("currency", "MVR"),
|
||||
agnt = cdtrAcct.optString("FinInstnId").takeIf { it.isNotBlank() }
|
||||
)
|
||||
} else {
|
||||
BmlAccountValidation(
|
||||
trnType = trnType,
|
||||
validationType = validationType,
|
||||
account = payload.optString("account"),
|
||||
originalInput = input,
|
||||
name = payload.optString("name"),
|
||||
alias = payload.optString("alias").takeIf { it.isNotBlank() && it != "null" },
|
||||
currency = payload.optString("currency", "MVR")
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun verifyMibAccount(session: BmlSession, account: String): BmlAccountValidation? {
|
||||
val resp = apiClient.newCall(
|
||||
Request.Builder().url("$BASE_URL/api/mobile/favara/account-verification/$account/MIB")
|
||||
.header("Authorization", "Bearer ${session.accessToken}")
|
||||
.header("User-Agent", APP_USER_AGENT)
|
||||
.header("x-app-version", APP_VERSION)
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: return null
|
||||
resp.close()
|
||||
return try {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return null
|
||||
BmlAccountValidation(
|
||||
trnType = "DOT",
|
||||
validationType = "MIB",
|
||||
account = root.optString("account"),
|
||||
originalInput = account,
|
||||
name = root.optString("name"),
|
||||
alias = null,
|
||||
currency = "MVR",
|
||||
agnt = root.optString("agnt").takeIf { it.isNotBlank() }
|
||||
)
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun saveContact(
|
||||
session: BmlSession,
|
||||
contactType: String,
|
||||
account: String,
|
||||
alias: String,
|
||||
currency: String? = null,
|
||||
name: String? = null,
|
||||
swift: String? = null
|
||||
): Boolean {
|
||||
val bodyObj = JSONObject().apply {
|
||||
put("contact_type", contactType)
|
||||
put("account", account)
|
||||
put("alias", alias)
|
||||
if (currency != null) put("currency", currency)
|
||||
if (name != null) put("name", name)
|
||||
if (swift != null) put("swift", swift)
|
||||
}
|
||||
val resp = apiClient.newCall(
|
||||
Request.Builder().url("$BASE_URL/api/mobile/contacts")
|
||||
.post(bodyObj.toString().toRequestBody("application/json".toMediaType()))
|
||||
.header("Authorization", "Bearer ${session.accessToken}")
|
||||
.header("User-Agent", APP_USER_AGENT)
|
||||
.header("x-app-version", APP_VERSION)
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: return false
|
||||
resp.close()
|
||||
return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute()
|
||||
val json = resp.body?.string() ?: return emptyList()
|
||||
|
||||
@@ -5,6 +5,17 @@ data class BmlSession(
|
||||
val deviceId: String
|
||||
)
|
||||
|
||||
data class BmlAccountValidation(
|
||||
val trnType: String, // IAT, QTR, DOT
|
||||
val validationType: String, // BML, alias, MIB
|
||||
val account: String, // resolved account number
|
||||
val originalInput: String, // original user input (alias/Favara for QTR)
|
||||
val name: String,
|
||||
val alias: String?,
|
||||
val currency: String,
|
||||
val agnt: String? = null // BIC for DOT (MIB account on BML)
|
||||
)
|
||||
|
||||
data class BmlForeignLimit(
|
||||
val type: String,
|
||||
val used: Double,
|
||||
|
||||
@@ -126,6 +126,62 @@ class MibContactsClient {
|
||||
}
|
||||
}
|
||||
|
||||
fun createContact(
|
||||
session: MibSession,
|
||||
benefType: String, // "I" = MIB internal, "L" = local/BML
|
||||
bankNo: Int, // 2 = MIB, 3 = BML
|
||||
benefAccount: String,
|
||||
benefName: String,
|
||||
nickName: String,
|
||||
transferCy: String = "462",
|
||||
categoryId: String = "0",
|
||||
imageBase64: String = ""
|
||||
): Boolean {
|
||||
val body = FormBody.Builder()
|
||||
.add("benefType", benefType)
|
||||
.add("imageSet", if (imageBase64.isNotBlank()) "1" else "0")
|
||||
.add("benefAccount", benefAccount)
|
||||
.add("benefIban", "")
|
||||
.add("benefName", benefName)
|
||||
.add("nickName", nickName)
|
||||
.add("transferCy", transferCy)
|
||||
.add("transferCySwift", "840")
|
||||
.add("benefAddress", "")
|
||||
.add("benefCity", "")
|
||||
.add("benefCountry", "4")
|
||||
.add("benefBankSwift", "")
|
||||
.add("bankNo", bankNo.toString())
|
||||
.add("benefBankName", "")
|
||||
.add("benefBankBranch", "")
|
||||
.add("benefBankAddress", "")
|
||||
.add("benefBankCity", "")
|
||||
.add("benefBankCountry", "4")
|
||||
.add("intBankSwift", "")
|
||||
.add("intBankName", "")
|
||||
.add("intBankAddress", "")
|
||||
.add("intBankBranch", "")
|
||||
.add("intBankCity", "")
|
||||
.add("intBankCountry", "4")
|
||||
.add("categoryId", categoryId)
|
||||
.add("email", "")
|
||||
.add("contactNumber", "")
|
||||
.add("website", "")
|
||||
.add("image", imageBase64)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_WV_URL/ajaxBeneficiary/createLocalBeneficiary")
|
||||
.post(body)
|
||||
.withSessionHeaders(session)
|
||||
.header("Referer", "$BASE_WV_URL/beneficiary/createNew")
|
||||
.build()
|
||||
|
||||
return client.newCall(request).execute().use { response ->
|
||||
val bodyStr = response.body?.string() ?: return@use false
|
||||
try { JSONObject(bodyStr).optBoolean("success") } catch (_: Exception) { false }
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchProfileImageBase64(session: MibSession, imageHash: String): String? {
|
||||
val body = FormBody.Builder()
|
||||
.add("imageHash", imageHash)
|
||||
|
||||
@@ -232,7 +232,8 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
||||
mvrBalance = a.optString("mvrBalance"),
|
||||
statusDesc = a.optString("statusDesc"),
|
||||
profileImageHash = profile.customerImage,
|
||||
loginTag = loginTag
|
||||
loginTag = loginTag,
|
||||
profileId = profile.profileId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ data class MibAccount(
|
||||
val mvrBalance: String,
|
||||
val statusDesc: String,
|
||||
val profileImageHash: String?,
|
||||
val loginTag: String = ""
|
||||
val loginTag: String = "",
|
||||
val profileId: String = "" // MIB profile ID; empty for BML accounts
|
||||
)
|
||||
|
||||
data class MibBeneficiaryCategory(
|
||||
@@ -53,7 +54,8 @@ data class MibBeneficiary(
|
||||
val benefStatus: String,
|
||||
val transferCyDesc: String,
|
||||
val customerImgHash: String?,
|
||||
val benefCategoryId: String // "0" = uncategorized
|
||||
val benefCategoryId: String, // "0" = uncategorized
|
||||
val profileId: String = "" // MIB profile ID; empty for BML contacts
|
||||
)
|
||||
|
||||
data class MibIpsAccountInfo(
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import sh.sar.basedbank.util.ContactsCache
|
||||
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.bml.BmlAccountValidation
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.api.mib.MibProfile
|
||||
import sh.sar.basedbank.api.mib.MibTransferClient
|
||||
import sh.sar.basedbank.databinding.SheetAddContactBinding
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
|
||||
private var _binding: SheetAddContactBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val app get() = requireActivity().application as BasedBankApp
|
||||
|
||||
private data class DestinationOption(
|
||||
val label: String,
|
||||
val isBml: Boolean,
|
||||
val mibProfile: MibProfile? = null
|
||||
)
|
||||
|
||||
private var destinations: List<DestinationOption> = emptyList()
|
||||
private var selectedDest: DestinationOption? = null
|
||||
|
||||
// Holds the resolved lookup result (for BML dest) or null (MIB uses account number directly)
|
||||
private var bmlLookup: BmlAccountValidation? = null
|
||||
private var mibLookupAccount: String? = null // resolved account for MIB save
|
||||
|
||||
private var selectedImageBase64: String = ""
|
||||
private var selectedCategoryId: String = "0"
|
||||
private var categories: List<MibBeneficiaryCategory> = emptyList()
|
||||
|
||||
private val imagePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
uri ?: return@registerForActivityResult
|
||||
encodeImage(uri)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = SheetAddContactBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
destinations = buildDestinations()
|
||||
|
||||
setupDestinationDropdown()
|
||||
setupAccountSearch()
|
||||
setupImagePicker()
|
||||
setupSaveButton()
|
||||
|
||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||
categories = cats.filter { it.id != "BML" }
|
||||
if (selectedDest?.isBml == false) setupCategoryDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDestinations(): List<DestinationOption> {
|
||||
val list = mutableListOf<DestinationOption>()
|
||||
for (profile in app.mibProfiles) {
|
||||
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile))
|
||||
}
|
||||
if (app.bmlSession != null) {
|
||||
list.add(DestinationOption("BML · Personal", isBml = true))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
private fun setupDestinationDropdown() {
|
||||
val labels = destinations.map { it.label }
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, labels)
|
||||
binding.actvDestination.setAdapter(adapter)
|
||||
binding.actvDestination.setOnItemClickListener { _, _, position, _ ->
|
||||
selectedDest = destinations[position]
|
||||
clearLookupResult()
|
||||
updateMibOnlyVisibility()
|
||||
binding.btnSave.isEnabled = false
|
||||
}
|
||||
if (destinations.size == 1) {
|
||||
selectedDest = destinations[0]
|
||||
binding.actvDestination.setText(destinations[0].label, false)
|
||||
updateMibOnlyVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMibOnlyVisibility() {
|
||||
val isMib = selectedDest?.isBml == false
|
||||
binding.layoutImage.visibility = if (isMib) View.VISIBLE else View.GONE
|
||||
binding.tilGroup.visibility = if (isMib) View.VISIBLE else View.GONE
|
||||
if (isMib) setupCategoryDropdown()
|
||||
}
|
||||
|
||||
private fun setupCategoryDropdown() {
|
||||
val items = mutableListOf(getString(R.string.contact_no_group))
|
||||
items.addAll(categories.map { it.categoryName })
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, items)
|
||||
binding.actvGroup.setAdapter(adapter)
|
||||
binding.actvGroup.setOnItemClickListener { _, _, position, _ ->
|
||||
selectedCategoryId = if (position == 0) "0" else categories[position - 1].id
|
||||
}
|
||||
if (binding.actvGroup.text.isNullOrBlank()) {
|
||||
binding.actvGroup.setText(getString(R.string.contact_no_group), false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAccountSearch() {
|
||||
binding.tilAccount.setEndIconOnClickListener { performLookup() }
|
||||
}
|
||||
|
||||
private fun performLookup() {
|
||||
val dest = selectedDest ?: run {
|
||||
Toast.makeText(requireContext(), R.string.contact_select_destination, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
val input = binding.etAccount.text?.toString()?.trim() ?: ""
|
||||
if (input.isBlank()) {
|
||||
Toast.makeText(requireContext(), R.string.transfer_enter_account_first, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Check own accounts and existing contacts before any network call
|
||||
val isBmlDest = dest.isBml
|
||||
val isOwnAccountPre = viewModel.accounts.value?.any { acc ->
|
||||
acc.accountNumber == input && when {
|
||||
isBmlDest -> acc.loginTag.startsWith("bml_")
|
||||
else -> acc.profileId == (dest.mibProfile?.profileId ?: "")
|
||||
}
|
||||
} == true
|
||||
if (isOwnAccountPre) {
|
||||
Toast.makeText(requireContext(), R.string.contact_own_account, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
val existing = viewModel.contacts.value?.firstOrNull { contact ->
|
||||
contact.benefAccount == input && when {
|
||||
isBmlDest -> contact.benefCategoryId == "BML"
|
||||
else -> contact.profileId == (dest.mibProfile?.profileId ?: "")
|
||||
}
|
||||
}
|
||||
if (existing != null) {
|
||||
Toast.makeText(requireContext(), getString(R.string.contact_already_exists, existing.benefNickName), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
binding.tilAccount.isEnabled = false
|
||||
binding.tilDestination.isEnabled = false
|
||||
binding.btnSave.isEnabled = false
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
if (dest.isBml) lookupForBml(input) else lookupForMib(dest, input)
|
||||
}
|
||||
binding.tilAccount.isEnabled = true
|
||||
binding.tilDestination.isEnabled = true
|
||||
if (result != null) {
|
||||
showLookupResult(result, input)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.contact_lookup_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun lookupForBml(input: String): BmlAccountValidation? {
|
||||
val bmlSess = app.bmlSession ?: return null
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
|
||||
// 1) Try BML validate
|
||||
val validated = try { bmlFlow.validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
||||
if (validated != null) return validated
|
||||
|
||||
// 2) Try BML MIB verify
|
||||
val mibVerified = try { bmlFlow.verifyMibAccount(bmlSess, input) } catch (_: Exception) { null }
|
||||
if (mibVerified != null) return mibVerified
|
||||
|
||||
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
|
||||
val mibSess = app.mibSession ?: return null
|
||||
return try {
|
||||
val info = MibTransferClient().lookup(mibSess, input)
|
||||
BmlAccountValidation(
|
||||
trnType = "DOT",
|
||||
validationType = "MIB",
|
||||
account = info.accountNumber,
|
||||
originalInput = input,
|
||||
name = info.accountName,
|
||||
alias = null,
|
||||
currency = "MVR",
|
||||
agnt = "MADVMVMV"
|
||||
)
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
private fun lookupForMib(dest: DestinationOption, input: String): BmlAccountValidation? {
|
||||
val mibSess = app.mibSession ?: return null
|
||||
val profile = dest.mibProfile ?: return null
|
||||
|
||||
val mibResult = try {
|
||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
||||
val info = MibTransferClient().lookup(mibSess, input)
|
||||
BmlAccountValidation(
|
||||
trnType = if (info.bankId == "MADVMVMV") "MIB_INTERNAL" else "MIB_LOCAL",
|
||||
validationType = "MIB",
|
||||
account = info.accountNumber,
|
||||
originalInput = input,
|
||||
name = info.accountName,
|
||||
alias = null,
|
||||
currency = "MVR",
|
||||
agnt = info.bankId
|
||||
)
|
||||
} catch (_: Exception) { null }
|
||||
|
||||
if (mibResult != null) return mibResult
|
||||
|
||||
// MIB lookup failed (e.g. BML USD account) — fall back to BML validate
|
||||
val bmlSess = app.bmlSession ?: return null
|
||||
return try { BmlLoginFlow().validateAccount(bmlSess, input) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
private fun showLookupResult(validation: BmlAccountValidation, input: String) {
|
||||
// Re-check resolved account (input may have been a Favara ID)
|
||||
val isBmlDest = selectedDest?.isBml == true
|
||||
val isOwnAccountPost = viewModel.accounts.value?.any { acc ->
|
||||
acc.accountNumber == validation.account && when {
|
||||
isBmlDest -> acc.loginTag.startsWith("bml_")
|
||||
else -> acc.profileId == (selectedDest?.mibProfile?.profileId ?: "")
|
||||
}
|
||||
} == true
|
||||
if (isOwnAccountPost) {
|
||||
Toast.makeText(requireContext(), R.string.contact_own_account, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
val existing = viewModel.contacts.value?.firstOrNull { contact ->
|
||||
contact.benefAccount == validation.account && when {
|
||||
isBmlDest -> contact.benefCategoryId == "BML"
|
||||
else -> contact.profileId == (selectedDest?.mibProfile?.profileId ?: "")
|
||||
}
|
||||
}
|
||||
if (existing != null) {
|
||||
Toast.makeText(requireContext(), getString(R.string.contact_already_exists, existing.benefNickName), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
bmlLookup = validation
|
||||
mibLookupAccount = validation.account
|
||||
|
||||
val displayName = validation.name.ifBlank { input }
|
||||
val bankName = when {
|
||||
validation.account.matches(Regex("^9\\d{16}$")) -> "MIB"
|
||||
validation.account.matches(Regex("^7\\d{12}$")) -> "BML"
|
||||
else -> validation.account
|
||||
}
|
||||
val bankLabel = "$bankName · ${validation.account}"
|
||||
|
||||
binding.tvResultName.text = displayName
|
||||
binding.tvResultBank.text = bankLabel
|
||||
binding.ivResultAvatar.setImageBitmap(makeInitialsBitmap(displayName, "#607D8B"))
|
||||
|
||||
binding.cardResult.visibility = View.VISIBLE
|
||||
binding.tilAccount.visibility = View.GONE
|
||||
|
||||
// Auto-fill alias with existing alias or name
|
||||
if (binding.etAlias.text.isNullOrBlank()) {
|
||||
binding.etAlias.setText(validation.name)
|
||||
}
|
||||
binding.etCurrency.setText(validation.currency)
|
||||
|
||||
binding.btnClearResult.setOnClickListener { clearLookupResult() }
|
||||
binding.btnSave.isEnabled = true
|
||||
}
|
||||
|
||||
private fun clearLookupResult() {
|
||||
bmlLookup = null
|
||||
mibLookupAccount = null
|
||||
binding.cardResult.visibility = View.GONE
|
||||
binding.tilAccount.visibility = View.VISIBLE
|
||||
binding.btnSave.isEnabled = false
|
||||
}
|
||||
|
||||
private fun setupImagePicker() {
|
||||
binding.btnPickImage.setOnClickListener {
|
||||
imagePicker.launch("image/*")
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeImage(uri: Uri) {
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val stream = requireContext().contentResolver.openInputStream(uri) ?: return@launch
|
||||
var bmp = BitmapFactory.decodeStream(stream)
|
||||
stream.close()
|
||||
// Resize to max 256x256
|
||||
if (bmp.width > 256 || bmp.height > 256) {
|
||||
val scale = 256f / maxOf(bmp.width, bmp.height)
|
||||
bmp = Bitmap.createScaledBitmap(bmp, (bmp.width * scale).toInt(), (bmp.height * scale).toInt(), true)
|
||||
}
|
||||
val out = ByteArrayOutputStream()
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, 85, out)
|
||||
selectedImageBase64 = Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (_binding != null) binding.ivContactImage.setImageBitmap(bmp)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSaveButton() {
|
||||
binding.btnSave.setOnClickListener { performSave() }
|
||||
}
|
||||
|
||||
private fun performSave() {
|
||||
val dest = selectedDest ?: return
|
||||
val alias = binding.etAlias.text?.toString()?.trim() ?: ""
|
||||
if (alias.isBlank()) {
|
||||
binding.tilAlias.error = "Alias is required"
|
||||
return
|
||||
}
|
||||
binding.tilAlias.error = null
|
||||
|
||||
binding.btnSave.isEnabled = false
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
if (dest.isBml) saveToBml(alias) else saveToMib(alias)
|
||||
}
|
||||
if (success) {
|
||||
Toast.makeText(requireContext(), R.string.contact_saved, Toast.LENGTH_SHORT).show()
|
||||
reloadSavedProfileContacts(dest)
|
||||
dismiss()
|
||||
} else {
|
||||
binding.btnSave.isEnabled = true
|
||||
Toast.makeText(requireContext(), R.string.contact_save_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveToBml(alias: String): Boolean {
|
||||
val bmlSess = app.bmlSession ?: return false
|
||||
val lookup = bmlLookup ?: return false
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
val account = lookup.account
|
||||
return when {
|
||||
account.matches(Regex("^7\\d{12}$")) ->
|
||||
// BML account → IAT
|
||||
bmlFlow.saveContact(bmlSess, "IAT", account, alias)
|
||||
account.matches(Regex("^9\\d{16}$")) ->
|
||||
// MIB internal → DOT; swift is BML's internal UUID for MIB bank
|
||||
bmlFlow.saveContact(bmlSess, "DOT", account, alias,
|
||||
currency = lookup.currency, name = lookup.name, swift = MIB_SWIFT_ON_BML)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveToMib(alias: String): Boolean {
|
||||
val mibSess = app.mibSession ?: return false
|
||||
val dest = selectedDest ?: return false
|
||||
val profile = dest.mibProfile ?: return false
|
||||
val account = mibLookupAccount ?: return false
|
||||
val currency = binding.etCurrency.text?.toString()?.trim() ?: "MVR"
|
||||
val transferCy = if (currency.equals("USD", ignoreCase = true)) "840" else "462"
|
||||
|
||||
val isMibInternal = account.matches(Regex("^9\\d{16}$"))
|
||||
val benefType = if (isMibInternal) "I" else "L"
|
||||
val bankNo = if (isMibInternal) 2 else 3
|
||||
val name = bmlLookup?.name ?: ""
|
||||
|
||||
return try {
|
||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
||||
MibContactsClient().createContact(
|
||||
session = mibSess,
|
||||
benefType = benefType,
|
||||
bankNo = bankNo,
|
||||
benefAccount = account,
|
||||
benefName = name,
|
||||
nickName = alias,
|
||||
transferCy = transferCy,
|
||||
categoryId = selectedCategoryId,
|
||||
imageBase64 = selectedImageBase64
|
||||
)
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
private fun makeInitialsBitmap(name: String, colorHex: String): Bitmap {
|
||||
val sizePx = (resources.displayMetrics.density * 40).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
|
||||
}
|
||||
|
||||
private fun reloadSavedProfileContacts(dest: DestinationOption) {
|
||||
// Use activity scope so the reload continues after sheet dismisses
|
||||
requireActivity().lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (dest.isBml) {
|
||||
val bmlSess = app.bmlSession ?: return@launch
|
||||
val fresh = BmlLoginFlow().fetchContacts(bmlSess)
|
||||
val existing = viewModel.contacts.value ?: emptyList()
|
||||
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
|
||||
viewModel.contacts.postValue(merged)
|
||||
ContactsCache.saveBml(requireContext(), fresh)
|
||||
} else {
|
||||
val profile = dest.mibProfile ?: return@launch
|
||||
val mibSess = app.mibSession ?: return@launch
|
||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
||||
val fresh = MibContactsClient().fetchContacts(mibSess)
|
||||
.map { it.copy(profileId = profile.profileId) }
|
||||
val existing = viewModel.contacts.value ?: emptyList()
|
||||
val merged = existing.filter { it.profileId != profile.profileId } + fresh
|
||||
viewModel.contacts.postValue(merged)
|
||||
val allMib = merged.filter { it.benefCategoryId != "BML" }
|
||||
ContactsCache.save(requireContext(), allMib, viewModel.contactCategories.value ?: emptyList())
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
// BML's internal UUID for MIB bank — used as the "swift" field when saving DOT contacts
|
||||
private const val MIB_SWIFT_ON_BML = "F4E79935-3E73-E611-80DD-00155D020F0A"
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ class ContactsFragment : Fragment() {
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
|
||||
binding.fabAddContact.setOnClickListener {
|
||||
AddContactSheetFragment().show(childFragmentManager, "add_contact")
|
||||
}
|
||||
|
||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||
categories = cats
|
||||
rebuildTabs(cats)
|
||||
|
||||
@@ -272,7 +272,8 @@ class HomeActivity : AppCompatActivity() {
|
||||
if (seenCategories.add(cat.id)) categories.add(cat)
|
||||
}
|
||||
for (contact in contactsClient.fetchContacts(session)) {
|
||||
if (seenContacts.add(contact.benefNo)) contacts.add(contact)
|
||||
if (seenContacts.add(contact.benefNo))
|
||||
contacts.add(contact.copy(profileId = profile.profileId))
|
||||
}
|
||||
} catch (_: Exception) { /* profile has no contacts access */ }
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
||||
@@ -44,6 +45,7 @@ class TransferFragment : Fragment() {
|
||||
|
||||
private var selectedAccount: MibAccount? = null
|
||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||
private val bmlSession get() = (requireActivity().application as BasedBankApp).bmlSession
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
@@ -154,25 +156,55 @@ class TransferFragment : Fragment() {
|
||||
return
|
||||
}
|
||||
|
||||
val sess = session
|
||||
if (sess == null) {
|
||||
val mibSess = session
|
||||
val bmlSess = bmlSession
|
||||
if (mibSess == null && bmlSess == null) {
|
||||
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true
|
||||
|
||||
binding.tilTo.isEnabled = false
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
var errorMsg: String? = null
|
||||
val info = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
MibTransferClient().lookup(sess, accountNumber)
|
||||
} catch (e: MibLookupException) {
|
||||
errorMsg = e.message
|
||||
null
|
||||
} catch (_: Exception) {
|
||||
errorMsg = getString(R.string.transfer_account_not_found)
|
||||
null
|
||||
if (isBmlSource && bmlSess != null) {
|
||||
// BML source: prefer BML validate, fall back to MIB IPS
|
||||
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess, accountNumber) } catch (_: Exception) { null }
|
||||
if (bmlResult != null) {
|
||||
val bankId = when (bmlResult.trnType) {
|
||||
"IAT" -> "MALBMVMV"
|
||||
else -> bmlResult.agnt ?: bmlResult.account
|
||||
}
|
||||
MibIpsAccountInfo(accountName = bmlResult.name, accountNumber = bmlResult.account, bankId = bankId)
|
||||
} else if (mibSess != null) {
|
||||
try { MibTransferClient().lookup(mibSess, accountNumber) }
|
||||
catch (e: MibLookupException) { errorMsg = e.message; null }
|
||||
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
|
||||
} else {
|
||||
errorMsg = getString(R.string.transfer_account_not_found); null
|
||||
}
|
||||
} else {
|
||||
// MIB source (or no preference): prefer MIB, fall back to BML validate
|
||||
if (mibSess != null) {
|
||||
try { MibTransferClient().lookup(mibSess, accountNumber) }
|
||||
catch (e: MibLookupException) { errorMsg = e.message; null }
|
||||
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
|
||||
} else {
|
||||
// MIB not available, try BML
|
||||
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
|
||||
if (bmlResult != null) {
|
||||
val bankId = when (bmlResult.trnType) {
|
||||
"IAT" -> "MALBMVMV"
|
||||
else -> bmlResult.agnt ?: bmlResult.account
|
||||
}
|
||||
MibIpsAccountInfo(accountName = bmlResult.name, accountNumber = bmlResult.account, bankId = bankId)
|
||||
} else {
|
||||
errorMsg = getString(R.string.transfer_account_not_found); null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.tilTo.isEnabled = true
|
||||
|
||||
@@ -27,6 +27,7 @@ object AccountCache {
|
||||
put("mvrBalance", acc.mvrBalance)
|
||||
put("statusDesc", acc.statusDesc)
|
||||
put("loginTag", acc.loginTag)
|
||||
put("profileId", acc.profileId)
|
||||
if (acc.profileImageHash != null) put("profileImageHash", acc.profileImageHash)
|
||||
})
|
||||
}
|
||||
@@ -102,7 +103,8 @@ object AccountCache {
|
||||
mvrBalance = o.optString("mvrBalance"),
|
||||
statusDesc = o.optString("statusDesc"),
|
||||
profileImageHash = o.optString("profileImageHash").takeIf { it.isNotBlank() },
|
||||
loginTag = o.optString("loginTag")
|
||||
loginTag = o.optString("loginTag"),
|
||||
profileId = o.optString("profileId", "")
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -34,6 +34,7 @@ object ContactsCache {
|
||||
put("transferCyDesc", c.transferCyDesc)
|
||||
put("customerImgHash", c.customerImgHash ?: "")
|
||||
put("benefCategoryId", c.benefCategoryId)
|
||||
put("profileId", c.profileId)
|
||||
})
|
||||
}
|
||||
prefs.putString(KEY_CONTACTS, contactsArr.toString())
|
||||
@@ -69,7 +70,8 @@ object ContactsCache {
|
||||
benefStatus = o.optString("benefStatus"),
|
||||
transferCyDesc = o.optString("transferCyDesc", "MVR"),
|
||||
customerImgHash = o.optString("customerImgHash").takeIf { it.isNotBlank() },
|
||||
benefCategoryId = o.optString("benefCategoryId", "0")
|
||||
benefCategoryId = o.optString("benefCategoryId", "0"),
|
||||
profileId = o.optString("profileId", "")
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,71 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
app:startIconDrawable="@android:drawable/ic_menu_search"
|
||||
app:boxCornerRadiusTopStart="24dp"
|
||||
app:boxCornerRadiusTopEnd="24dp"
|
||||
app:boxCornerRadiusBottomStart="24dp"
|
||||
app:boxCornerRadiusBottomEnd="24dp">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSearch"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/contacts_search_hint"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionSearch" />
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
app:startIconDrawable="@android:drawable/ic_menu_search"
|
||||
app:boxCornerRadiusTopStart="24dp"
|
||||
app:boxCornerRadiusTopEnd="24dp"
|
||||
app:boxCornerRadiusBottomStart="24dp"
|
||||
app:boxCornerRadiusBottomEnd="24dp">
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/contacts_search_hint"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionSearch" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabMode="scrollable"
|
||||
app:tabGravity="start" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loadingView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="visible" />
|
||||
app:tabMode="scrollable"
|
||||
app:tabGravity="start" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/contacts_empty"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
</FrameLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingVertical="4dp"
|
||||
android:paddingBottom="80dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/loadingView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/contacts_empty"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabAddContact"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/contact_add"
|
||||
app:srcCompat="@android:drawable/ic_input_add" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
220
app/src/main/res/layout/sheet_add_contact.xml
Normal file
220
app/src/main/res/layout/sheet_add_contact.xml
Normal file
@@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
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="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/contact_add"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge" />
|
||||
|
||||
<!-- Save to (destination) -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilDestination"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/contact_save_to"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/actvDestination"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:focusable="false" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Account number / Favara ID -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilAccount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/transfer_to"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@android:drawable/ic_menu_search"
|
||||
app:endIconContentDescription="@string/transfer_lookup_account">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAccount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionSearch" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Lookup result card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardResult"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivResultAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvResultName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvResultBank"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnClearResult"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
android:contentDescription="@string/transfer_clear_recipient" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Alias -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilAlias"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/contact_alias"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAlias"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Currency (read-only) -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilCurrency"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/contact_currency"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etCurrency"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Image upload (MIB only) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivContactImage"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:importantForAccessibility="no"
|
||||
/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPickImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:text="@string/contact_image"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Group dropdown (MIB only) -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="@string/contact_group"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/actvGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:focusable="false" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Save button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/contact_save"
|
||||
android:enabled="false" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -123,6 +123,23 @@
|
||||
<string name="recents_remove">Remove from recents</string>
|
||||
<string name="contacts_tab_all">All</string>
|
||||
|
||||
<!-- Add Contact -->
|
||||
<string name="contact_add">Add Contact</string>
|
||||
<string name="contact_save_to">Save to account</string>
|
||||
<string name="contact_alias">Alias / Nickname</string>
|
||||
<string name="contact_currency">Currency</string>
|
||||
<string name="contact_group">Group</string>
|
||||
<string name="contact_no_group">No group</string>
|
||||
<string name="contact_save">Save Contact</string>
|
||||
<string name="contact_image">Upload Image</string>
|
||||
<string name="contact_saved">Contact saved</string>
|
||||
<string name="contact_save_failed">Failed to save contact</string>
|
||||
<string name="contact_no_session">No bank session available</string>
|
||||
<string name="contact_lookup_failed">Could not find account</string>
|
||||
<string name="contact_select_destination">Select a destination account first</string>
|
||||
<string name="contact_already_exists">Contact already exists: %s</string>
|
||||
<string name="contact_own_account">Cannot save your own account as a contact</string>
|
||||
|
||||
<!-- Financing -->
|
||||
<string name="financing_empty">No financing deals found</string>
|
||||
<string name="financing_total">Total</string>
|
||||
|
||||
Reference in New Issue
Block a user