working mib login and list accounts

This commit is contained in:
2026-05-14 05:24:11 +05:00
parent 31c8a5500d
commit 076a58359a
73 changed files with 3076 additions and 550 deletions
@@ -1,42 +0,0 @@
package sh.sar.basedbank.ui.gallery
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import sh.sar.basedbank.databinding.FragmentGalleryBinding
class GalleryFragment : Fragment() {
private var _binding: FragmentGalleryBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val galleryViewModel =
ViewModelProvider(this).get(GalleryViewModel::class.java)
_binding = FragmentGalleryBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textGallery
galleryViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -1,13 +0,0 @@
package sh.sar.basedbank.ui.gallery
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class GalleryViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is gallery Fragment"
}
val text: LiveData<String> = _text
}
@@ -0,0 +1,75 @@
package sh.sar.basedbank.ui.home
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.databinding.ItemAccountBinding
import sh.sar.basedbank.databinding.ItemProfileHeaderBinding
class AccountsAdapter(private val accounts: List<MibAccount>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private sealed class Item {
data class Header(val profileName: String, val profileType: String) : Item()
data class Account(val account: MibAccount) : Item()
}
private val items: List<Item> = buildList {
var lastProfile = ""
for (account in accounts) {
if (account.profileName != lastProfile) {
add(Item.Header(account.profileName, account.profileType))
lastProfile = account.profileName
}
add(Item.Account(account))
}
}
override fun getItemViewType(position: Int) = when (items[position]) {
is Item.Header -> TYPE_HEADER
is Item.Account -> TYPE_ACCOUNT
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return if (viewType == TYPE_HEADER) {
HeaderViewHolder(ItemProfileHeaderBinding.inflate(inflater, parent, false))
} else {
AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = items[position]) {
is Item.Header -> (holder as HeaderViewHolder).bind(item)
is Item.Account -> (holder as AccountViewHolder).bind(item.account)
}
}
override fun getItemCount() = items.size
private inner class HeaderViewHolder(private val binding: ItemProfileHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item.Header) {
binding.tvProfileName.text = item.profileName
binding.tvProfileType.text = if (item.profileType == "0") "Personal" else "Business"
}
}
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: MibAccount) {
binding.tvAccountName.text = account.accountBriefName
binding.tvAccountNumber.text = account.accountNumber
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
binding.tvAccountType.text = account.accountTypeName
binding.tvStatus.text = account.statusDesc
}
}
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ACCOUNT = 1
}
}
@@ -0,0 +1,26 @@
package sh.sar.basedbank.ui.home
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.databinding.ActivityHomeBinding
class HomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityHomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val accounts = (application as BasedBankApp).accounts
val adapter = AccountsAdapter(accounts)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
}
}
@@ -1,42 +0,0 @@
package sh.sar.basedbank.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import sh.sar.basedbank.databinding.FragmentHomeBinding
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textHome
homeViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -1,13 +0,0 @@
package sh.sar.basedbank.ui.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class HomeViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is home Fragment"
}
val text: LiveData<String> = _text
}
@@ -0,0 +1,32 @@
package sh.sar.basedbank.ui.login
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentBankSelectionBinding
class BankSelectionFragment : Fragment() {
private var _binding: FragmentBankSelectionBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentBankSelectionBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.cardMib.setOnClickListener {
findNavController().navigate(R.id.action_bankSelection_to_credentials)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,129 @@
package sh.sar.basedbank.ui.login
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.util.Totp
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
import sh.sar.basedbank.ui.home.HomeActivity
class CredentialsFragment : Fragment() {
private val TAG = "CredentialsFragment"
private val otpHandler = Handler(Looper.getMainLooper())
private val otpRunnable = object : Runnable {
override fun run() {
updateOtpDisplay()
otpHandler.postDelayed(this, 1000)
}
}
private var _binding: FragmentCredentialsBinding? = null
private val binding get() = _binding!!
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?) {
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) {}
})
}
override fun onResume() {
super.onResume()
otpHandler.post(otpRunnable)
}
override fun onPause() {
super.onPause()
otpHandler.removeCallbacks(otpRunnable)
}
private fun updateOtpDisplay() {
val seed = binding.etOtpSeed.text.toString().trim()
if (seed.isEmpty()) {
binding.cardOtp.visibility = View.GONE
return
}
try {
val otp = Totp.generate(seed)
val secondsInPeriod = (System.currentTimeMillis() / 1000L % 30).toInt()
val remaining = 30 - secondsInPeriod
binding.tvOtpCode.text = otp
binding.otpTimer.max = 30
binding.otpTimer.progress = remaining
binding.cardOtp.visibility = View.VISIBLE
} catch (e: Exception) {
binding.cardOtp.visibility = View.GONE
}
}
private fun attemptLogin() {
val username = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString()
val otpSeed = binding.etOtpSeed.text.toString().trim()
Log.d(TAG, "Login button pressed for username=$username")
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
Log.w(TAG, "Validation failed: empty fields")
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 prefs = requireContext().getSharedPreferences("mib_prefs", android.content.Context.MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
viewLifecycleOwner.lifecycleScope.launch {
try {
Log.d(TAG, "Starting login flow on IO dispatcher")
val accounts = withContext(Dispatchers.IO) {
flow.login(username, password, otpSeed)
}
Log.d(TAG, "Login succeeded, got ${accounts.size} accounts")
(requireActivity().application as BasedBankApp).accounts = accounts
startActivity(Intent(requireContext(), HomeActivity::class.java))
requireActivity().finish()
} catch (e: Exception) {
Log.e(TAG, "Login failed: ${e.message}", e)
binding.tvError.text = e.message ?: "Login failed"
binding.tvError.visibility = View.VISIBLE
} finally {
binding.progressBar.visibility = View.GONE
binding.btnLogin.isEnabled = true
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,18 @@
package sh.sar.basedbank.ui.login
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.ActivityLoginBinding
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
@@ -0,0 +1,51 @@
package sh.sar.basedbank.ui.onboarding
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import sh.sar.basedbank.databinding.ActivityOnboardingBinding
import sh.sar.basedbank.ui.login.LoginActivity
class OnboardingActivity : AppCompatActivity() {
private lateinit var binding: ActivityOnboardingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityOnboardingBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = OnboardingPagerAdapter(this)
binding.viewPager.adapter = adapter
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach()
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
updateButtons(position, adapter.itemCount)
}
})
updateButtons(0, adapter.itemCount)
binding.btnNext.setOnClickListener {
val next = binding.viewPager.currentItem + 1
if (next < adapter.itemCount) binding.viewPager.currentItem = next
}
binding.btnGetStarted.setOnClickListener {
getSharedPreferences("prefs", MODE_PRIVATE)
.edit().putBoolean("onboarding_done", true).apply()
startActivity(Intent(this, LoginActivity::class.java))
finish()
}
}
private fun updateButtons(position: Int, count: Int) {
val isLast = position == count - 1
binding.btnNext.visibility = if (isLast) View.GONE else View.VISIBLE
binding.btnGetStarted.visibility = if (isLast) View.VISIBLE else View.GONE
}
}
@@ -0,0 +1,55 @@
package sh.sar.basedbank.ui.onboarding
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import sh.sar.basedbank.databinding.FragmentOnboardingSlideBinding
class OnboardingFragment : Fragment() {
private var _binding: FragmentOnboardingSlideBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentOnboardingSlideBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val title = requireArguments().getString(ARG_TITLE, "")
val desc = requireArguments().getString(ARG_DESC, "")
val icon = requireArguments().getInt(ARG_ICON, 0)
val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false)
binding.icon.setImageResource(icon)
binding.title.text = title
binding.description.text = desc
// On the first slide, show the two placeholder cards for upcoming banks
binding.placeholderCards.visibility = if (isFirst) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val ARG_TITLE = "title"
private const val ARG_DESC = "desc"
private const val ARG_ICON = "icon"
private const val ARG_IS_FIRST = "is_first"
fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply {
arguments = bundleOf(
ARG_TITLE to slide.title,
ARG_DESC to slide.description,
ARG_ICON to slide.iconRes,
ARG_IS_FIRST to slide.isFirst
)
}
}
}
@@ -0,0 +1,42 @@
package sh.sar.basedbank.ui.onboarding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import sh.sar.basedbank.R
class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
private val slides = listOf(
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_1),
description = activity.getString(R.string.onboarding_desc_1),
iconRes = R.drawable.ic_launcher_foreground,
isFirst = true
),
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_2),
description = activity.getString(R.string.onboarding_desc_2),
iconRes = R.drawable.ic_launcher_foreground,
isFirst = false
),
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_3),
description = activity.getString(R.string.onboarding_desc_3),
iconRes = R.drawable.ic_launcher_foreground,
isFirst = false
)
)
override fun getItemCount() = slides.size
override fun createFragment(position: Int): Fragment =
OnboardingFragment.newInstance(slides[position])
}
data class OnboardingSlide(
val title: String,
val description: String,
val iconRes: Int,
val isFirst: Boolean
)
@@ -1,42 +0,0 @@
package sh.sar.basedbank.ui.slideshow
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import sh.sar.basedbank.databinding.FragmentSlideshowBinding
class SlideshowFragment : Fragment() {
private var _binding: FragmentSlideshowBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val slideshowViewModel =
ViewModelProvider(this).get(SlideshowViewModel::class.java)
_binding = FragmentSlideshowBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textSlideshow
slideshowViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -1,13 +0,0 @@
package sh.sar.basedbank.ui.slideshow
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SlideshowViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is slideshow Fragment"
}
val text: LiveData<String> = _text
}