delete and transfer contacts
This commit is contained in:
@@ -311,6 +311,22 @@ class BmlLoginFlow {
|
||||
return parseContacts(json)
|
||||
}
|
||||
|
||||
fun deleteContact(session: BmlSession, contactId: String): Boolean {
|
||||
val body = """{"_method":"delete"}""".toRequestBody("application/json".toMediaType())
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/api/mobile/contacts/$contactId")
|
||||
.post(body)
|
||||
.header("Authorization", "Bearer ${session.accessToken}")
|
||||
.header("User-Agent", APP_USER_AGENT)
|
||||
.header("x-app-version", APP_VERSION)
|
||||
.header("accept", "application/json")
|
||||
.build()
|
||||
return apiClient.newCall(request).execute().use { response ->
|
||||
val bodyStr = response.body?.string() ?: return@use false
|
||||
try { JSONObject(bodyStr).optBoolean("success") } catch (_: Exception) { false }
|
||||
}
|
||||
}
|
||||
|
||||
private fun apiRequest(session: BmlSession, url: String) =
|
||||
Request.Builder().url(url)
|
||||
.header("Authorization", "Bearer ${session.accessToken}")
|
||||
|
||||
@@ -182,6 +182,19 @@ class MibContactsClient {
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteContact(session: MibSession, benefNo: String): Boolean {
|
||||
val body = FormBody.Builder().add("benefNo", benefNo).build()
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_WV_URL/ajaxBeneficiary/deleteBeneficiary")
|
||||
.post(body)
|
||||
.withSessionHeaders(session)
|
||||
.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)
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||
import sh.sar.basedbank.databinding.ItemContactBinding
|
||||
|
||||
class ContactsAdapter(
|
||||
private val onImageNeeded: (hash: String) -> Unit
|
||||
private val onImageNeeded: (hash: String) -> Unit,
|
||||
private val onDeleteClick: (MibBeneficiary) -> Unit,
|
||||
private val onTransferClick: (MibBeneficiary) -> Unit
|
||||
) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
|
||||
|
||||
private var allContacts: List<MibBeneficiary> = emptyList()
|
||||
private var displayed: List<MibBeneficiary> = emptyList()
|
||||
private val imageCache = mutableMapOf<String, Bitmap>()
|
||||
private val expandedPositions = mutableSetOf<Int>()
|
||||
|
||||
private var activeCategoryId: String? = null
|
||||
private var searchQuery: String = ""
|
||||
|
||||
fun updateContacts(contacts: List<MibBeneficiary>) {
|
||||
allContacts = contacts
|
||||
expandedPositions.clear()
|
||||
applyFilter()
|
||||
}
|
||||
|
||||
@@ -36,6 +46,7 @@ class ContactsAdapter(
|
||||
fun setFilter(categoryId: String?, query: String) {
|
||||
activeCategoryId = categoryId
|
||||
searchQuery = query
|
||||
expandedPositions.clear()
|
||||
applyFilter()
|
||||
}
|
||||
|
||||
@@ -61,15 +72,42 @@ class ContactsAdapter(
|
||||
val cachedImage = contact.customerImgHash?.let { hash ->
|
||||
imageCache[hash] ?: run { onImageNeeded(hash); null }
|
||||
}
|
||||
holder.bind(contact, cachedImage)
|
||||
holder.bind(contact, cachedImage, position in expandedPositions)
|
||||
|
||||
holder.binding.root.setOnClickListener {
|
||||
val pos = holder.bindingAdapterPosition
|
||||
if (pos == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||
if (pos in expandedPositions) expandedPositions.remove(pos) else expandedPositions.add(pos)
|
||||
notifyItemChanged(pos)
|
||||
}
|
||||
|
||||
holder.binding.root.setOnLongClickListener {
|
||||
val ctx = it.context
|
||||
val clipboard = ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("account", contact.benefAccount))
|
||||
Toast.makeText(ctx, contact.benefAccount, Toast.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
holder.binding.btnTransferContact.setOnClickListener {
|
||||
onTransferClick(contact)
|
||||
}
|
||||
|
||||
holder.binding.btnEditContact.setOnClickListener {
|
||||
Toast.makeText(it.context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
holder.binding.btnDeleteContact.setOnClickListener {
|
||||
onDeleteClick(contact)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = displayed.size
|
||||
|
||||
inner class ViewHolder(private val binding: ItemContactBinding) :
|
||||
inner class ViewHolder(val binding: ItemContactBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(contact: MibBeneficiary, photo: Bitmap?) {
|
||||
fun bind(contact: MibBeneficiary, photo: Bitmap?, expanded: Boolean) {
|
||||
binding.tvContactName.text = contact.benefNickName
|
||||
binding.tvContactBank.text = contact.benefBankName
|
||||
binding.tvContactAccount.text = "${contact.benefAccount} · ${contact.transferCyDesc}"
|
||||
@@ -81,6 +119,10 @@ class ContactsAdapter(
|
||||
makeInitialsBitmap(contact.benefNickName, contact.bankColor)
|
||||
)
|
||||
}
|
||||
|
||||
val vis = if (expanded) View.VISIBLE else View.GONE
|
||||
binding.dividerExpand.visibility = vis
|
||||
binding.expandedSection.visibility = vis
|
||||
}
|
||||
|
||||
private fun makeInitialsBitmap(name: String, colorHex: String): Bitmap {
|
||||
@@ -88,22 +130,17 @@ class ContactsAdapter(
|
||||
.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
.coerceAtLeast(96)
|
||||
val bgColor = try { Color.parseColor(colorHex) } catch (e: 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
|
||||
val textY = sizePx / 2f - (metrics.ascent + metrics.descent) / 2f
|
||||
canvas.drawText(letter, sizePx / 2f, textY, paint)
|
||||
|
||||
canvas.drawText(letter, sizePx / 2f, sizePx / 2f - (metrics.ascent + metrics.descent) / 2f, paint)
|
||||
return bm
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -17,9 +19,12 @@ 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.MibBeneficiary
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.databinding.FragmentContactsBinding
|
||||
import sh.sar.basedbank.util.ContactsCache
|
||||
|
||||
class ContactsFragment : Fragment() {
|
||||
|
||||
@@ -29,10 +34,11 @@ class ContactsFragment : Fragment() {
|
||||
private lateinit var adapter: ContactsAdapter
|
||||
|
||||
private val pendingHashes = mutableSetOf<String>()
|
||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||
private val app get() = requireActivity().application as BasedBankApp
|
||||
private val session get() = app.mibSession
|
||||
|
||||
private var categories: List<MibBeneficiaryCategory> = emptyList()
|
||||
private var activeCategoryId: String? = null // null = All
|
||||
private var activeCategoryId: String? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
||||
@@ -40,7 +46,20 @@ class ContactsFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
adapter = ContactsAdapter { hash -> fetchImage(hash) }
|
||||
adapter = ContactsAdapter(
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onDeleteClick = { contact -> confirmDelete(contact) },
|
||||
onTransferClick = { contact ->
|
||||
val fragment = TransferFragment.newInstance(
|
||||
accountNumber = contact.benefAccount,
|
||||
displayName = contact.benefNickName,
|
||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||
colorHex = contact.bankColor,
|
||||
imageHash = contact.customerImgHash
|
||||
)
|
||||
(requireActivity() as HomeActivity).showWithBackStack(fragment)
|
||||
}
|
||||
)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
@@ -97,6 +116,60 @@ class ContactsFragment : Fragment() {
|
||||
})
|
||||
}
|
||||
|
||||
private fun confirmDelete(contact: MibBeneficiary) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.contact_delete_title)
|
||||
.setMessage(getString(R.string.contact_delete_message, contact.benefNickName))
|
||||
.setPositiveButton(R.string.contact_delete) { _, _ -> deleteContact(contact) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteContact(contact: MibBeneficiary) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
if (contact.benefCategoryId == "BML") deleteBml(contact) else deleteMib(contact)
|
||||
}
|
||||
if (success) {
|
||||
Toast.makeText(requireContext(), R.string.contact_deleted, Toast.LENGTH_SHORT).show()
|
||||
removeFromViewModel(contact)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.contact_delete_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteBml(contact: MibBeneficiary): Boolean {
|
||||
val sess = app.bmlSession ?: return false
|
||||
val contactId = contact.benefNo.removePrefix("bml_")
|
||||
return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
private fun deleteMib(contact: MibBeneficiary): Boolean {
|
||||
val sess = session ?: return false
|
||||
return try {
|
||||
if (contact.profileId.isNotBlank()) {
|
||||
val profile = app.mibProfiles.firstOrNull { it.profileId == contact.profileId }
|
||||
if (profile != null) app.mibLoginFlow.switchProfile(sess, profile)
|
||||
}
|
||||
MibContactsClient().deleteContact(sess, contact.benefNo)
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
private fun removeFromViewModel(contact: MibBeneficiary) {
|
||||
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.benefNo } ?: return
|
||||
viewModel.contacts.value = updated
|
||||
if (contact.benefCategoryId == "BML") {
|
||||
ContactsCache.saveBml(requireContext(), updated.filter { it.benefCategoryId == "BML" })
|
||||
} else {
|
||||
ContactsCache.save(
|
||||
requireContext(),
|
||||
updated.filter { it.benefCategoryId != "BML" },
|
||||
viewModel.contactCategories.value ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchImage(hash: String) {
|
||||
if (!pendingHashes.add(hash)) return
|
||||
val sess = session ?: return
|
||||
@@ -106,11 +179,9 @@ class ContactsFragment : Fragment() {
|
||||
val base64 = client.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) {
|
||||
adapter.updateImage(hash, bitmap)
|
||||
}
|
||||
withContext(Dispatchers.Main) { adapter.updateImage(hash, bitmap) }
|
||||
} catch (_: Exception) {
|
||||
pendingHashes.remove(hash) // allow retry
|
||||
pendingHashes.remove(hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,30 @@ class TransferFragment : Fragment() {
|
||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||
private val bmlSession get() = (requireActivity().application as BasedBankApp).bmlSession
|
||||
|
||||
companion object {
|
||||
private const val ARG_ACCOUNT = "contact_account"
|
||||
private const val ARG_NAME = "contact_name"
|
||||
private const val ARG_SUBTITLE = "contact_subtitle"
|
||||
private const val ARG_COLOR = "contact_color"
|
||||
private const val ARG_IMAGE_HASH = "contact_image_hash"
|
||||
|
||||
fun newInstance(
|
||||
accountNumber: String,
|
||||
displayName: String,
|
||||
subtitle: String,
|
||||
colorHex: String,
|
||||
imageHash: String?
|
||||
) = TransferFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_ACCOUNT, accountNumber)
|
||||
putString(ARG_NAME, displayName)
|
||||
putString(ARG_SUBTITLE, subtitle)
|
||||
putString(ARG_COLOR, colorHex)
|
||||
if (imageHash != null) putString(ARG_IMAGE_HASH, imageHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
@@ -94,6 +118,17 @@ class TransferFragment : Fragment() {
|
||||
binding.btnTransfer.setOnClickListener {
|
||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
// Pre-select contact if navigated from contacts page
|
||||
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
||||
prefillToDirectly(
|
||||
accountNumber = account,
|
||||
displayName = arguments?.getString(ARG_NAME) ?: account,
|
||||
subtitle = arguments?.getString(ARG_SUBTITLE) ?: account,
|
||||
colorHex = arguments?.getString(ARG_COLOR) ?: "#607D8B",
|
||||
imageHash = arguments?.getString(ARG_IMAGE_HASH)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFromDropdown() {
|
||||
|
||||
Reference in New Issue
Block a user