From 3105de73801217e153711a9198853a1e8105ca24 Mon Sep 17 00:00:00 2001 From: kmanikanta335 <118070186+kmanikanta335@users.noreply.github.com> Date: Sat, 25 Nov 2023 18:09:33 +0530 Subject: [PATCH 01/10] fix:When Recent Transactions is opened without connection of internet App crashes --- .../fragments/RecentTransactionsFragment.kt | 2 +- .../viewModels/RecentTransactionViewModel.kt | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt index fa64eacc9..917da11d2 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt @@ -74,7 +74,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { } is RecentTransactionUiState.Error -> { hideProgress() - showMessage(getString(it.message)) + showErrorFetchingRecentTransactions(getString(it.message)) } is RecentTransactionUiState.EmptyTransaction -> { hideProgress() diff --git a/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt b/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt index 5c00319cb..135cfe416 100644 --- a/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt +++ b/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt @@ -28,22 +28,25 @@ class RecentTransactionViewModel @Inject constructor(private val recentTransacti private fun loadRecentTransactions(offset: Int, limit: Int) { viewModelScope.launch { - _recentTransactionUiState.value = RecentTransactionUiState.Loading - val response = recentTransactionRepositoryImp.recentTransactions(offset, limit) - if (response?.isSuccessful == true) { - if (response.body()?.totalFilteredRecords == 0) { - _recentTransactionUiState.value = RecentTransactionUiState.EmptyTransaction - } else if (loadmore && response.body()?.pageItems?.isNotEmpty() == true) { - _recentTransactionUiState.value = - RecentTransactionUiState.LoadMoreRecentTransactions( - response.body()!!.pageItems - ) - } else if (response.body()?.pageItems?.isNotEmpty() == true) { - _recentTransactionUiState.value = RecentTransactionUiState.RecentTransactions( - response.body()?.pageItems!! - ) + try { + _recentTransactionUiState.value = RecentTransactionUiState.Loading + val response = recentTransactionRepositoryImp.recentTransactions(offset, limit) + if (response?.isSuccessful == true) { + if (response.body()?.totalFilteredRecords == 0) { + _recentTransactionUiState.value = RecentTransactionUiState.EmptyTransaction + } else if (loadmore && response.body()?.pageItems?.isNotEmpty() == true) { + _recentTransactionUiState.value = + RecentTransactionUiState.LoadMoreRecentTransactions( + response.body()!!.pageItems + ) + } else if (response.body()?.pageItems?.isNotEmpty() == true) { + _recentTransactionUiState.value = + RecentTransactionUiState.RecentTransactions( + response.body()?.pageItems!! + ) + } } - } else { + } catch(e: Exception){ _recentTransactionUiState.value = RecentTransactionUiState.Error(R.string.recent_transactions) } From e07e300369bab18ca7456aab029193b799718fa4 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Fri, 3 Nov 2023 19:27:14 +0530 Subject: [PATCH 02/10] refactor #2415: recent transaction viewmodel from to stateflow --- .../java/org/mifos/mobile/api/DataManager.kt | 3 +- .../api/services/RecentTransactionsService.kt | 3 +- .../RecentTransactionRepository.kt | 4 +- .../RecentTransactionRepositoryImp.kt | 10 ++- .../fragments/RecentTransactionsFragment.kt | 62 +++++++++++-------- .../mobile/utils/RecentTransactionUiState.kt | 4 +- .../viewModels/RecentTransactionViewModel.kt | 34 +++++----- 7 files changed, 66 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/api/DataManager.kt b/app/src/main/java/org/mifos/mobile/api/DataManager.kt index 259d60bf4..301b01220 100644 --- a/app/src/main/java/org/mifos/mobile/api/DataManager.kt +++ b/app/src/main/java/org/mifos/mobile/api/DataManager.kt @@ -1,7 +1,6 @@ package org.mifos.mobile.api import io.reactivex.Observable -import kotlinx.coroutines.flow.Flow import okhttp3.ResponseBody import org.mifos.mobile.api.local.DatabaseHelper import org.mifos.mobile.api.local.PreferencesHelper @@ -74,7 +73,7 @@ class DataManager @Inject constructor( return baseApiManager.clientsApi.getAccounts(clientId, accountType) } - suspend fun getRecentTransactions(offset: Int, limit: Int): Response?>? { + suspend fun getRecentTransactions(offset: Int, limit: Int): Page { return baseApiManager.recentTransactionsApi .getRecentTransactionsList(clientId, offset, limit) } diff --git a/app/src/main/java/org/mifos/mobile/api/services/RecentTransactionsService.kt b/app/src/main/java/org/mifos/mobile/api/services/RecentTransactionsService.kt index 68aad7067..10507e325 100644 --- a/app/src/main/java/org/mifos/mobile/api/services/RecentTransactionsService.kt +++ b/app/src/main/java/org/mifos/mobile/api/services/RecentTransactionsService.kt @@ -3,7 +3,6 @@ package org.mifos.mobile.api.services import org.mifos.mobile.api.ApiEndPoints import org.mifos.mobile.models.Page import org.mifos.mobile.models.Transaction -import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query @@ -18,5 +17,5 @@ interface RecentTransactionsService { @Path("clientId") clientId: Long?, @Query("offset") offset: Int?, @Query("limit") limit: Int?, - ): Response?>? + ): Page } diff --git a/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt index bdd004df4..6a4fc7200 100644 --- a/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt +++ b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt @@ -1,14 +1,14 @@ package org.mifos.mobile.repositories +import kotlinx.coroutines.flow.Flow import org.mifos.mobile.models.Page import org.mifos.mobile.models.Transaction -import retrofit2.Response interface RecentTransactionRepository { suspend fun recentTransactions( offset: Int?, limit: Int? - ): Response?>? + ): Flow> } \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt index a43f978b9..87fa6e408 100644 --- a/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt +++ b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt @@ -1,9 +1,10 @@ package org.mifos.mobile.repositories +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.mifos.mobile.api.DataManager import org.mifos.mobile.models.Page import org.mifos.mobile.models.Transaction -import retrofit2.Response import javax.inject.Inject class RecentTransactionRepositoryImp @Inject constructor(private val dataManager: DataManager) : @@ -12,7 +13,10 @@ class RecentTransactionRepositoryImp @Inject constructor(private val dataManager override suspend fun recentTransactions( offset: Int?, limit: Int? - ): Response?>? { - return limit?.let { offset?.let { it1 -> dataManager.getRecentTransactions(it1, it) } } + ): Flow> { + return flow { + offset?.let { limit?.let { it1 -> dataManager.getRecentTransactions(it, it1) } } + ?.let { emit(it) } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt index fa64eacc9..7f98534f0 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt @@ -6,12 +6,17 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.mifos.mobile.R import org.mifos.mobile.databinding.FragmentRecentTransactionsBinding import org.mifos.mobile.models.Transaction @@ -37,7 +42,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { @Inject var recentTransactionsListAdapter: RecentTransactionListAdapter? = null - private lateinit var recentTransactionViewModel: RecentTransactionViewModel + private val recentTransactionViewModel: RecentTransactionViewModel by viewModels() private var sweetUIErrorHandler: SweetUIErrorHandler? = null private var recentTransactionList: MutableList? = null @@ -52,7 +57,6 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { savedInstanceState: Bundle?, ): View { _binding = FragmentRecentTransactionsBinding.inflate(inflater, container, false) - recentTransactionViewModel = ViewModelProvider(this)[RecentTransactionViewModel::class.java] sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) showUserInterface() setToolbarTitle(getString(R.string.recent_transactions)) @@ -65,24 +69,30 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recentTransactionViewModel.recentTransactionUiState.observe(viewLifecycleOwner) { - when (it) { - is RecentTransactionUiState.Loading -> showProgress() - is RecentTransactionUiState.RecentTransactions -> { - hideProgress() - showRecentTransactions(it.transactions) - } - is RecentTransactionUiState.Error -> { - hideProgress() - showMessage(getString(it.message)) - } - is RecentTransactionUiState.EmptyTransaction -> { - hideProgress() - showEmptyTransaction() - } - is RecentTransactionUiState.LoadMoreRecentTransactions -> { - hideProgress() - showLoadMoreRecentTransactions(it.transactions) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + recentTransactionViewModel.recentTransactionUiState.collect{ + when (it) { + is RecentTransactionUiState.Loading -> showProgress() + is RecentTransactionUiState.RecentTransactions -> { + hideProgress() + showRecentTransactions(it.transactions) + } + is RecentTransactionUiState.Error -> { + hideProgress() + showMessage(getString(it.message)) + } + is RecentTransactionUiState.EmptyTransaction -> { + hideProgress() + showEmptyTransaction() + } + is RecentTransactionUiState.LoadMoreRecentTransactions -> { + hideProgress() + showLoadMoreRecentTransactions(it.transactions) + } + + RecentTransactionUiState.Initial -> {} + } } } } @@ -163,7 +173,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { /** * Shows a Toast */ - fun showMessage(message: String?) { + private fun showMessage(message: String?) { (activity as BaseActivity?)?.showToast(message!!) } @@ -173,7 +183,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { * * @param recentTransactionList List of [Transaction] */ - fun showRecentTransactions(recentTransactionList: List?) { + private fun showRecentTransactions(recentTransactionList: List?) { this.recentTransactionList = recentTransactionList as MutableList? recentTransactionsListAdapter?.setTransactions(recentTransactionList) } @@ -188,7 +198,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { recentTransactionsListAdapter?.notifyDataSetChanged() } - fun resetUI() { + private fun resetUI() { sweetUIErrorHandler?.hideSweetErrorLayoutUI( binding.rvRecentTransactions, binding.layoutError.root, @@ -198,7 +208,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { /** * Hides `rvRecentTransactions` and shows a textview prompting no transactions */ - fun showEmptyTransaction() { + private fun showEmptyTransaction() { sweetUIErrorHandler?.showSweetEmptyUI( getString(R.string.recent_transactions), R.drawable.ic_error_black_24dp, @@ -227,7 +237,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { } } - fun retryClicked() { + private fun retryClicked() { if (isConnected(requireContext())) { sweetUIErrorHandler?.hideSweetErrorLayoutUI( binding.rvRecentTransactions, @@ -251,7 +261,7 @@ class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { showSwipeRefreshLayout(false) } - fun showSwipeRefreshLayout(show: Boolean) { + private fun showSwipeRefreshLayout(show: Boolean) { binding.swipeTransactionContainer.post { binding.swipeTransactionContainer.isRefreshing = show } diff --git a/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt b/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt index 2838e9512..5fa15e671 100644 --- a/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt +++ b/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt @@ -3,9 +3,11 @@ package org.mifos.mobile.utils import org.mifos.mobile.models.Transaction sealed class RecentTransactionUiState { + object Initial : RecentTransactionUiState() object Loading : RecentTransactionUiState() object EmptyTransaction : RecentTransactionUiState() data class Error(val message: Int) : RecentTransactionUiState() data class RecentTransactions(val transactions: List) : RecentTransactionUiState() - data class LoadMoreRecentTransactions(val transactions: List) : RecentTransactionUiState() + data class LoadMoreRecentTransactions(val transactions: List) : + RecentTransactionUiState() } diff --git a/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt b/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt index 5c00319cb..e6cf3a80c 100644 --- a/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt +++ b/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt @@ -1,10 +1,11 @@ package org.mifos.mobile.viewModels -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch import org.mifos.mobile.R import org.mifos.mobile.repositories.RecentTransactionRepository @@ -18,8 +19,9 @@ class RecentTransactionViewModel @Inject constructor(private val recentTransacti private val limit = 50 private var loadmore = false - private val _recentTransactionUiState = MutableLiveData() - val recentTransactionUiState: LiveData = _recentTransactionUiState + private val _recentTransactionUiState = + MutableStateFlow(RecentTransactionUiState.Initial) + val recentTransactionUiState: StateFlow = _recentTransactionUiState fun loadRecentTransactions(loadmore: Boolean, offset: Int) { this.loadmore = loadmore @@ -29,23 +31,19 @@ class RecentTransactionViewModel @Inject constructor(private val recentTransacti private fun loadRecentTransactions(offset: Int, limit: Int) { viewModelScope.launch { _recentTransactionUiState.value = RecentTransactionUiState.Loading - val response = recentTransactionRepositoryImp.recentTransactions(offset, limit) - if (response?.isSuccessful == true) { - if (response.body()?.totalFilteredRecords == 0) { + recentTransactionRepositoryImp.recentTransactions(offset, limit).catch { + _recentTransactionUiState.value = + RecentTransactionUiState.Error(R.string.recent_transactions) + }.collect { + if (it.totalFilteredRecords == 0) { _recentTransactionUiState.value = RecentTransactionUiState.EmptyTransaction - } else if (loadmore && response.body()?.pageItems?.isNotEmpty() == true) { + } else if (loadmore && it.pageItems.isNotEmpty()) { + _recentTransactionUiState.value = + RecentTransactionUiState.LoadMoreRecentTransactions(it.pageItems) + } else if (it.pageItems.isNotEmpty()) { _recentTransactionUiState.value = - RecentTransactionUiState.LoadMoreRecentTransactions( - response.body()!!.pageItems - ) - } else if (response.body()?.pageItems?.isNotEmpty() == true) { - _recentTransactionUiState.value = RecentTransactionUiState.RecentTransactions( - response.body()?.pageItems!! - ) + RecentTransactionUiState.RecentTransactions(it.pageItems) } - } else { - _recentTransactionUiState.value = - RecentTransactionUiState.Error(R.string.recent_transactions) } } } From ec9a256d460d87053131a366737ef64735050bc6 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Sat, 4 Nov 2023 11:18:47 +0530 Subject: [PATCH 03/10] refactor #2415: client charge repository migrated to stateflow --- .../java/org/mifos/mobile/api/DataManager.kt | 10 +-- .../mifos/mobile/api/local/DatabaseHelper.kt | 12 ++-- .../api/services/ClientChargeService.kt | 6 +- .../repositories/ClientChargeRepository.kt | 10 +-- .../repositories/ClientChargeRepositoryImp.kt | 26 +++++--- .../ui/fragments/ClientChargeFragment.kt | 45 +++++++++----- .../mifos/mobile/utils/ClientChargeUiState.kt | 1 + .../viewModels/ClientChargeViewModel.kt | 62 ++++++------------- 8 files changed, 86 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/api/DataManager.kt b/app/src/main/java/org/mifos/mobile/api/DataManager.kt index 301b01220..fdecaf09f 100644 --- a/app/src/main/java/org/mifos/mobile/api/DataManager.kt +++ b/app/src/main/java/org/mifos/mobile/api/DataManager.kt @@ -78,17 +78,17 @@ class DataManager @Inject constructor( .getRecentTransactionsList(clientId, offset, limit) } - suspend fun getClientCharges(clientId: Long): Response?>? { + suspend fun getClientCharges(clientId: Long): Page { return baseApiManager.clientChargeApi.getClientChargeList(clientId).apply { - databaseHelper.syncCharges(this?.body()) + databaseHelper.syncCharges(this) } } - suspend fun getLoanCharges(loanId: Long): Response?>? { + suspend fun getLoanCharges(loanId: Long): List { return baseApiManager.clientChargeApi.getLoanAccountChargeList(loanId) } - suspend fun getSavingsCharges(savingsId: Long): Response?>? { + suspend fun getSavingsCharges(savingsId: Long): List { return baseApiManager.clientChargeApi.getSavingsAccountChargeList(savingsId) } @@ -205,7 +205,7 @@ class DataManager @Inject constructor( return baseApiManager.registrationApi.verifyUser(userVerify) } - suspend fun clientLocalCharges(): Response?> = databaseHelper.clientCharges() + suspend fun clientLocalCharges(): Page = databaseHelper.clientCharges() fun notifications(): List = databaseHelper.notifications() diff --git a/app/src/main/java/org/mifos/mobile/api/local/DatabaseHelper.kt b/app/src/main/java/org/mifos/mobile/api/local/DatabaseHelper.kt index 7818d3a08..024e3040c 100644 --- a/app/src/main/java/org/mifos/mobile/api/local/DatabaseHelper.kt +++ b/app/src/main/java/org/mifos/mobile/api/local/DatabaseHelper.kt @@ -2,6 +2,8 @@ package org.mifos.mobile.api.local import com.raizlabs.android.dbflow.sql.language.SQLite import io.reactivex.Observable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.mifos.mobile.models.Charge import org.mifos.mobile.models.Page import org.mifos.mobile.models.notification.MifosNotification @@ -17,21 +19,21 @@ import javax.inject.Singleton */ @Singleton class DatabaseHelper @Inject constructor() { - fun syncCharges(charges: Page?): Response?> { + fun syncCharges(charges: Page?): Page? { if (charges != null) { for (charge in charges.pageItems) - charge?.save() + charge.save() } - return Response.success(charges) + return charges } - fun clientCharges(): Response?> { + fun clientCharges(): Page { val charges = SQLite.select() .from(Charge::class.java) .queryList() val chargePage = Page() chargePage.pageItems = charges - return Response.success(chargePage) + return chargePage } fun notifications(): List { diff --git a/app/src/main/java/org/mifos/mobile/api/services/ClientChargeService.kt b/app/src/main/java/org/mifos/mobile/api/services/ClientChargeService.kt index dce4d237d..6c080b28d 100644 --- a/app/src/main/java/org/mifos/mobile/api/services/ClientChargeService.kt +++ b/app/src/main/java/org/mifos/mobile/api/services/ClientChargeService.kt @@ -14,11 +14,11 @@ import retrofit2.http.Path interface ClientChargeService { @GET(ApiEndPoints.CLIENTS + "/{clientId}/charges") - suspend fun getClientChargeList(@Path("clientId") clientId: Long?): Response?>? + suspend fun getClientChargeList(@Path("clientId") clientId: Long?): Page @GET(ApiEndPoints.LOANS + "/{loanId}/charges") - suspend fun getLoanAccountChargeList(@Path("loanId") loanId: Long?): Response?>? + suspend fun getLoanAccountChargeList(@Path("loanId") loanId: Long?): List @GET(ApiEndPoints.SAVINGS_ACCOUNTS + "/{savingsId}/charges") - suspend fun getSavingsAccountChargeList(@Path("savingsId") savingsId: Long?): Response?>? + suspend fun getSavingsAccountChargeList(@Path("savingsId") savingsId: Long?): List } diff --git a/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepository.kt b/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepository.kt index e69d77226..0ed6d793e 100644 --- a/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepository.kt +++ b/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepository.kt @@ -1,13 +1,13 @@ package org.mifos.mobile.repositories +import kotlinx.coroutines.flow.Flow import org.mifos.mobile.models.Charge import org.mifos.mobile.models.Page -import retrofit2.Response interface ClientChargeRepository { - suspend fun getClientCharges(clientId: Long): Response?>? - suspend fun getLoanCharges(loanId: Long): Response?>? - suspend fun getSavingsCharges(savingsId: Long): Response?>? - suspend fun clientLocalCharges(): Response?> + suspend fun getClientCharges(clientId: Long): Flow> + suspend fun getLoanCharges(loanId: Long): Flow> + suspend fun getSavingsCharges(savingsId: Long): Flow> + suspend fun clientLocalCharges(): Flow> } \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepositoryImp.kt b/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepositoryImp.kt index ad7a0cd15..2bdfef2eb 100644 --- a/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepositoryImp.kt +++ b/app/src/main/java/org/mifos/mobile/repositories/ClientChargeRepositoryImp.kt @@ -1,5 +1,7 @@ package org.mifos.mobile.repositories +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.mifos.mobile.api.DataManager import org.mifos.mobile.models.Charge import org.mifos.mobile.models.Page @@ -9,19 +11,27 @@ import javax.inject.Inject class ClientChargeRepositoryImp @Inject constructor(private val dataManager: DataManager) : ClientChargeRepository { - override suspend fun getClientCharges(clientId: Long): Response?>? { - return dataManager.getClientCharges(clientId) + override suspend fun getClientCharges(clientId: Long): Flow> { + return flow { + emit(dataManager.getClientCharges(clientId)) + } } - override suspend fun getLoanCharges(loanId: Long): Response?>? { - return dataManager.getLoanCharges(loanId) + override suspend fun getLoanCharges(loanId: Long): Flow> { + return flow { + emit(dataManager.getLoanCharges(loanId)) + } } - override suspend fun getSavingsCharges(savingsId: Long): Response?>? { - return dataManager.getSavingsCharges(savingsId) + override suspend fun getSavingsCharges(savingsId: Long): Flow> { + return flow { + emit(dataManager.getSavingsCharges(savingsId)) + } } - override suspend fun clientLocalCharges(): Response?> { - return dataManager.clientLocalCharges() + override suspend fun clientLocalCharges(): Flow> { + return flow { + emit(dataManager.clientLocalCharges()) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/ClientChargeFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/ClientChargeFragment.kt index 14a1fd4c2..59db3f0a4 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/ClientChargeFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/ClientChargeFragment.kt @@ -6,10 +6,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.mifos.mobile.R import org.mifos.mobile.databinding.FragmentClientChargeBinding import org.mifos.mobile.models.Charge @@ -21,7 +25,6 @@ import org.mifos.mobile.utils.Constants import org.mifos.mobile.utils.Network import org.mifos.mobile.utils.Toaster import org.mifos.mobile.viewModels.ClientChargeViewModel -import java.util.* /** * @author Vishwajeet @@ -33,7 +36,7 @@ class ClientChargeFragment : BaseFragment() { private var _binding: FragmentClientChargeBinding? = null private val binding get() = _binding!! - private lateinit var viewModel: ClientChargeViewModel + private val viewModel: ClientChargeViewModel by viewModels() private var clientChargeAdapter: ClientChargeAdapter? = null private var id: Long? = 0 @@ -55,7 +58,6 @@ class ClientChargeFragment : BaseFragment() { savedInstanceState: Bundle?, ): View { _binding = FragmentClientChargeBinding.inflate(inflater, container, false) - viewModel = ViewModelProvider(this)[ClientChargeViewModel::class.java] clientChargeAdapter = ClientChargeAdapter(::onItemClick) setToolbarTitle(getString(R.string.charges)) sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) @@ -78,16 +80,23 @@ class ClientChargeFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.clientChargeUiState.observe(viewLifecycleOwner) { - when (it) { - is ClientChargeUiState.Loading -> showProgress() - is ClientChargeUiState.ShowError -> { - hideProgress() - showErrorFetchingClientCharges(getString(it.message)) - } - is ClientChargeUiState.ShowClientCharges -> { - hideProgress() - showClientCharges(it.charges) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.clientChargeUiState.collect { + when (it) { + is ClientChargeUiState.Loading -> showProgress() + is ClientChargeUiState.ShowError -> { + hideProgress() + showErrorFetchingClientCharges(getString(it.message)) + } + + is ClientChargeUiState.ShowClientCharges -> { + hideProgress() + showClientCharges(it.charges) + } + + is ClientChargeUiState.Initial -> {} + } } } } @@ -101,9 +110,11 @@ class ClientChargeFragment : BaseFragment() { super.onSaveInstanceState(outState) outState.putParcelableArrayList( Constants.CHARGES, - ArrayList( - clientChargeList, - ), + clientChargeList?.let { + ArrayList( + it, + ) + }, ) } diff --git a/app/src/main/java/org/mifos/mobile/utils/ClientChargeUiState.kt b/app/src/main/java/org/mifos/mobile/utils/ClientChargeUiState.kt index 698f1eccf..6582f8156 100644 --- a/app/src/main/java/org/mifos/mobile/utils/ClientChargeUiState.kt +++ b/app/src/main/java/org/mifos/mobile/utils/ClientChargeUiState.kt @@ -3,6 +3,7 @@ package org.mifos.mobile.utils import org.mifos.mobile.models.Charge sealed class ClientChargeUiState { + object Initial : ClientChargeUiState() object Loading : ClientChargeUiState() data class ShowError(val message: Int) : ClientChargeUiState() data class ShowClientCharges(val charges: List) : ClientChargeUiState() diff --git a/app/src/main/java/org/mifos/mobile/viewModels/ClientChargeViewModel.kt b/app/src/main/java/org/mifos/mobile/viewModels/ClientChargeViewModel.kt index d7ba9757e..3803adc5a 100644 --- a/app/src/main/java/org/mifos/mobile/viewModels/ClientChargeViewModel.kt +++ b/app/src/main/java/org/mifos/mobile/viewModels/ClientChargeViewModel.kt @@ -1,9 +1,11 @@ package org.mifos.mobile.viewModels -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch import org.mifos.mobile.R import org.mifos.mobile.repositories.ClientChargeRepository @@ -14,24 +16,19 @@ import javax.inject.Inject class ClientChargeViewModel @Inject constructor(private val clientChargeRepositoryImp: ClientChargeRepository) : ViewModel() { - private val _clientChargeUiState = MutableLiveData() - val clientChargeUiState get() = _clientChargeUiState + private val _clientChargeUiState = + MutableStateFlow(ClientChargeUiState.Initial) + + val clientChargeUiState: StateFlow get() = _clientChargeUiState fun loadClientCharges(clientId: Long) { viewModelScope.launch { _clientChargeUiState.value = ClientChargeUiState.Loading - try { - val response = clientChargeRepositoryImp.getClientCharges(clientId) - if (response?.isSuccessful == true) { - _clientChargeUiState.value = - response.body()?.pageItems?.let { ClientChargeUiState.ShowClientCharges(it) } - } else { - _clientChargeUiState.value = - ClientChargeUiState.ShowError(R.string.client_charges) - } - } catch (e: Throwable) { + clientChargeRepositoryImp.getClientCharges(clientId).catch { _clientChargeUiState.value = ClientChargeUiState.ShowError(R.string.client_charges) + }.collect { + _clientChargeUiState.value = ClientChargeUiState.ShowClientCharges(it.pageItems) } } } @@ -39,18 +36,11 @@ class ClientChargeViewModel @Inject constructor(private val clientChargeReposito fun loadLoanAccountCharges(loanId: Long) { viewModelScope.launch { _clientChargeUiState.value = ClientChargeUiState.Loading - try { - val response = clientChargeRepositoryImp.getLoanCharges(loanId) - if (response?.isSuccessful == true) { - _clientChargeUiState.value = response.body() - ?.let { ClientChargeUiState.ShowClientCharges(it) } - } else { - _clientChargeUiState.value = - ClientChargeUiState.ShowError(R.string.client_charges) - } - } catch (e: Throwable) { + clientChargeRepositoryImp.getLoanCharges(loanId).catch { _clientChargeUiState.value = ClientChargeUiState.ShowError(R.string.client_charges) + }.collect { + _clientChargeUiState.value = ClientChargeUiState.ShowClientCharges(it) } } } @@ -58,18 +48,11 @@ class ClientChargeViewModel @Inject constructor(private val clientChargeReposito fun loadSavingsAccountCharges(savingsId: Long) { viewModelScope.launch { _clientChargeUiState.value = ClientChargeUiState.Loading - try { - val response = clientChargeRepositoryImp.getSavingsCharges(savingsId) - if (response?.isSuccessful == true) { - _clientChargeUiState.value = response.body() - ?.let { ClientChargeUiState.ShowClientCharges(it) } - } else { - _clientChargeUiState.value = - ClientChargeUiState.ShowError(R.string.client_charges) - } - } catch (e: Throwable) { + clientChargeRepositoryImp.getSavingsCharges(savingsId).catch { _clientChargeUiState.value = ClientChargeUiState.ShowError(R.string.client_charges) + }.collect { + _clientChargeUiState.value = ClientChargeUiState.ShowClientCharges(it) } } } @@ -77,18 +60,11 @@ class ClientChargeViewModel @Inject constructor(private val clientChargeReposito fun loadClientLocalCharges() { viewModelScope.launch { _clientChargeUiState.value = ClientChargeUiState.Loading - try { - val response = clientChargeRepositoryImp.clientLocalCharges() - if (response.isSuccessful) { - _clientChargeUiState.value = - response.body()?.pageItems?.let { ClientChargeUiState.ShowClientCharges(it) } - } else { - _clientChargeUiState.value = - ClientChargeUiState.ShowError(R.string.client_charges) - } - } catch (e: Throwable) { + clientChargeRepositoryImp.clientLocalCharges().catch { _clientChargeUiState.value = ClientChargeUiState.ShowError(R.string.client_charges) + }.collect { + _clientChargeUiState.value = ClientChargeUiState.ShowClientCharges(it.pageItems) } } } From b3009e7e0daba9f7d948793f1d68a807c121cc99 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Thu, 9 Nov 2023 22:23:53 +0530 Subject: [PATCH 04/10] refactor: login screen from xml view to compose --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 2 +- .../mobile/ui/activities/HomeActivity.kt | 1 + .../mobile/ui/activities/PassCodeActivity.kt | 1 + .../mobile/ui/activities/SplashActivity.kt | 2 +- .../RegistrationVerificationFragment.kt | 2 +- .../ui/{activities => login}/LoginActivity.kt | 183 +++++++----------- .../org/mifos/mobile/ui/login/LoginScreen.kt | 181 +++++++++++++++++ .../login}/LoginViewModel.kt | 2 +- .../ConfigurationDialogFragmentCompat.kt | 2 +- .../mobile/viewModels/LoginViewModelTest.kt | 1 + .../core/ui/component/MifosMobileIcon.kt | 27 +++ .../ui/component/MifosOutlinedTextField.kt | 86 ++++++++ .../org/mifos/mobile/core/ui/theme/Color.kt | 3 +- .../org/mifos/mobile/core/ui/theme/Theme.kt | 3 +- 15 files changed, 380 insertions(+), 118 deletions(-) rename app/src/main/java/org/mifos/mobile/ui/{activities => login}/LoginActivity.kt (52%) create mode 100644 app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt rename app/src/main/java/org/mifos/mobile/{viewModels => ui/login}/LoginViewModel.kt (98%) create mode 100644 core/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt create mode 100644 core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt diff --git a/app/build.gradle b/app/build.gradle index 5b84004a7..e276955ac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -203,5 +203,7 @@ dependencies { debugImplementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion" implementation "androidx.compose.material3:material3:$rootProject.materialVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$rootProject.lifecycleVersion" + implementation "androidx.compose.material:material-icons-extended:$rootProject.composeVersion" + } apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0070e56a8..e01c1fed3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ android:value="@string/google_maps_key" /> diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt index cd74b71bd..59bc14e57 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt @@ -36,6 +36,7 @@ import org.mifos.mobile.ui.enums.AccountType import org.mifos.mobile.ui.enums.ChargeType import org.mifos.mobile.ui.fragments.* import org.mifos.mobile.ui.getThemeAttributeColor +import org.mifos.mobile.ui.login.LoginActivity import org.mifos.mobile.utils.Constants import org.mifos.mobile.utils.TextDrawable import org.mifos.mobile.utils.Toaster diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/PassCodeActivity.kt b/app/src/main/java/org/mifos/mobile/ui/activities/PassCodeActivity.kt index b88fcc645..46065bf22 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/PassCodeActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/activities/PassCodeActivity.kt @@ -15,6 +15,7 @@ import com.mifos.mobile.passcode.MifosPassCodeActivity import com.mifos.mobile.passcode.utils.EncryptionUtil import org.mifos.mobile.R import org.mifos.mobile.ui.enums.BiometricCapability +import org.mifos.mobile.ui.login.LoginActivity import org.mifos.mobile.utils.CheckSelfPermissionAndRequest import org.mifos.mobile.utils.Constants import org.mifos.mobile.utils.MaterialDialog diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/SplashActivity.kt b/app/src/main/java/org/mifos/mobile/ui/activities/SplashActivity.kt index 0bae7c94b..857a4b441 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/SplashActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/activities/SplashActivity.kt @@ -3,8 +3,8 @@ package org.mifos.mobile.ui.activities import android.content.Intent import android.os.Bundle import com.mifos.mobile.passcode.utils.PasscodePreferencesHelper -import dagger.hilt.android.AndroidEntryPoint import org.mifos.mobile.ui.activities.base.BaseActivity +import org.mifos.mobile.ui.login.LoginActivity import org.mifos.mobile.utils.Constants /* diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationVerificationFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationVerificationFragment.kt index 04d40b8ed..0907a3a14 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationVerificationFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationVerificationFragment.kt @@ -14,7 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.mifos.mobile.R import org.mifos.mobile.databinding.FragmentRegistrationVerificationBinding -import org.mifos.mobile.ui.activities.LoginActivity +import org.mifos.mobile.ui.login.LoginActivity import org.mifos.mobile.ui.fragments.base.BaseFragment import org.mifos.mobile.utils.RegistrationUiState import org.mifos.mobile.utils.Toaster diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/LoginActivity.kt b/app/src/main/java/org/mifos/mobile/ui/login/LoginActivity.kt similarity index 52% rename from app/src/main/java/org/mifos/mobile/ui/activities/LoginActivity.kt rename to app/src/main/java/org/mifos/mobile/ui/login/LoginActivity.kt index 3521c3d39..7e6ebdbec 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/LoginActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/login/LoginActivity.kt @@ -1,11 +1,9 @@ -package org.mifos.mobile.ui.activities +package org.mifos.mobile.ui.login import android.content.Intent import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.EditText import android.widget.Toast +import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -14,13 +12,13 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.mifos.mobile.MifosSelfServiceApp.Companion.context import org.mifos.mobile.R -import org.mifos.mobile.databinding.ActivityLoginBinding +import org.mifos.mobile.core.ui.theme.MifosMobileTheme +import org.mifos.mobile.ui.activities.PassCodeActivity +import org.mifos.mobile.ui.activities.RegistrationActivity import org.mifos.mobile.ui.activities.base.BaseActivity import org.mifos.mobile.utils.Constants import org.mifos.mobile.utils.LoginUiState import org.mifos.mobile.utils.Network -import org.mifos.mobile.utils.Toaster -import org.mifos.mobile.viewModels.LoginViewModel /** * @author Vishwajeet @@ -29,26 +27,26 @@ import org.mifos.mobile.viewModels.LoginViewModel @AndroidEntryPoint class LoginActivity : BaseActivity() { - private lateinit var binding: ActivityLoginBinding private val viewModel: LoginViewModel by viewModels() + private lateinit var usernameContent: String + private lateinit var passwordContent: String + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityLoginBinding.inflate(layoutInflater) - setContentView(binding.root) - dismissSoftKeyboardOnBkgTap(binding.nsvBackground) - binding.btnLogin.setOnClickListener { - onLoginClicked() - } - binding.btnRegister.setOnClickListener { - onRegisterClicked() - } - binding.etUsername.setOnTouchListener { view, _ -> - onTouch(view) - } - - binding.etPassword.setOnTouchListener { view, _ -> - onTouch(view) + setContent { + MifosMobileTheme { + LoginScreen( + login = { username, password -> + usernameContent = username + passwordContent = password + onLoginClicked() + }, + createAccount = { onRegisterClicked() }, + getUsernameError = { validateUsername(it) }, + getPasswordError = { validatePassword(it) } + ) + } } lifecycleScope.launch { @@ -78,29 +76,6 @@ class LoginActivity : BaseActivity() { } } - private fun dismissSoftKeyboardOnBkgTap(view: View) { - if (view !is EditText) { - view.setOnTouchListener { _, _ -> - hideKeyboard(this@LoginActivity) - false - } - } - if (view is ViewGroup) { - for (i in 0 until view.childCount) { - val innerView = view.getChildAt(i) - dismissSoftKeyboardOnBkgTap(innerView) - } - } - } - - private fun onTouch(v: View): Boolean { - when (v) { - binding.etUsername -> clearUsernameError() - binding.etPassword -> clearPasswordError() - } - return false - } - /** * Called when Login is user has successfully logged in */ @@ -137,23 +112,6 @@ class LoginActivity : BaseActivity() { */ private fun showMessage(errorMessage: String?) { showToast(errorMessage!!, Toast.LENGTH_LONG) - binding.llLogin.visibility = View.VISIBLE - } - - private fun showUsernameError(error: String?) { - binding.tilUsername.error = error - } - - private fun showPasswordError(error: String?) { - binding.tilPassword.error = error - } - - private fun clearUsernameError() { - binding.tilUsername.isErrorEnabled = false - } - - private fun clearPasswordError() { - binding.tilPassword.isErrorEnabled = false } /** @@ -161,84 +119,91 @@ class LoginActivity : BaseActivity() { */ private fun onLoginClicked() { - val username = binding.tilUsername.editText?.editableText.toString() - val password = binding.tilPassword.editText?.editableText.toString() if (Network.isConnected(this)) { - if (isCredentialsValid(username, password)) - viewModel.login(username, password) + if (isCredentialsValid(usernameContent, passwordContent)) + viewModel.login(usernameContent, passwordContent) } else { - Toaster.show(binding.llLogin, getString(R.string.no_internet_connection)) + showMessage(context?.getString(R.string.no_internet_connection)) } } private fun isCredentialsValid(username: String, password: String): Boolean { var credentialValid = true - val resources = context?.resources when { viewModel.isFieldEmpty(username) -> { - showUsernameError( - context?.getString( - R.string.error_validation_blank, - context?.getString(R.string.username), - ), - ) credentialValid = false } viewModel.isUsernameLengthInadequate(username) -> { - showUsernameError( - context?.getString( - R.string.error_validation_minimum_chars, - resources?.getString(R.string.username), - resources?.getInteger(R.integer.username_minimum_length), - ), - ) credentialValid = false } viewModel.usernameHasSpaces(username) -> { - showUsernameError( - context?.getString( - R.string.error_validation_cannot_contain_spaces, - resources?.getString(R.string.username), - context?.getString(R.string.not_contain_username), - ), - ) credentialValid = false } - - else -> { - clearUsernameError() - } } when { viewModel.isFieldEmpty(password) -> { - showPasswordError( - context?.getString( - R.string.error_validation_blank, - context?.getString(R.string.password), - ), - ) credentialValid = false } viewModel.isPasswordLengthInadequate(password) -> { - showPasswordError( - context?.getString( - R.string.error_validation_minimum_chars, - resources?.getString(R.string.password), - resources?.getInteger(R.integer.password_minimum_length), - ), - ) credentialValid = false } + } + return credentialValid + } + + private fun validateUsername(username: String): String { + var usernameError = "" + when { + viewModel.isFieldEmpty(username) -> { + usernameError = context?.getString( + R.string.error_validation_blank, + context?.getString(R.string.username), + ).toString() + } + + viewModel.isUsernameLengthInadequate(username) -> { + usernameError = context?.getString( + R.string.error_validation_minimum_chars, + resources?.getString(R.string.username), + resources?.getInteger(R.integer.username_minimum_length), + ).toString() + } - else -> { - clearPasswordError() + viewModel.usernameHasSpaces(username) -> { + usernameError = context?.getString( + R.string.error_validation_cannot_contain_spaces, + resources?.getString(R.string.username), + context?.getString(R.string.not_contain_username), + ).toString() } } - return credentialValid + return usernameError + } + + private fun validatePassword(password: String): String { + var passwordError = "" + when { + viewModel.isFieldEmpty(password) -> { + passwordError = context?.getString( + R.string.error_validation_blank, + context?.getString(R.string.password), + ).toString() + + } + + viewModel.isPasswordLengthInadequate(password) -> { + passwordError = context?.getString( + R.string.error_validation_minimum_chars, + resources?.getString(R.string.password), + resources?.getInteger(R.integer.password_minimum_length), + ).toString() + } + } + return passwordError } private fun onRegisterClicked() { diff --git a/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt b/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt new file mode 100644 index 000000000..faf63588f --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt @@ -0,0 +1,181 @@ +package org.mifos.mobile.ui.login + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.mifos.mobile.R +import org.mifos.mobile.core.ui.component.MifosMobileIcon +import org.mifos.mobile.core.ui.component.MifosOutlinedTextField + +@Composable +fun LoginScreen( + login: (username: String, password: String) -> Unit, + createAccount: () -> Unit, + getUsernameError: (String) -> String, + getPasswordError: (String) -> String +) { + val keyboardController = LocalSoftwareKeyboardController.current + + var username by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(""))} + var password by rememberSaveable(stateSaver = TextFieldValue.Saver) {mutableStateOf( TextFieldValue(""))} + + var passwordVisibility: Boolean by remember { mutableStateOf(false) } + + var usernameError by rememberSaveable { mutableStateOf(false) } + var passwordError by rememberSaveable { mutableStateOf(false)} + + val usernameErrorContent = getUsernameError.invoke(username.text) + val passwordErrorContent = getPasswordError.invoke(password.text) + + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .pointerInput(Unit) { + detectTapGestures(onTap = { + keyboardController?.hide() + }) + } + ) { + MifosMobileIcon(id = R.drawable.mifos_logo) + + MifosOutlinedTextField( + value = username, + onValueChange = { + username = it + usernameError = false + }, + label = R.string.username, + icon = R.drawable.ic_person_black_24dp, + error = usernameError, + supportingText = usernameErrorContent, + trailingIcon = { + if (usernameError) { + Icon(imageVector = Icons.Filled.Error, contentDescription = null) + } + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MifosOutlinedTextField( + value = password, + onValueChange = { + password = it + passwordError = false + }, + label = R.string.password, + icon = R.drawable.ic_lock_black_24dp, + visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + if (!passwordError) { + val image = if (passwordVisibility) + Icons.Filled.Visibility + else Icons.Filled.VisibilityOff + IconButton(onClick = { passwordVisibility = !passwordVisibility }) { + Icon(imageVector = image, null) + } + } else { + Icon(imageVector = Icons.Filled.Error, contentDescription = null) + } + }, + error = passwordError, + supportingText = passwordErrorContent + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { + when { + usernameErrorContent.isEmpty() && passwordErrorContent.isEmpty() -> { + login.invoke(username.text, password.text) + } + usernameErrorContent.isEmpty() && passwordErrorContent.isNotEmpty() -> { + passwordError = true + } + usernameErrorContent.isNotEmpty() && passwordErrorContent.isEmpty() -> { + usernameError = true + } + else -> { + passwordError = true + usernameError = true + } + } + keyboardController?.hide() + }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 4.dp), + contentPadding = PaddingValues(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (isSystemInDarkTheme()) Color( + 0xFF9bb1e3 + ) else Color(0xFF325ca8) + ) + ) { + Text(text = stringResource(id = R.string.login)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TextButton( + onClick = { + createAccount.invoke() + }, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + colors = ButtonDefaults.textButtonColors( + contentColor = if (isSystemInDarkTheme()) Color( + 0xFF9bb1e3 + ) else Color(0xFF325ca8) + ) + ) { + Text(text = stringResource(id = R.string.create_an_account)) + } + } +} + +@Preview(showSystemUi = true, uiMode = UI_MODE_NIGHT_NO) +@Composable +fun LoginScreenPreview() { + LoginScreen({ _, _ -> }, {}, { "" }, { "" }) +} diff --git a/app/src/main/java/org/mifos/mobile/viewModels/LoginViewModel.kt b/app/src/main/java/org/mifos/mobile/ui/login/LoginViewModel.kt similarity index 98% rename from app/src/main/java/org/mifos/mobile/viewModels/LoginViewModel.kt rename to app/src/main/java/org/mifos/mobile/ui/login/LoginViewModel.kt index 1a8658dcd..ad330a095 100644 --- a/app/src/main/java/org/mifos/mobile/viewModels/LoginViewModel.kt +++ b/app/src/main/java/org/mifos/mobile/ui/login/LoginViewModel.kt @@ -1,4 +1,4 @@ -package org.mifos.mobile.viewModels +package org.mifos.mobile.ui.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/org/mifos/mobile/utils/ConfigurationDialogFragmentCompat.kt b/app/src/main/java/org/mifos/mobile/utils/ConfigurationDialogFragmentCompat.kt index 30e2f6b60..0ad133e68 100644 --- a/app/src/main/java/org/mifos/mobile/utils/ConfigurationDialogFragmentCompat.kt +++ b/app/src/main/java/org/mifos/mobile/utils/ConfigurationDialogFragmentCompat.kt @@ -15,7 +15,7 @@ import butterknife.ButterKnife import com.google.android.material.textfield.TextInputLayout import org.mifos.mobile.R import org.mifos.mobile.api.local.PreferencesHelper -import org.mifos.mobile.ui.activities.LoginActivity +import org.mifos.mobile.ui.login.LoginActivity import java.net.MalformedURLException import java.net.URL diff --git a/app/src/test/java/org/mifos/mobile/viewModels/LoginViewModelTest.kt b/app/src/test/java/org/mifos/mobile/viewModels/LoginViewModelTest.kt index 77526baf9..953c5e8a6 100644 --- a/app/src/test/java/org/mifos/mobile/viewModels/LoginViewModelTest.kt +++ b/app/src/test/java/org/mifos/mobile/viewModels/LoginViewModelTest.kt @@ -15,6 +15,7 @@ import org.mifos.mobile.models.User import org.mifos.mobile.models.client.Client import org.mifos.mobile.repositories.ClientRepository import org.mifos.mobile.repositories.UserAuthRepository +import org.mifos.mobile.ui.login.LoginViewModel import org.mifos.mobile.util.RxSchedulersOverrideRule import org.mifos.mobile.utils.LoginUiState import org.mockito.Mock diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt new file mode 100644 index 000000000..4c2dba29f --- /dev/null +++ b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt @@ -0,0 +1,27 @@ +package org.mifos.mobile.core.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp + +@Composable +fun MifosMobileIcon( + id: Int +) { + Column { + Image( + painter = painterResource(id = id), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(0.dp, 56.dp) + ) + } +} \ No newline at end of file diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt new file mode 100644 index 000000000..ccaa1481c --- /dev/null +++ b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt @@ -0,0 +1,86 @@ +package org.mifos.mobile.core.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MifosOutlinedTextField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + maxLines: Int = 1, + singleLine: Boolean = true, + icon: Int? = null, + label: Int, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null, + error: Boolean = false, + supportingText: String +) { + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(stringResource(id = label)) }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), + leadingIcon = { + icon?.let { painterResource(id = it) }?.let { + Image( + painter = it, + contentDescription = null, + colorFilter = if (isSystemInDarkTheme()) ColorFilter.tint(Color.White) else ColorFilter.tint( + Color.Black + ) + ) + } + }, + trailingIcon = trailingIcon, + maxLines = maxLines, + singleLine = singleLine, + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = if (isSystemInDarkTheme()) Color( + 0xFF9bb1e3 + ) else Color(0xFF325ca8) + ), + textStyle = LocalDensity.current.run { + TextStyle(fontSize = 18.sp) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + visualTransformation = visualTransformation, + isError = error, + supportingText = { + if (error) { + Text( + modifier = Modifier.fillMaxWidth(), + text = supportingText, + color = MaterialTheme.colorScheme.error + ) + } + } + ) +} \ No newline at end of file diff --git a/core/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt b/core/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt index 29b037ed2..59520f9a3 100644 --- a/core/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt +++ b/core/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt @@ -10,5 +10,4 @@ val Black2 = Color(0xFF000000) val BackgroundLight = Color(0xFFFEFBFF) val BackgroundDark = Color(0xFF1B1B1F) -val RedErrorDark = Color(0xFFB00020) -val RedErrorLight = Color(0xFFEF5350) \ No newline at end of file +val RedErrorDark = Color(0xFFB00020) \ No newline at end of file diff --git a/core/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt b/core/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt index 65691cb27..924beda10 100644 --- a/core/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt +++ b/core/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt @@ -10,7 +10,6 @@ private val LightThemeColors = lightColorScheme( primary = Blue600, onPrimary = Black2, error = RedErrorDark, - onError = RedErrorLight, background = BackgroundLight, onSurface = Black2, ) @@ -18,7 +17,7 @@ private val LightThemeColors = lightColorScheme( private val DarkThemeColors = darkColorScheme( primary = Blue700, secondary = Black1, - error = RedErrorLight, + error = RedErrorDark, background = BackgroundDark, surface = Black1, ) From abc64adba799e0ca96bb985c7fd7495821db10b7 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Mon, 13 Nov 2023 12:40:02 +0530 Subject: [PATCH 05/10] fix: edge case to handle leading icon --- .../mobile/core/ui/component/MifosOutlinedTextField.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt index ccaa1481c..58157c564 100644 --- a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt +++ b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt @@ -48,17 +48,17 @@ fun MifosOutlinedTextField( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp), - leadingIcon = { - icon?.let { painterResource(id = it) }?.let { + leadingIcon = if (icon != null) { + { Image( - painter = it, + painter = painterResource(id = icon), contentDescription = null, colorFilter = if (isSystemInDarkTheme()) ColorFilter.tint(Color.White) else ColorFilter.tint( Color.Black ) ) } - }, + } else null, trailingIcon = trailingIcon, maxLines = maxLines, singleLine = singleLine, From d5dae0f68b66f1690d53341901c428d9ffb86596 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Fri, 24 Nov 2023 11:03:05 +0530 Subject: [PATCH 06/10] design: divider in login screen --- .../org/mifos/mobile/ui/login/LoginScreen.kt | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt b/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt index faf63588f..c81d04aec 100644 --- a/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt @@ -3,8 +3,10 @@ package org.mifos.mobile.ui.login import android.content.res.Configuration.UI_MODE_NIGHT_NO import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -18,6 +20,7 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -25,7 +28,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -39,6 +41,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.mifos.mobile.R import org.mifos.mobile.core.ui.component.MifosMobileIcon import org.mifos.mobile.core.ui.component.MifosOutlinedTextField @@ -52,13 +55,21 @@ fun LoginScreen( ) { val keyboardController = LocalSoftwareKeyboardController.current - var username by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(""))} - var password by rememberSaveable(stateSaver = TextFieldValue.Saver) {mutableStateOf( TextFieldValue(""))} + var username by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue("") + ) + } + var password by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue("") + ) + } - var passwordVisibility: Boolean by remember { mutableStateOf(false) } + var passwordVisibility: Boolean by rememberSaveable { mutableStateOf(false) } var usernameError by rememberSaveable { mutableStateOf(false) } - var passwordError by rememberSaveable { mutableStateOf(false)} + var passwordError by rememberSaveable { mutableStateOf(false) } val usernameErrorContent = getUsernameError.invoke(username.text) val passwordErrorContent = getPasswordError.invoke(password.text) @@ -128,12 +139,15 @@ fun LoginScreen( usernameErrorContent.isEmpty() && passwordErrorContent.isEmpty() -> { login.invoke(username.text, password.text) } + usernameErrorContent.isEmpty() && passwordErrorContent.isNotEmpty() -> { passwordError = true } + usernameErrorContent.isNotEmpty() && passwordErrorContent.isEmpty() -> { usernameError = true } + else -> { passwordError = true usernameError = true @@ -156,6 +170,31 @@ fun LoginScreen( Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp) + .weight(1f), color = Color.Gray, thickness = 1.dp + ) + Text( + modifier = Modifier.padding(8.dp), + text = "or", + fontSize = 18.sp, + color = if (isSystemInDarkTheme()) Color.White else Color.Black + ) + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp) + .weight(1f), color = Color.Gray, thickness = 1.dp + ) + } + TextButton( onClick = { createAccount.invoke() From 71a2aab7af0d8ab8dac4e11849bad6662f557de0 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Sun, 24 Dec 2023 10:13:34 +0530 Subject: [PATCH 07/10] refactor: user profile screen to compose --- app/src/main/AndroidManifest.xml | 2 +- .../mobile/ui/activities/HomeActivity.kt | 3 +- .../mobile/ui/fragments/HomeOldFragment.kt | 2 +- .../user_profile}/UserDetailViewModel.kt | 2 +- .../mobile/ui/user_profile/UserDetails.kt | 27 ++++ .../UserProfileActivity.kt | 4 +- .../ui/user_profile/UserProfileDetails.kt | 92 +++++++++++ .../UserProfileFragment.kt | 146 ++++++++++-------- .../ui/user_profile/UserProfileScreen.kt | 101 ++++++++++++ .../viewModels/UserDetailViewModelTest.kt | 1 + .../core/ui/component/MifosUserImage.kt | 41 +++++ .../mobile/core/ui/component/NoInternet.kt | 51 ++++++ .../core/ui/component/UserProfileField.kt | 74 +++++++++ .../core/ui/component/UserProfileTopBar.kt | 86 +++++++++++ 14 files changed, 561 insertions(+), 71 deletions(-) rename app/src/main/java/org/mifos/mobile/{viewModels => ui/user_profile}/UserDetailViewModel.kt (99%) create mode 100644 app/src/main/java/org/mifos/mobile/ui/user_profile/UserDetails.kt rename app/src/main/java/org/mifos/mobile/ui/{activities => user_profile}/UserProfileActivity.kt (85%) create mode 100644 app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileDetails.kt rename app/src/main/java/org/mifos/mobile/ui/{fragments => user_profile}/UserProfileFragment.kt (64%) create mode 100644 app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt create mode 100644 core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt create mode 100644 core/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt create mode 100644 core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt create mode 100644 core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e01c1fed3..1ca6da262 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -67,7 +67,7 @@ android:windowSoftInputMode="adjustResize" /> diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt index 59bc14e57..5895cb8c3 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt @@ -42,7 +42,8 @@ import org.mifos.mobile.utils.TextDrawable import org.mifos.mobile.utils.Toaster import org.mifos.mobile.utils.UserDetailUiState import org.mifos.mobile.utils.fcm.RegistrationIntentService -import org.mifos.mobile.viewModels.UserDetailViewModel +import org.mifos.mobile.ui.user_profile.UserDetailViewModel +import org.mifos.mobile.ui.user_profile.UserProfileActivity import javax.inject.Inject /** diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/HomeOldFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/HomeOldFragment.kt index ea8a7ee22..d1eef0368 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/HomeOldFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/HomeOldFragment.kt @@ -22,7 +22,7 @@ import org.mifos.mobile.models.client.Client import org.mifos.mobile.ui.activities.HomeActivity import org.mifos.mobile.ui.activities.LoanApplicationActivity import org.mifos.mobile.ui.activities.NotificationActivity -import org.mifos.mobile.ui.activities.UserProfileActivity +import org.mifos.mobile.ui.user_profile.UserProfileActivity import org.mifos.mobile.ui.activities.base.BaseActivity import org.mifos.mobile.ui.enums.AccountType import org.mifos.mobile.ui.enums.ChargeType diff --git a/app/src/main/java/org/mifos/mobile/viewModels/UserDetailViewModel.kt b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserDetailViewModel.kt similarity index 99% rename from app/src/main/java/org/mifos/mobile/viewModels/UserDetailViewModel.kt rename to app/src/main/java/org/mifos/mobile/ui/user_profile/UserDetailViewModel.kt index c0e194d10..59a43340d 100644 --- a/app/src/main/java/org/mifos/mobile/viewModels/UserDetailViewModel.kt +++ b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserDetailViewModel.kt @@ -1,4 +1,4 @@ -package org.mifos.mobile.viewModels +package org.mifos.mobile.ui.user_profile import android.util.Base64 import android.util.Log diff --git a/app/src/main/java/org/mifos/mobile/ui/user_profile/UserDetails.kt b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserDetails.kt new file mode 100644 index 000000000..776a1b8c1 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserDetails.kt @@ -0,0 +1,27 @@ +package org.mifos.mobile.ui.user_profile + +data class UserDetails( + val userName: String?, + val accountNumber: String?, + val activationDate: String?, + val officeName: String?, + val clientType: String?, + val groups: String?, + val clientClassification: String?, + val phoneNumber: String?, + val dob: String?, + val gender: String? +) { + constructor() : this( + accountNumber = "", + activationDate = "", + clientClassification = "", + clientType = "", + dob = "", + gender = "", + groups = "", + officeName = "", + phoneNumber = "", + userName = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/UserProfileActivity.kt b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileActivity.kt similarity index 85% rename from app/src/main/java/org/mifos/mobile/ui/activities/UserProfileActivity.kt rename to app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileActivity.kt index 422f04763..529f25508 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/UserProfileActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileActivity.kt @@ -1,10 +1,10 @@ -package org.mifos.mobile.ui.activities +package org.mifos.mobile.ui.user_profile import android.os.Bundle import org.mifos.mobile.R import org.mifos.mobile.databinding.ActivityUserProfileBinding import org.mifos.mobile.ui.activities.base.BaseActivity -import org.mifos.mobile.ui.fragments.UserProfileFragment +import org.mifos.mobile.ui.user_profile.UserProfileFragment class UserProfileActivity : BaseActivity() { diff --git a/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileDetails.kt b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileDetails.kt new file mode 100644 index 000000000..b092fc099 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileDetails.kt @@ -0,0 +1,92 @@ +package org.mifos.mobile.ui.user_profile + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.mifos.mobile.R + +/** + * @author pratyush + * @since 20/12/2023 + */ + +@Composable +fun UserProfileDetails( + userDetails: UserDetails +) { + Column { + Text( + modifier = Modifier.padding(top = 16.dp, start = 8.dp), + text = stringResource(id = R.string.user_details), + color = Color(0xFF8E9099), + style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold), + ) + Row( + modifier = Modifier.padding(start = 8.dp), + verticalAlignment = Alignment.Bottom + ) { + Icon( + modifier = Modifier.padding(top = 8.dp, end = 8.dp), + painter = painterResource(id = R.drawable.ic_phone_24dp), + tint = if (isSystemInDarkTheme()) Color(0xFF9bb1e3) else Color(0xFF325ca8), + contentDescription = null + ) + userDetails.phoneNumber?.let { + Text( + text = it, + color = if (isSystemInDarkTheme()) Color.White else Color.Black, + style = TextStyle(fontSize = 14.sp) + ) + } + } + Row( + modifier = Modifier.padding(start = 8.dp), + verticalAlignment = Alignment.Bottom + ) { + Icon( + modifier = Modifier.padding(top = 8.dp, end = 8.dp), + painter = painterResource(id = R.drawable.ic_cake_24dp), + tint = if (isSystemInDarkTheme()) Color(0xFF9bb1e3) else Color(0xFF325ca8), + contentDescription = null + ) + userDetails.dob?.let { + Text( + text = it, + color = if (isSystemInDarkTheme()) Color.White else Color.Black, + style = TextStyle(fontSize = 14.sp) + ) + } + } + Row( + modifier = Modifier.padding(start = 8.dp), + verticalAlignment = Alignment.Bottom + ) { + Icon( + modifier = Modifier.padding(top = 8.dp, end = 8.dp), + painter = painterResource(id = R.drawable.ic_gender_24dp), + tint = if (isSystemInDarkTheme()) Color(0xFF9bb1e3) else Color(0xFF325ca8), + contentDescription = null + ) + userDetails.gender?.let { + Text( + text = it, + color = if (isSystemInDarkTheme()) Color.White else Color.Black, + style = TextStyle(fontSize = 14.sp) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/UserProfileFragment.kt b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileFragment.kt similarity index 64% rename from app/src/main/java/org/mifos/mobile/ui/fragments/UserProfileFragment.kt rename to app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileFragment.kt index 8dc48a385..0ed1aa942 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/UserProfileFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileFragment.kt @@ -1,4 +1,4 @@ -package org.mifos.mobile.ui.fragments +package org.mifos.mobile.ui.user_profile import android.content.Intent import android.graphics.Bitmap @@ -6,21 +6,30 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.graphics.drawable.toBitmap import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler import dagger.hilt.android.AndroidEntryPoint import org.mifos.mobile.R import org.mifos.mobile.api.local.PreferencesHelper -import org.mifos.mobile.databinding.FragmentUserProfileBinding +import org.mifos.mobile.core.ui.theme.MifosMobileTheme import org.mifos.mobile.models.client.Client import org.mifos.mobile.models.client.Group import org.mifos.mobile.ui.activities.EditUserDetailActivity import org.mifos.mobile.ui.activities.base.BaseActivity import org.mifos.mobile.ui.fragments.base.BaseFragment import org.mifos.mobile.ui.getThemeAttributeColor -import org.mifos.mobile.utils.* -import org.mifos.mobile.viewModels.UserDetailViewModel +import org.mifos.mobile.utils.Constants +import org.mifos.mobile.utils.DateHelper +import org.mifos.mobile.utils.Network +import org.mifos.mobile.utils.TextDrawable +import org.mifos.mobile.utils.Toaster +import org.mifos.mobile.utils.UserDetailUiState import javax.inject.Inject /** @@ -29,31 +38,48 @@ import javax.inject.Inject @AndroidEntryPoint class UserProfileFragment : BaseFragment() { - private var _binding: FragmentUserProfileBinding? = null - private val binding get() = _binding!! - private val viewModel: UserDetailViewModel by viewModels() + private var userBitmap: Bitmap? = null @JvmField @Inject var preferencesHelper: PreferencesHelper? = null - private var userBitmap: Bitmap? = null private var client: Client? = null - private var sweetUIErrorHandler: SweetUIErrorHandler? = null + + private var userDetails by mutableStateOf(UserDetails()) + private var isOnline by mutableStateOf(false) + + private var bitmap by mutableStateOf(null) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - _binding = FragmentUserProfileBinding.inflate(inflater, container, false) - (activity as BaseActivity?)?.setSupportActionBar(binding.toolbar) // check this part before pushing (activity as BaseActivity?)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) - sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) if (savedInstanceState == null) { viewModel.userDetails viewModel.userImage } - return binding.root + + isOnline = Network.isConnected(context) + + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MifosMobileTheme { + bitmap?.let { + UserProfileScreen( + userDetails = userDetails, + isOnline = isOnline, + bitmap = it, + changePassword = { changePassword() }, + home = { backToHome() } + ) + } + } + } + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -67,10 +93,12 @@ class UserProfileFragment : BaseFragment() { hideProgress() showUserDetails(it.client) } + is UserDetailUiState.ShowUserImage -> { hideProgress() showUserImage(it.image) } + is UserDetailUiState.ShowError -> { hideProgress() showError(getString(it.message)) @@ -78,10 +106,6 @@ class UserProfileFragment : BaseFragment() { } } } - - binding.btnChangePassword.setOnClickListener { - changePassword() - } } override fun onSaveInstanceState(outState: Bundle) { @@ -103,40 +127,38 @@ class UserProfileFragment : BaseFragment() { * * @param client instance of [Client] which contains information about client */ - fun showUserDetails(client: Client?) { + private fun showUserDetails(client: Client?): UserDetails { this.client = client - binding.tvUserName.text = nullFieldCheck(getString(R.string.username), client?.displayName) - binding.tvAccountNumber.text = nullFieldCheck( - getString(R.string.account_number), - client?.accountNo, - ) - binding.tvActivationDate.text = nullFieldCheck( + val userName = nullFieldCheck(getString(R.string.username), client?.displayName) + val accountNumber = nullFieldCheck(getString(R.string.account_number), client?.accountNo) + val activationDate = nullFieldCheck( getString(R.string.activation_date), - DateHelper.getDateAsString(client?.activationDate), - ) - binding.tvOfficeName.text = nullFieldCheck( - getString(R.string.office_name), - client?.officeName, - ) - binding.tvClientType.text = nullFieldCheck( - getString(R.string.client_type), - client?.clientType?.name, - ) - binding.tvGroups.text = nullFieldCheck( - getString(R.string.groups), - getGroups(client?.groups), - ) - binding.tvClientClassification.text = client?.clientClassification?.name ?: "-" - binding.tvPhoneNumber.text = nullFieldCheck( - getString(R.string.phone_number), - client?.mobileNo, + DateHelper.getDateAsString(client?.activationDate) ) - if (client?.dobDate?.size != 3) { // no data entry in database for the client - binding.tvDob.text = getString(R.string.no_dob_found) + val officeName = nullFieldCheck(getString(R.string.office_name), client?.officeName) + val clientType = nullFieldCheck(getString(R.string.client_type), client?.clientType?.name) + val groups = nullFieldCheck(getString(R.string.groups), getGroups(client?.groups)) + val clientClassification = client?.clientClassification?.name ?: "-" + val phoneNumber = nullFieldCheck(getString(R.string.phone_number), client?.mobileNo) + val dob = if (client?.dobDate?.size != 3) { + getString(R.string.no_dob_found) } else { - binding.tvDob.text = DateHelper.getDateAsString(client.dobDate) + DateHelper.getDateAsString(client.dobDate) } - binding.tvGender.text = nullFieldCheck(getString(R.string.gender), client?.gender?.name) + val gender = nullFieldCheck(getString(R.string.gender), client?.gender?.name) + userDetails = UserDetails( + userName, + accountNumber, + activationDate, + officeName, + clientType, + groups, + clientClassification, + phoneNumber, + dob, + gender + ) + return userDetails } private fun nullFieldCheck(field: String, value: String?): String { @@ -172,12 +194,14 @@ class UserProfileFragment : BaseFragment() { * * @param bitmap User Image */ - fun showUserImage(bitmap: Bitmap?) { + private fun showUserImage(bitmap: Bitmap?): Bitmap? { activity?.runOnUiThread { userBitmap = bitmap if (userBitmap == null) { val textDrawable = TextDrawable.builder() .beginConfig() + .width(100) + .height(100) .toUpperCase() .endConfig() .buildRound( @@ -193,31 +217,29 @@ class UserProfileFragment : BaseFragment() { ?.substring(0, 1), requireContext().getThemeAttributeColor(R.attr.colorPrimaryVariant), ) - binding.ivProfile.setImageDrawable(textDrawable) + this.bitmap = textDrawable.toBitmap() } else { - binding.ivProfile.setImageBitmap(bitmap) + this.bitmap = bitmap } } + return this.bitmap } - fun changePassword() { + private fun changePassword() { startActivity(Intent(context, EditUserDetailActivity::class.java)) } + private fun backToHome() { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + /** * It is called whenever any error occurs while executing a request * * @param message Error message that tells the user about the problem. */ fun showError(message: String?) { - Toaster.show(binding.root, message) - sweetUIErrorHandler?.showSweetCustomErrorUI( - getString(R.string.error_fetching_user_profile), - R.drawable.ic_error_black_24dp, - binding.appBarLayout, - binding.layoutError.root, - ) - binding.fabEdit.visibility = View.GONE + Toaster.show(view, message) } fun showProgress() { @@ -228,12 +250,6 @@ class UserProfileFragment : BaseFragment() { hideProgressBar() } - override fun onDestroyView() { - super.onDestroyView() - hideProgress() - _binding = null - } - companion object { @JvmStatic fun newInstance(): UserProfileFragment { diff --git a/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt new file mode 100644 index 000000000..2c804913b --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt @@ -0,0 +1,101 @@ +package org.mifos.mobile.ui.user_profile + +import android.content.res.Configuration +import android.graphics.Bitmap +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Divider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.mifos.mobile.R +import org.mifos.mobile.core.ui.component.MifosUserImage +import org.mifos.mobile.core.ui.component.NoInternet +import org.mifos.mobile.core.ui.component.UserProfileField +import org.mifos.mobile.core.ui.component.UserProfileTopBar + +/** + * @author pratyush + * @since 20/12/2023 + */ + +@Composable +fun UserProfileScreen( + userDetails: UserDetails, + changePassword: () -> Unit, + isOnline: Boolean, + bitmap: Bitmap, + home: () -> Unit +) { + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + UserProfileTopBar(home = home, text = R.string.user_details) + + if (!isOnline) { + NoInternet( + icon = R.drawable.ic_error_black_24dp, + error = R.string.error_fetching_user_profile + ) + return + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp, bottom = 20.dp), + horizontalArrangement = Arrangement.Center + ) { + MifosUserImage(isDarkTheme = isSystemInDarkTheme(), bitmap = bitmap) + } + Divider(color = Color(0xFF8E9099)) + userDetails.userName?.let { UserProfileField(label = R.string.username, value = it) } + userDetails.accountNumber?.let { + UserProfileField( + label = R.string.account_number, value = it + ) + } + userDetails.activationDate?.let { + UserProfileField( + label = R.string.activation_date, value = it + ) + } + userDetails.officeName?.let { UserProfileField(label = R.string.office_name, value = it) } + userDetails.groups?.let { UserProfileField(label = R.string.groups, value = it) } + userDetails.clientType?.let { UserProfileField(label = R.string.client_type, value = it) } + userDetails.clientClassification?.let { + UserProfileField( + label = R.string.client_classification, value = it + ) + } + UserProfileField(text = R.string.change_password, + icon = R.drawable.ic_keyboard_arrow_right_black_24dp, + onClick = { changePassword.invoke() }) + + UserProfileDetails(userDetails = userDetails) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +fun UserProfileScreenPreview() { + UserProfileScreen( + UserDetails(), + {}, + true, + Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), + {} + ) +} \ No newline at end of file diff --git a/app/src/test/java/org/mifos/mobile/viewModels/UserDetailViewModelTest.kt b/app/src/test/java/org/mifos/mobile/viewModels/UserDetailViewModelTest.kt index e37cdf0b5..bfb5bb1ff 100644 --- a/app/src/test/java/org/mifos/mobile/viewModels/UserDetailViewModelTest.kt +++ b/app/src/test/java/org/mifos/mobile/viewModels/UserDetailViewModelTest.kt @@ -17,6 +17,7 @@ import org.mifos.mobile.api.local.PreferencesHelper import org.mifos.mobile.models.client.Client import org.mifos.mobile.repositories.HomeRepositoryImp import org.mifos.mobile.repositories.UserDetailRepositoryImp +import org.mifos.mobile.ui.user_profile.UserDetailViewModel import org.mifos.mobile.util.RxSchedulersOverrideRule import org.mifos.mobile.utils.UserDetailUiState import org.mockito.Mock diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt new file mode 100644 index 000000000..e249affaa --- /dev/null +++ b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt @@ -0,0 +1,41 @@ +package org.mifos.mobile.core.ui.component + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp + +/** + * @author pratyush + * @since 20/12/2023 + */ + +@Composable +fun MifosUserImage( + isDarkTheme: Boolean, + bitmap: Bitmap +) { + val backgroundColor = if (isDarkTheme) { + Color(0xFF9bb1e3) + } else { + Color(0xFF325ca8) + } + Image( + modifier = Modifier + .size(100.dp) + .clip(CircleShape) + .background(backgroundColor), + bitmap = bitmap.asImageBitmap(), + contentDescription = "Profile Image", + contentScale = ContentScale.Crop, + ) + +} \ No newline at end of file diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt new file mode 100644 index 000000000..ffa83bf9d --- /dev/null +++ b/core/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt @@ -0,0 +1,51 @@ +package org.mifos.mobile.core.ui.component + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * @author pratyush + * @since 20/12/2023 + */ + +@Composable +fun NoInternet( + icon: Int, + error: Int +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier + .size(100.dp) + .padding(bottom = 12.dp), + painter = painterResource(id = icon), + contentDescription = null, + tint = if (isSystemInDarkTheme()) Color.White else Color.Black + ) + + Text( + text = stringResource(id = error), + style = TextStyle(fontSize = 20.sp), + color = if (isSystemInDarkTheme()) Color.White else Color.Black + ) + } +} \ No newline at end of file diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt new file mode 100644 index 000000000..6720fd44c --- /dev/null +++ b/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt @@ -0,0 +1,74 @@ +package org.mifos.mobile.core.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * @author pratyush + * @since 20/12/2023 + */ + +@Composable +fun UserProfileField( + text: Int, icon: Int, onClick: (() -> Unit) +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(top = 16.dp, bottom = 16.dp, start = 8.dp, end = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = text), + color = Color(0xFF8E9099), + style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold), + ) + Icon( + painter = painterResource(id = icon), + contentDescription = null, + tint = if (isSystemInDarkTheme()) Color.White else Color.Black, + ) + } + Divider(color = if (isSystemInDarkTheme()) Color(0xFF8E9099) else Color.Black) +} + +@Composable +fun UserProfileField( + label: Int, value: String +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 16.dp, start = 8.dp, end = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = label), + color = Color(0xFF8E9099), + style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold), + ) + Text( + text = value, + color = if (isSystemInDarkTheme()) Color.White else Color.Black, + style = TextStyle(fontSize = 14.sp), + ) + } + Divider(color = Color(0xFF8E9099)) +} \ No newline at end of file diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt new file mode 100644 index 000000000..60aa7b5a8 --- /dev/null +++ b/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt @@ -0,0 +1,86 @@ +package org.mifos.mobile.core.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * @author pratyush + * @since 20/12/2023 + */ + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UserProfileTopBar( + home : () -> Unit, + text: Int, +) { + TopAppBar( + modifier = Modifier.height(64.dp), + title = { + Row( + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = text), + color = if (isSystemInDarkTheme()) Color.White else Color.Black, + style = TextStyle(fontSize = 24.sp), + ) + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null, + tint = if (isSystemInDarkTheme()) Color.White else Color.Black, + ) + } + }, + navigationIcon = { + Column( + modifier = Modifier + .fillMaxHeight() + .padding(start = 8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = null, + modifier = Modifier.clickable(onClick = { + home.invoke() + }), + tint = if (isSystemInDarkTheme()) Color.White else Color.Black, + ) + } + }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = if (isSystemInDarkTheme()) Color( + 0xFF1B1B1F + ) else Color(0xFFFEFBFF) + ) + ) +} From 938f83316898492b0b585e0d1db79853a1c6a151 Mon Sep 17 00:00:00 2001 From: kmanikanta335 <118070186+kmanikanta335@users.noreply.github.com> Date: Sat, 11 Nov 2023 22:39:42 +0530 Subject: [PATCH 08/10] fix #1075: ui layout for loan application fragment fix #1075: ui layout for loan application fragment --- .../ui/fragments/LoanApplicationFragment.kt | 38 +++++++++++-------- .../layout/fragment_add_loan_application.xml | 4 +- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/LoanApplicationFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/LoanApplicationFragment.kt index 9b616a401..0a8cfb0a2 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/LoanApplicationFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/LoanApplicationFragment.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.mifos.mobile.R @@ -61,7 +62,7 @@ class LoanApplicationFragment : BaseFragment() { private var submittedDate: String? = null private var isDisbursementDate = false private var isLoanUpdatePurposesInitialization = true - + private var sweetUIErrorHandler: SweetUIErrorHandler? = null /** * Used when we want to apply for a Loan * @@ -114,6 +115,7 @@ class LoanApplicationFragment : BaseFragment() { savedInstanceState: Bundle?, ): View { _binding = FragmentAddLoanApplicationBinding.inflate(inflater, container, false) + sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) showUserInterface() if (savedInstanceState == null) { loadLoanTemplate() @@ -197,7 +199,7 @@ class LoanApplicationFragment : BaseFragment() { setTvDisbursementOnDate() } - llError.ivStatus.setOnClickListener { + binding.layoutError.btnTryAgain.setOnClickListener { onRetry() } } @@ -322,11 +324,16 @@ class LoanApplicationFragment : BaseFragment() { * Retries to fetch [LoanTemplate] by calling `loadLoanTemplate()` */ private fun onRetry() { - binding.apply { - llError.root.visibility = View.GONE - llAddLoan.visibility = View.VISIBLE + if (Network.isConnected(context)) { + sweetUIErrorHandler?.hideSweetErrorLayoutUI( + binding.viewFlipper, + binding.layoutError.root, + ) + loadLoanTemplate() + } else { + Toaster.show(binding.viewFlipper, getString(R.string.internet_not_connected)) } - loadLoanTemplate() + } /** @@ -546,16 +553,17 @@ class LoanApplicationFragment : BaseFragment() { * @param message Error message that tells the user about the problem. */ fun showError(message: String?) { - with(binding) { - if (!Network.isConnected(activity)) { - llError.ivStatus.setImageResource(R.drawable.ic_error_black_24dp) - llError.tvStatus.text = getString(R.string.internet_not_connected) - llAddLoan.visibility = View.GONE - llError.root.visibility = View.VISIBLE - } else { - Toaster.show(root, message) - } + if (!Network.isConnected(context)) { + sweetUIErrorHandler?.showSweetNoInternetUI(binding.viewFlipper, binding.layoutError.root) + } else { + sweetUIErrorHandler?.showSweetErrorUI( + message, + binding.viewFlipper, + binding.layoutError.root, + ) + Toaster.show(binding.viewFlipper, message) } + hideProgress() } fun showProgress() { diff --git a/app/src/main/res/layout/fragment_add_loan_application.xml b/app/src/main/res/layout/fragment_add_loan_application.xml index a516de8eb..c776a06cd 100644 --- a/app/src/main/res/layout/fragment_add_loan_application.xml +++ b/app/src/main/res/layout/fragment_add_loan_application.xml @@ -181,8 +181,8 @@ From d25746d66c8caf61736828bcdc3a4bc1ad16cd65 Mon Sep 17 00:00:00 2001 From: Ayush Keshri Date: Tue, 14 Nov 2023 22:09:38 +0530 Subject: [PATCH 09/10] Fix #2432: fixed about us landscape orientation ui --- .../mifos/mobile/ui/about/AboutUsFragment.kt | 2 + .../mifos/mobile/ui/about/AboutUsScreen.kt | 74 ++++++++++--------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt index b81b63c8b..e27f0a72a 100644 --- a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt @@ -1,5 +1,6 @@ package org.mifos.mobile.ui.about +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.net.Uri @@ -30,6 +31,7 @@ class AboutUsFragment : BaseFragment() { private val viewModel: AboutUsViewModel by viewModels() + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt index 00a88b1bc..746a95126 100644 --- a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt @@ -17,49 +17,57 @@ import java.util.* @SuppressWarnings("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun AboutUsScreen(viewModel: AboutUsViewModel) { - Column( + + + LazyColumn( modifier = Modifier .fillMaxSize() .padding(16.dp), ) { - Spacer(modifier = Modifier.height(48.dp)) - AboutUsHeader() - LazyColumn { - items(viewModel.aboutUsItems) { item -> - MifosItemCard( - modifier = Modifier.padding(bottom = 8.dp), - onClick = { - when (item.itemId) { - AboutUsListItemId.OFFICE_WEBSITE -> { - viewModel.navigateToItem(AboutUsListItemId.OFFICE_WEBSITE) - } - AboutUsListItemId.LICENSES -> { - viewModel.navigateToItem(AboutUsListItemId.LICENSES) - } - AboutUsListItemId.PRIVACY_POLICY -> { - viewModel.navigateToItem(AboutUsListItemId.PRIVACY_POLICY) - } - AboutUsListItemId.SOURCE_CODE -> { - viewModel.navigateToItem(AboutUsListItemId.SOURCE_CODE) - } - AboutUsListItemId.LICENSES_STRING_WITH_VALUE -> { - viewModel.navigateToItem(AboutUsListItemId.LICENSES_STRING_WITH_VALUE) - } - else -> {} + item { + Spacer(modifier = Modifier.height(48.dp)) + AboutUsHeader() + } + items(viewModel.aboutUsItems) { item -> + MifosItemCard( + modifier = Modifier.padding(bottom = 8.dp), + onClick = { + when (item.itemId) { + AboutUsListItemId.OFFICE_WEBSITE -> { + viewModel.navigateToItem(AboutUsListItemId.OFFICE_WEBSITE) } + + AboutUsListItemId.LICENSES -> { + viewModel.navigateToItem(AboutUsListItemId.LICENSES) + } + + AboutUsListItemId.PRIVACY_POLICY -> { + viewModel.navigateToItem(AboutUsListItemId.PRIVACY_POLICY) + } + + AboutUsListItemId.SOURCE_CODE -> { + viewModel.navigateToItem(AboutUsListItemId.SOURCE_CODE) + } + + AboutUsListItemId.LICENSES_STRING_WITH_VALUE -> { + viewModel.navigateToItem(AboutUsListItemId.LICENSES_STRING_WITH_VALUE) + } + + else -> {} } - ) { - item.title?.let { - AboutUsItemCard( - title = it, - subtitle = item.subtitle, - iconUrl = item.iconUrl - ) - } + } + ) { + item.title?.let { + AboutUsItemCard( + title = it, + subtitle = item.subtitle, + iconUrl = item.iconUrl + ) } } } } + } @Composable From 6b7a3345882d8041c6e456dd4c7c49786341d51c Mon Sep 17 00:00:00 2001 From: Ayush Keshri Date: Tue, 14 Nov 2023 22:43:06 +0530 Subject: [PATCH 10/10] implemented requested changes --- .../java/org/mifos/mobile/ui/about/AboutUsFragment.kt | 9 ++++++--- .../main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt | 3 --- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt index e27f0a72a..4ca4ba372 100644 --- a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsFragment.kt @@ -41,9 +41,7 @@ class AboutUsFragment : BaseFragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { MifosMobileTheme { - Scaffold { - AboutUsScreen(viewModel) - } + AboutUsScreen(viewModel) } } } @@ -56,18 +54,23 @@ class AboutUsFragment : BaseFragment() { AboutUsListItemId.OFFICE_WEBSITE -> { startActivity(WEBSITE_LINK) } + AboutUsListItemId.LICENSES -> { startActivity(LICENSE_LINK) } + AboutUsListItemId.PRIVACY_POLICY -> { startActivity(PrivacyPolicyActivity::class.java) } + AboutUsListItemId.SOURCE_CODE -> { startActivity(SOURCE_CODE_LINK) } + AboutUsListItemId.LICENSES_STRING_WITH_VALUE -> { startActivity(OssLicensesMenuActivity::class.java) } + else -> {} } } diff --git a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt index 746a95126..b5d800a28 100644 --- a/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/about/AboutUsScreen.kt @@ -14,11 +14,8 @@ import org.mifos.mobile.core.ui.component.MifosItemCard import org.mifos.mobile.ui.enums.AboutUsListItemId import java.util.* -@SuppressWarnings("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun AboutUsScreen(viewModel: AboutUsViewModel) { - - LazyColumn( modifier = Modifier .fillMaxSize()