add support for fahipay login and view history
Auto Tag on Version Change / check-version (push) Successful in 2s

This commit is contained in:
2026-05-16 21:31:34 +05:00
parent 99a32dc9ed
commit 7864655a82
29 changed files with 2097 additions and 32 deletions
@@ -139,7 +139,11 @@ class AccountHistoryAdapter(
fun bind(acc: MibAccount) {
b.tvHeaderAccountName.text = acc.accountBriefName
b.tvHeaderAccountNumber.text = acc.accountNumber
b.tvHeaderPillBank.text = if (acc.profileType.startsWith("BML")) "BML" else "MIB"
b.tvHeaderPillBank.text = when {
acc.profileType.startsWith("BML") -> "BML"
acc.profileType == "FAHIPAY" -> "FP"
else -> null
}
b.tvHeaderPillType.text = friendlyType(acc.accountTypeName)
b.tvHeaderAvailable.text = "${acc.currencyName} ${acc.availableBalance}"
b.tvHeaderBalance.text = "${acc.currencyName} ${acc.currentBalance}"
@@ -19,6 +19,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibHistoryClient
@@ -50,6 +51,8 @@ class AccountHistoryFragment : Fragment() {
private var bmlNextPage = 1
private var bmlTotalPages = -1
private var cardMonthOffset = 0 // 0 = current month, 1 = prev, etc.
private var fahipayNextStart = 0
private var fahipayTotal = -1
private var isLoading = false
private val pageSize = 10
@@ -121,10 +124,12 @@ class AccountHistoryFragment : Fragment() {
binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE
}
private fun isMib() = !account.profileType.startsWith("BML")
private fun isMib() = !account.profileType.startsWith("BML") && account.profileType != "FAHIPAY"
private fun isBmlCard() = account.profileType == "BML_PREPAID"
private fun isFahipay() = account.profileType == "FAHIPAY"
private fun hasMore(): Boolean = when {
isFahipay() -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
isMib() -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
isBmlCard() -> cardMonthOffset < 3 // load up to 3 months
else -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
@@ -143,6 +148,20 @@ class AccountHistoryFragment : Fragment() {
lifecycleScope.launch {
val transactions: List<Transaction> = withContext(Dispatchers.IO) {
when {
isFahipay() -> {
val session = app.fahipaySession ?: return@withContext emptyList()
val flow = FahipayLoginFlow()
flow.setSessionCookie(session.sessionCookie)
val (list, total) = flow.fetchHistory(
session = session,
accountDisplayName = account.accountBriefName,
accountNumber = account.accountNumber,
start = fahipayNextStart
)
if (total > 0) fahipayTotal = total
fahipayNextStart += list.size
list
}
isMib() -> {
val session = app.mibSession ?: return@withContext emptyList()
app.mibMutex.withLock {
@@ -91,7 +91,11 @@ class AccountsAdapter(
fun bind(account: MibAccount) {
binding.tvAccountName.text = account.accountBriefName
binding.tvAccountNumber.text = account.accountNumber
binding.tvPillBank.text = if (account.profileType.startsWith("BML")) "BML" else "MIB"
binding.tvPillBank.text = when {
account.profileType.startsWith("BML") -> "BML"
account.profileType == "FAHIPAY" -> "FP"
else -> null
}
binding.tvPillType.text = friendlyAccountType(account.accountTypeName)
binding.tvPillProfile.text = when (account.profileType) {
"0" -> "Personal"
@@ -31,6 +31,8 @@ import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.AuthExpiredException
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.bml.BmlSession
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
import sh.sar.basedbank.api.fahipay.FahipaySession
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding
@@ -104,13 +106,14 @@ class HomeActivity : AppCompatActivity() {
// Load data
val app = application as BasedBankApp
if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty()) {
if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
// Came from fresh manual login — accounts ready, rest fetched in background
val mibAccounts = app.accounts.filter { it.profileType != "BML" }
val merged = mibAccounts + app.bmlAccounts
val mibAccounts = app.accounts.filter { !it.profileType.startsWith("BML") && it.profileType != "FAHIPAY" }
val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts
viewModel.accounts.value = merged
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
if (app.bmlAccounts.isNotEmpty()) AccountCache.saveBml(this, app.bmlAccounts)
if (app.fahipayAccounts.isNotEmpty()) AccountCache.saveFahipay(this, app.fahipayAccounts)
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
@@ -131,7 +134,8 @@ class HomeActivity : AppCompatActivity() {
// Came from lock screen — show caches immediately, refresh everything in background
val cachedMib = AccountCache.load(this)
val cachedBml = AccountCache.loadBml(this)
val merged = cachedMib + cachedBml
val cachedFahipay = AccountCache.loadFahipay(this)
val merged = cachedMib + cachedBml + cachedFahipay
if (merged.isNotEmpty()) viewModel.accounts.value = merged
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
@@ -143,7 +147,7 @@ class HomeActivity : AppCompatActivity() {
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
val store = CredentialStore(this)
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store)
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store.loadFahipayCredentials(), store)
}
// Show dashboard on first create
@@ -289,7 +293,8 @@ class HomeActivity : AppCompatActivity() {
val store = CredentialStore(this)
val hasMib = store.hasMibCredentials()
val hasBml = store.hasBmlCredentials()
if (!hasMib && !hasBml) {
val hasFahipay = store.hasFahipayCredentials()
if (!hasMib && !hasBml && !hasFahipay) {
startActivity(Intent(this, LoginActivity::class.java))
finish()
return
@@ -297,19 +302,21 @@ class HomeActivity : AppCompatActivity() {
// Immediately drop accounts for logged-out banks from the displayed list
val current = viewModel.accounts.value ?: emptyList()
viewModel.accounts.value = current.filter { acc ->
if (!hasMib && !acc.profileType.startsWith("BML")) return@filter false
if (!hasMib && !acc.profileType.startsWith("BML") && acc.profileType != "FAHIPAY") return@filter false
if (!hasBml && acc.profileType.startsWith("BML")) return@filter false
if (!hasFahipay && acc.profileType == "FAHIPAY") return@filter false
true
}
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store)
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store.loadFahipayCredentials(), store)
}
private fun autoRefresh(
mibCreds: CredentialStore.MibCredentials?,
bmlCreds: CredentialStore.BmlCredentials?,
fahipayCreds: CredentialStore.FahipayCredentials?,
store: CredentialStore
) {
if (mibCreds == null && bmlCreds == null) return
if (mibCreds == null && bmlCreds == null && fahipayCreds == null) return
binding.refreshIndicator.visibility = View.VISIBLE
lifecycleScope.launch {
@@ -366,10 +373,62 @@ class HomeActivity : AppCompatActivity() {
}
}
val fahipayJob = fahipayCreds?.let { creds ->
async(Dispatchers.IO) {
val fahipayFlow = FahipayLoginFlow()
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
// Try cached session first
val savedSession = store.loadFahipaySession()
if (savedSession != null) {
try {
val session = FahipaySession(savedSession.first, savedSession.second)
fahipayFlow.setSessionCookie(session.sessionCookie)
val balance = fahipayFlow.fetchBalance(session)
val profile = fahipayFlow.fetchProfile(session)
val loginTag = "fahipay_${profile.profileId}"
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
val app = application as BasedBankApp
app.fahipaySession = session
app.fahipayAccounts = accounts
AccountCache.saveFahipay(this@HomeActivity, accounts)
return@async Pair(session, accounts)
} catch (_: Exception) {
// Session expired — fall through to full login
}
}
// Full re-login (only works if user has no 2FA, or 2FA was skipped)
try {
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
if (step.twoFactorRequired) {
// Can't auto-complete 2FA — use cached data
return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
}
val authId = step.authId ?: return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
val session = FahipaySession(authId, cookieValue)
store.saveFahipaySession(authId, cookieValue)
val profile = fahipayFlow.fetchProfile(session)
val balance = fahipayFlow.fetchBalance(session)
val loginTag = "fahipay_${profile.profileId}"
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
val app = application as BasedBankApp
app.fahipaySession = session
app.fahipayAccounts = accounts
AccountCache.saveFahipay(this@HomeActivity, accounts)
Pair(session, accounts)
} catch (_: Exception) {
Pair(null, AccountCache.loadFahipay(this@HomeActivity))
}
}
}
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
val (bmlSession, bmlAccounts) = bmlJob?.await() ?: Pair(null, AccountCache.loadBml(this@HomeActivity))
val (_, fahipayAccounts) = fahipayJob?.await() ?: Pair(null, AccountCache.loadFahipay(this@HomeActivity))
viewModel.accounts.postValue(mibAccounts + bmlAccounts)
viewModel.accounts.postValue(mibAccounts + bmlAccounts + fahipayAccounts)
binding.refreshIndicator.visibility = View.GONE
val app = application as BasedBankApp
@@ -464,7 +523,24 @@ class HomeActivity : AppCompatActivity() {
val app = application as BasedBankApp
lifecycleScope.launch {
val current = viewModel.accounts.value ?: emptyList()
if (src.profileType.startsWith("BML")) {
if (src.profileType == "FAHIPAY") {
val fresh = withContext(Dispatchers.IO) {
val sess = app.fahipaySession ?: return@withContext null
try {
val flow = FahipayLoginFlow()
flow.setSessionCookie(sess.sessionCookie)
val balance = flow.fetchBalance(sess)
val profile = flow.fetchProfile(sess)
val loginTag = "fahipay_${profile.profileId}"
val accounts = listOf(flow.buildAccount(profile, balance, loginTag))
AccountCache.saveFahipay(this@HomeActivity, accounts)
app.fahipayAccounts = accounts
accounts
} catch (_: Exception) { null }
} ?: return@launch
val others = current.filter { it.profileType != "FAHIPAY" }
viewModel.accounts.postValue(others + fresh)
} else if (src.profileType.startsWith("BML")) {
val fresh = withContext(Dispatchers.IO) {
val sess = app.bmlSession ?: return@withContext null
try {
@@ -163,15 +163,16 @@ class SettingsFragment : Fragment() {
val hasMib = store.hasMibCredentials()
val hasBml = store.hasBmlCredentials()
val hasFahipay = store.hasFahipayCredentials()
binding.tvLoginsTitle.visibility = if (hasMib || hasBml) View.VISIBLE else View.GONE
binding.tvLoginsTitle.visibility = if (hasMib || hasBml || hasFahipay) View.VISIBLE else View.GONE
if (hasMib) {
val profile = store.loadMibUserProfile()
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name)
val profileNames = AccountCache.load(ctx)
.map { it.profileName }.filter { it.isNotBlank() }.distinct()
addLoginRow(container, R.drawable.mib_faisanet_logo, displayName) {
addLoginRow(container, R.drawable.mib_logo, displayName) {
showLoginDetails(
title = getString(R.string.mib_name),
details = buildString {
@@ -213,6 +214,23 @@ class SettingsFragment : Fragment() {
)
}
}
if (hasFahipay) {
val profile = store.loadFahipayUserProfile()
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
showLoginDetails(
title = getString(R.string.fahipay_name),
details = buildString {
if (!profile?.fullName.isNullOrBlank()) appendLine("${getString(R.string.login_detail_name)}: ${profile!!.fullName}")
if (!profile?.email.isNullOrBlank()) appendLine("${getString(R.string.login_detail_email)}: ${profile!!.email}")
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}")
}.trim(),
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store) } }
)
}
}
}
private fun addLoginRow(
@@ -298,6 +316,18 @@ class SettingsFragment : Fragment() {
buildLoginsSection()
}
private fun logoutFahipay(store: CredentialStore) {
val ctx = requireContext()
store.clearFahipayCredentials()
store.clearFahipaySession()
val app = requireActivity().application as BasedBankApp
app.fahipaySession = null
app.fahipayAccounts = emptyList()
clearAllCaches(ctx)
(activity as HomeActivity).relogin()
buildLoginsSection()
}
private fun applyFlagSecure(enabled: Boolean) {
val win = activity?.window ?: return
if (enabled) {
@@ -167,8 +167,16 @@ class TransferFragment : Fragment() {
private fun showFromCard(account: MibAccount) {
val isBml = account.profileType.startsWith("BML")
val colorHex = if (isBml) "#0066A1" else "#FE860E"
val bankLabel = if (isBml) "BML" else "MIB"
val colorHex = when {
isBml -> "#0066A1"
account.profileType == "FAHIPAY" -> "#15BEA7"
else -> "#FE860E"
}
val bankLabel = when {
isBml -> "BML"
account.profileType == "FAHIPAY" -> "FP"
else -> null
}
val typeLabel = when {
account.profileType == "BML_PREPAID" -> "Prepaid Card"
account.accountTypeName.isNotBlank() -> account.accountTypeName
@@ -177,7 +185,7 @@ class TransferFragment : Fragment() {
binding.tvFromAccountName.text = account.accountBriefName
binding.tvFromAccountNumber.text = account.accountNumber
binding.tvFromAccountDetails.text = "$bankLabel · $typeLabel · ${account.currencyName} ${account.availableBalance}"
binding.tvFromAccountDetails.text = listOfNotNull(bankLabel, typeLabel, "${account.currencyName} ${account.availableBalance}").joinToString(" · ")
binding.ivFromPhoto.setImageBitmap(makeInitialsBitmap(account.accountBriefName, colorHex))
binding.tilFrom.visibility = View.GONE
binding.cardFromInfo.visibility = View.VISIBLE
@@ -27,6 +27,10 @@ class BankSelectionFragment : Fragment() {
val args = android.os.Bundle().apply { putString("bankType", "BML") }
findNavController().navigate(R.id.action_bankSelection_to_credentials_bml, args)
}
binding.cardFahipay.setOnClickListener {
val args = android.os.Bundle().apply { putString("bankType", "FAHIPAY") }
findNavController().navigate(R.id.action_bankSelection_to_credentials_fahipay, args)
}
}
override fun onDestroyView() {
@@ -19,6 +19,8 @@ import sh.sar.basedbank.util.Totp
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
import sh.sar.basedbank.api.fahipay.FahipaySession
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.CredentialStore
@@ -40,29 +42,45 @@ class CredentialsFragment : Fragment() {
private var _binding: FragmentCredentialsBinding? = null
private val binding get() = _binding!!
// Fahipay two-step state
private var fahipayFlow: FahipayLoginFlow? = null
private var fahipayAwaitingTotp = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCredentialsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (bankType == "BML") {
binding.ivBankLogo.setImageResource(R.drawable.bml_logo_vector)
binding.tvSignInDesc.setText(R.string.bml_sign_in_desc)
when (bankType) {
"BML" -> {
binding.ivBankLogo.setImageResource(R.drawable.bml_logo_vector)
binding.tvSignInDesc.setText(R.string.bml_sign_in_desc)
}
"FAHIPAY" -> {
binding.ivBankLogo.setImageResource(R.drawable.fahipay_logo_long)
binding.tvSignInDesc.setText(R.string.fahipay_sign_in_desc)
binding.tilUsername.hint = getString(R.string.fahipay_id_card)
binding.tilOtpSeed.visibility = android.view.View.GONE
binding.etOtpSeed.isEnabled = false
binding.etOtpSeed.isFocusable = false
}
}
binding.btnLogin.setOnClickListener { attemptLogin() }
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { updateOtpDisplay() }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
if (bankType != "FAHIPAY") {
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { updateOtpDisplay() }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
}
override fun onResume() {
super.onResume()
otpHandler.post(otpRunnable)
if (bankType != "FAHIPAY") otpHandler.post(otpRunnable)
}
override fun onPause() {
@@ -91,9 +109,9 @@ class CredentialsFragment : Fragment() {
}
private fun attemptLogin() {
if (bankType == "BML") {
attemptBmlLogin()
return
when (bankType) {
"BML" -> { attemptBmlLogin(); return }
"FAHIPAY" -> { attemptFahipayLogin(); return }
}
val username = binding.etUsername.text.toString().trim()
@@ -208,6 +226,136 @@ class CredentialsFragment : Fragment() {
}
}
private fun attemptFahipayLogin() {
if (fahipayAwaitingTotp) {
submitFahipayTotp()
return
}
val idCard = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString()
if (idCard.isEmpty() || password.isEmpty()) {
binding.tvError.text = "Please fill in all fields"
binding.tvError.visibility = View.VISIBLE
return
}
binding.tvError.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
binding.btnLogin.isEnabled = false
val store = CredentialStore(requireContext())
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
val flow = FahipayLoginFlow().also { fahipayFlow = it }
viewLifecycleOwner.lifecycleScope.launch {
try {
val step = withContext(Dispatchers.IO) {
flow.login(idCard, password, deviceUuid)
}
if (step.twoFactorRequired) {
// Show TOTP input, disable ID + password fields
fahipayAwaitingTotp = true
binding.etUsername.isEnabled = false
binding.etPassword.isEnabled = false
binding.tilTotpCode.visibility = View.VISIBLE
binding.btnLogin.text = getString(R.string.fahipay_verify)
binding.tvError.visibility = View.GONE
} else {
// No 2FA — finish login with the authId from the login response
val authId = step.authId ?: throw Exception("No authID received")
val cookieValue = flow.getSessionCookieValue() ?: ""
finishFahipayLogin(flow, FahipaySession(authId, cookieValue), idCard, password, store)
}
} catch (e: Exception) {
binding.tvError.text = e.message ?: "Login failed"
binding.tvError.visibility = View.VISIBLE
} finally {
binding.progressBar.visibility = View.GONE
binding.btnLogin.isEnabled = true
}
}
}
private fun submitFahipayTotp() {
val code = binding.etTotpCode.text.toString().trim()
if (code.length != 6) {
binding.tvError.text = "Enter the 6-digit code from your authenticator app"
binding.tvError.visibility = View.VISIBLE
return
}
binding.tvError.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
binding.btnLogin.isEnabled = false
val flow = fahipayFlow ?: run {
binding.tvError.text = "Session lost — please restart login"
binding.tvError.visibility = View.VISIBLE
binding.progressBar.visibility = View.GONE
binding.btnLogin.isEnabled = true
return
}
val store = CredentialStore(requireContext())
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
val idCard = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString()
viewLifecycleOwner.lifecycleScope.launch {
try {
val authId = withContext(Dispatchers.IO) { flow.verifyTotp(code, deviceUuid) }
val cookieValue = flow.getSessionCookieValue() ?: ""
finishFahipayLogin(flow, FahipaySession(authId, cookieValue), idCard, password, store)
} catch (e: Exception) {
binding.tvError.text = e.message ?: "OTP verification failed"
binding.tvError.visibility = View.VISIBLE
} finally {
binding.progressBar.visibility = View.GONE
binding.btnLogin.isEnabled = true
}
}
}
private suspend fun finishFahipayLogin(
flow: FahipayLoginFlow,
session: FahipaySession,
idCard: String,
password: String,
store: CredentialStore
) {
val (profile, balance) = withContext(Dispatchers.IO) {
val p = flow.fetchProfile(session)
val b = flow.fetchBalance(session)
Pair(p, b)
}
val loginTag = "fahipay_${profile.profileId}"
val account = flow.buildAccount(profile, balance, loginTag)
store.saveFahipayCredentials(idCard, password)
store.saveFahipaySession(session.authId, session.sessionCookie)
store.saveFahipayUserProfile(
CredentialStore.FahipayUserProfile(
fullName = profile.fullName,
email = profile.email,
mobile = profile.mobile,
nid = profile.nid,
profileId = profile.profileId,
walletAccount = profile.walletAccount,
linkedAccounts = profile.linkedAccounts
)
)
AccountCache.saveFahipay(requireContext(), listOf(account))
val app = requireActivity().application as BasedBankApp
app.fahipaySession = session
app.fahipayAccounts = listOf(account)
app.accounts = app.accounts + listOf(account)
val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null