moved savedcards to feature module (#1660)

This commit is contained in:
Aditya Kumdale 2024-06-17 22:41:54 +05:30 committed by GitHub
parent a7787c2601
commit a6bf5f3da4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 989 additions and 0 deletions

1
feature/savedcards/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,13 @@
plugins {
alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose)
}
android {
namespace = "org.mifospay.savedcards"
}
dependencies {
implementation(projects.core.data)
implementation(projects.mifospay)
}

View File

21
feature/savedcards/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package org.mifospay.savedcards
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.mifospay.savedcards.test", appContext.packageName)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,227 @@
package org.mifospay.feature.savedcards
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mifospay.core.model.entity.savedcards.Card
import org.mifospay.R
import org.mifospay.common.CreditCardUtils
import org.mifospay.core.designsystem.component.MifosBottomSheet
import org.mifospay.core.designsystem.component.MifosButton
import org.mifospay.core.designsystem.component.MifosOutlinedTextField
import org.mifospay.core.designsystem.utils.ExpirationDateMask
import java.util.Calendar
@Composable
fun AddCardDialogSheet(
cancelClicked: () -> Unit,
addClicked: (Card) -> Unit,
onDismiss: () -> Unit
) {
MifosBottomSheet(
content = {
AddCardDialogSheetContent(
cancelClicked = cancelClicked,
addClicked = addClicked
)
},
onDismiss = onDismiss
)
}
@Composable
fun AddCardDialogSheetContent(cancelClicked: () -> Unit, addClicked: (Card) -> Unit) {
val context = LocalContext.current
var firstName by rememberSaveable { mutableStateOf("") }
var lastName by rememberSaveable { mutableStateOf("") }
var creditCardNumber by rememberSaveable { mutableStateOf("") }
var expiration by rememberSaveable { mutableStateOf("") }
var cvv by rememberSaveable { mutableStateOf("") }
var firstNameValidator by rememberSaveable { mutableStateOf<String?>(null) }
var lastNameValidator by rememberSaveable { mutableStateOf<String?>(null) }
var creditCardNumberValidator by rememberSaveable { mutableStateOf<String?>(null) }
var expirationValidator by rememberSaveable { mutableStateOf<String?>(null) }
var cvvValidator by rememberSaveable { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = firstName) {
firstNameValidator = when {
firstName.trim().isEmpty() -> context.getString(R.string.all_fields_required)
else -> null
}
}
LaunchedEffect(key1 = lastName) {
lastNameValidator = when {
lastName.trim().isEmpty() -> context.getString(R.string.all_fields_required)
else -> null
}
}
LaunchedEffect(key1 = creditCardNumber) {
creditCardNumberValidator = when {
creditCardNumber.trim().isEmpty() -> context.getString(R.string.all_fields_required)
creditCardNumber.length < 16 -> context.getString(R.string.invalid_credit_card)
!CreditCardUtils.validateCreditCardNumber(creditCardNumber) -> context.getString(R.string.invalid_credit_card)
else -> null
}
}
LaunchedEffect(key1 = expiration) {
expirationValidator = when {
expiration.trim().isEmpty() -> context.getString(R.string.all_fields_required)
expiration.length < 4 -> context.getString(R.string.expiry_date_length_error)
(expiration.substring(2, 4) == Calendar.getInstance()[Calendar.YEAR].toString()
.substring(2, 4) && expiration.substring(0, 2)
.toInt() < Calendar.getInstance()[Calendar.MONTH] + 1) || expiration.substring(0, 2)
.toInt() > 12 -> context.getString(R.string.invalid_expiry_date)
else -> null
}
}
LaunchedEffect(key1 = cvv) {
cvvValidator = when {
cvv.trim().isEmpty() -> context.getString(R.string.all_fields_required)
cvv.length < 3 -> context.getString(R.string.cvv_length_error)
else -> null
}
}
fun validateAllFields(): Boolean {
when {
firstNameValidator != null -> {
Toast.makeText(context, firstNameValidator, Toast.LENGTH_SHORT).show()
return false
}
lastNameValidator != null -> {
Toast.makeText(context, lastNameValidator, Toast.LENGTH_SHORT).show()
return false
}
creditCardNumberValidator != null -> {
Toast.makeText(context, creditCardNumberValidator, Toast.LENGTH_SHORT).show()
return false
}
expirationValidator != null -> {
Toast.makeText(context, expirationValidator, Toast.LENGTH_SHORT).show()
return false
}
cvvValidator != null -> {
Toast.makeText(context, cvvValidator, Toast.LENGTH_SHORT).show()
return false
}
else -> {
return true
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
MifosOutlinedTextField(
value = firstName,
onValueChange = { firstName = it },
modifier = Modifier.fillMaxWidth(),
label = R.string.first_name
)
Spacer(modifier = Modifier.height(8.dp))
MifosOutlinedTextField(
value = lastName,
onValueChange = { lastName = it },
modifier = Modifier.fillMaxWidth(),
label = R.string.last_name
)
Spacer(modifier = Modifier.height(8.dp))
MifosOutlinedTextField(
value = creditCardNumber,
onValueChange = { if (it.length <= 16) creditCardNumber = it },
modifier = Modifier.fillMaxWidth(),
label = R.string.credit_card_number,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.NumberPassword)
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = stringResource(id = R.string.expiry_date))
Spacer(modifier = Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth()) {
MifosOutlinedTextField(
value = expiration,
onValueChange = { if (it.length <= 4) expiration = it },
modifier = Modifier.weight(1f),
label = R.string.mm_yy,
visualTransformation = ExpirationDateMask(),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.NumberPassword)
)
Spacer(modifier = Modifier.width(16.dp))
MifosOutlinedTextField(
value = cvv,
onValueChange = { if (it.length <= 3) cvv = it },
modifier = Modifier.weight(1f),
label = R.string.cvv,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.NumberPassword)
)
}
Spacer(modifier = Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
MifosButton(onClick = { cancelClicked() }) {
Text(text = stringResource(id = R.string.cancel))
}
Spacer(modifier = Modifier.width(16.dp))
MifosButton(onClick = {
val card = Card(
cardNumber = creditCardNumber,
cvv = cvv,
expiryDate = expiration,
firstName = firstName,
lastName = lastName
)
if (validateAllFields()) {
addClicked(card)
}
}) {
Text(text = stringResource(id = R.string.add))
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun AddCardDialogSheetContentPreview() {
AddCardDialogSheetContent(cancelClicked = {}, addClicked = {})
}
@Preview(showBackground = true)
@Composable
private fun AddCardDialogSheetPreview() {
AddCardDialogSheet(cancelClicked = {}, addClicked = {}, onDismiss = {})
}

View File

@ -0,0 +1,66 @@
package org.mifospay.feature.savedcards
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ViewCompositionStrategy
import dagger.hilt.android.AndroidEntryPoint
import org.mifospay.base.BaseFragment
import org.mifospay.databinding.FragmentCardsBinding
import org.mifospay.core.designsystem.theme.MifosTheme
import org.mifospay.savedcards.ui.AddCardDialog
/**
* This is the UI component of the SavedCards Architecture.
* @author ankur
* @since 21/May/2018
*/
@AndroidEntryPoint
class CardsFragment : BaseFragment() {
private lateinit var binding: FragmentCardsBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCardsBinding.inflate(inflater, container, false)
binding.composeViewCards.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MifosTheme {
Surface(modifier = Modifier.fillMaxWidth()) {
CardsScreen(
onEditCard = {
// TODO open Add card flow with edit flag to allow edit the card
}
)
}
}
}
}
return binding.root
}
private fun onAddBtnClicked() {
if (activity != null) {
activity?.startActivity(Intent(activity, AddCardDialog::class.java))
}
}
companion object {
fun newInstance(): CardsFragment {
val args = Bundle()
val fragment = CardsFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,451 @@
package org.mifospay.feature.savedcards
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.Text
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mifospay.core.model.entity.savedcards.Card
import org.mifospay.R
import org.mifospay.core.designsystem.component.MfLoadingWheel
import org.mifospay.core.designsystem.component.MifosDialogBox
import org.mifospay.core.designsystem.theme.MifosTheme
import org.mifospay.core.ui.EmptyContentScreen
import org.mifospay.core.ui.utility.AddCardChip
import org.mifospay.savedcards.presenter.CardsScreenViewModel
import org.mifospay.savedcards.presenter.CardsUiEvent
import org.mifospay.savedcards.presenter.CardsUiState
enum class CardMenuAction {
EDIT, DELETE, CANCEL
}
@Composable
fun CardsScreen(
viewModel: CardsScreenViewModel = hiltViewModel(),
onEditCard: (Card) -> Unit
) {
val cardState by viewModel.cardState.collectAsStateWithLifecycle()
val cardListUiState by viewModel.cardListUiState.collectAsStateWithLifecycle()
var showCardBottomSheet by rememberSaveable { mutableStateOf(false) }
var showConfirmDeleteDialog by rememberSaveable { mutableStateOf(false) }
var deleteCardID by rememberSaveable { mutableStateOf<Int?>(null) }
if (showCardBottomSheet) {
AddCardDialogSheet(
cancelClicked = {
showCardBottomSheet = false
},
addClicked = {
showCardBottomSheet = false
viewModel.addCard(it)
},
onDismiss = {
showCardBottomSheet = false
}
)
}
MifosDialogBox(
showDialogState = showConfirmDeleteDialog,
onDismiss = { showConfirmDeleteDialog = false },
title = R.string.delete_card,
confirmButtonText = R.string.yes,
onConfirm = {
deleteCardID?.let { viewModel.deleteCard(it) }
showConfirmDeleteDialog = false
},
dismissButtonText = R.string.no,
message = R.string.confirm_delete_card
)
CardsScreen(
cardState = cardState,
cardListUiState = cardListUiState,
onEditCard = onEditCard,
onDeleteCard = {
showConfirmDeleteDialog = true
deleteCardID = it.id
},
onAddBtn = { showCardBottomSheet = true },
updateQuery = {
viewModel.updateSearchQuery(it)
}
)
}
@Composable
fun CardsScreen(
cardState: CardsUiState,
cardListUiState: CardsUiState,
onEditCard: (Card) -> Unit,
onDeleteCard: (Card) -> Unit,
onAddBtn: () -> Unit,
updateQuery: (String) -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
when (cardState) {
CardsUiState.Loading -> {
MfLoadingWheel(
contentDesc = stringResource(R.string.loading),
backgroundColor = Color.White
)
}
is CardsUiState.Empty -> {
NoCardAddCardsScreen(onAddBtn)
}
is CardsUiState.Error -> {
EmptyContentScreen(
modifier = Modifier,
title = stringResource(id = R.string.error_oops),
subTitle = stringResource(id = R.string.unexpected_error_subtitle),
iconTint = Color.Black,
iconImageVector = Icons.Rounded.Info
)
}
is CardsUiState.CreditCardForm -> {
CardsScreenContent(
cardList = (cardListUiState as CardsUiState.CreditCardForm).cards,
onAddBtn = onAddBtn,
onDeleteCard = onDeleteCard,
onEditCard = onEditCard,
updateQuery = updateQuery
)
}
is CardsUiState.Success -> {
when (cardState.cardsUiEvent) {
CardsUiEvent.CARD_ADDED_SUCCESSFULLY -> {
Toast.makeText(
LocalContext.current,
stringResource(id = R.string.card_added_successfully),
Toast.LENGTH_SHORT
).show()
}
CardsUiEvent.CARD_UPDATED_SUCCESSFULLY -> {
Toast.makeText(
LocalContext.current,
stringResource(id = R.string.card_updated_successfully),
Toast.LENGTH_SHORT
).show()
}
CardsUiEvent.CARD_DELETED_SUCCESSFULLY -> {
Toast.makeText(
LocalContext.current,
stringResource(id = R.string.card_deleted_successfully),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
}
@Composable
fun CardsScreenContent(
cardList: List<Card>,
onEditCard: (Card) -> Unit,
onDeleteCard: (Card) -> Unit,
onAddBtn: () -> Unit,
updateQuery: (String) -> Unit
) {
val query by rememberSaveable { mutableStateOf("") }
Box(
modifier = Modifier
) {
Column {
SearchBarScreen(
query = query,
onQueryChange = { q ->
updateQuery(q)
},
onSearch = {},
onClearQuery = { updateQuery("") }
)
CardsList(
cards = cardList,
onMenuItemClick = { card, menuItem ->
when (menuItem) {
CardMenuAction.EDIT -> onEditCard(card)
CardMenuAction.DELETE -> onDeleteCard(card)
CardMenuAction.CANCEL -> Unit
}
}
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.align(Alignment.BottomCenter)
.background(color = Color.White)
) {
AddCardChip(
modifier = Modifier.align(Alignment.Center),
onAddBtn = onAddBtn,
text = R.string.add_cards,
btnText = R.string.add_cards
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBarScreen(
query: String,
onQueryChange: (String) -> Unit,
onSearch: (String) -> Unit,
onClearQuery: () -> Unit
) {
SearchBar(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 16.dp),
query = query,
onQueryChange = onQueryChange,
onSearch = onSearch,
active = false,
onActiveChange = { },
placeholder = {
Text(text = stringResource(R.string.search))
},
leadingIcon = {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = stringResource(R.string.search)
)
},
trailingIcon = {
IconButton(
onClick = onClearQuery
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close)
)
}
}
) {}
}
@Composable
fun CardsList(
cards: List<Card>,
onMenuItemClick: (Card, CardMenuAction) -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
items(cards) { card ->
CardItem(card = card) { clickedCard, menuItem ->
onMenuItemClick(clickedCard, menuItem)
}
}
}
}
@Composable
fun CardItem(card: Card, onMenuItemClick: (Card, CardMenuAction) -> Unit) {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.clickable { expanded = true }
.background(color = Color.White)
.fillMaxWidth()
.padding(10.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row {
Column {
Row {
Text(text = card.firstName, style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(6.dp))
Text(text = card.lastName, style = MaterialTheme.typography.bodyMedium)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "${stringResource(R.string.card_number)} ${card.cardNumber}",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = card.expiryDate, style = MaterialTheme.typography.bodyMedium)
}
Spacer(modifier = Modifier.height(38.dp))
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.background(MaterialTheme.colorScheme.background)
) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.edit_card)) },
onClick = {
onMenuItemClick(card, CardMenuAction.EDIT)
expanded = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.delete_card)) },
onClick = {
onMenuItemClick(card, CardMenuAction.DELETE)
expanded = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.cancel)) },
onClick = {
onMenuItemClick(card, CardMenuAction.CANCEL)
expanded = false
}
)
}
}
}
}
}
@Composable
fun NoCardAddCardsScreen(onAddBtn: () -> Unit) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = stringResource(R.string.add_cards))
AddCardChip(
modifier = Modifier,
onAddBtn = onAddBtn,
text = R.string.add_cards,
btnText = R.string.add_cards
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun CardsScreenWithSampleDataPreview() {
MifosTheme {
CardsScreen(
cardState = CardsUiState.CreditCardForm(sampleCards),
cardListUiState = CardsUiState.CreditCardForm(sampleCards),
onEditCard = {},
onDeleteCard = {},
updateQuery = {},
onAddBtn = {}
)
}
}
@Preview(showBackground = true)
@Composable
private fun CardsScreenEmptyPreview() {
MifosTheme {
CardsScreen(
cardState = CardsUiState.Empty,
cardListUiState = CardsUiState.CreditCardForm(sampleCards),
onEditCard = {},
onDeleteCard = {},
updateQuery = {},
onAddBtn = {}
)
}
}
@Preview(showBackground = true)
@Composable
private fun CardsScreenErrorPreview() {
MifosTheme {
CardsScreen(
cardState = CardsUiState.Error,
cardListUiState = CardsUiState.CreditCardForm(sampleCards),
onEditCard = {},
onDeleteCard = {},
updateQuery = {},
onAddBtn = {}
)
}
}
@Preview(showBackground = true)
@Composable
private fun CardsScreenLoadingPreview() {
MifosTheme {
CardsScreen(
cardState = CardsUiState.Loading,
cardListUiState = CardsUiState.CreditCardForm(sampleCards),
onEditCard = {},
onDeleteCard = {},
updateQuery = {},
onAddBtn = {}
)
}
}
val sampleCards = List(7) { index ->
Card(
cardNumber = "**** **** **** ${index + 1000}",
cvv = "${index + 100}",
expiryDate = "$index /0$index/202$index",
firstName = "ABC",
lastName = " XYZ",
id = index
)
}

View File

@ -0,0 +1,162 @@
package org.mifospay.feature.savedcards
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.mifospay.core.model.entity.savedcards.Card
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import org.mifospay.core.data.base.UseCase
import org.mifospay.core.data.base.UseCaseHandler
import org.mifospay.core.data.domain.usecase.savedcards.AddCard
import org.mifospay.core.data.domain.usecase.savedcards.DeleteCard
import org.mifospay.core.data.domain.usecase.savedcards.EditCard
import org.mifospay.core.data.domain.usecase.savedcards.FetchSavedCards
import org.mifospay.data.local.LocalRepository
import javax.inject.Inject
@HiltViewModel
class CardsScreenViewModel @Inject constructor(
private val mUseCaseHandler: UseCaseHandler,
private val mLocalRepository: LocalRepository,
private val addCardUseCase: AddCard,
private val fetchSavedCardsUseCase: FetchSavedCards,
private val editCardUseCase: EditCard,
private val deleteCardUseCase: DeleteCard
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
private val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
private val _cardState = MutableStateFlow<CardsUiState>(CardsUiState.Loading)
val cardState: StateFlow<CardsUiState> = _cardState.asStateFlow()
init {
fetchSavedCards()
}
val cardListUiState: StateFlow<CardsUiState> = searchQuery
.map { q ->
when (_cardState.value) {
is CardsUiState.CreditCardForm -> {
val cardList = (cardState.value as CardsUiState.CreditCardForm).cards
val filterCards = cardList.filter {
it.cardNumber.lowercase().contains(q.lowercase())
it.firstName.lowercase().contains(q.lowercase())
it.lastName.lowercase().contains(q.lowercase())
it.cvv.lowercase().contains(q.lowercase())
it.expiryDate.lowercase().contains(q.lowercase())
}
CardsUiState.CreditCardForm(filterCards)
}
else -> CardsUiState.CreditCardForm(arrayListOf())
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = CardsUiState.CreditCardForm(arrayListOf())
)
fun updateSearchQuery(query: String) {
_searchQuery.update { query }
}
fun fetchSavedCards() {
fetchSavedCardsUseCase.walletRequestValues = FetchSavedCards.RequestValues(
mLocalRepository.clientDetails.clientId
)
val requestValues = fetchSavedCardsUseCase.walletRequestValues
mUseCaseHandler.execute(fetchSavedCardsUseCase, requestValues,
object : UseCase.UseCaseCallback<FetchSavedCards.ResponseValue> {
override fun onSuccess(response: FetchSavedCards.ResponseValue) {
response.cardList.let { _cardState.value = CardsUiState.CreditCardForm(it) }
}
override fun onError(message: String) {
_cardState.value = CardsUiState.Error
}
})
}
fun addCard(card: Card) {
addCardUseCase.walletRequestValues = AddCard.RequestValues(
mLocalRepository.clientDetails.clientId,
card
)
val requestValues = addCardUseCase.walletRequestValues
mUseCaseHandler.execute(addCardUseCase, requestValues,
object : UseCase.UseCaseCallback<AddCard.ResponseValue> {
override fun onSuccess(response: AddCard.ResponseValue) {
_cardState.value = CardsUiState.Success(CardsUiEvent.CARD_ADDED_SUCCESSFULLY)
fetchSavedCards()
}
override fun onError(message: String) {
_cardState.value = CardsUiState.Error
}
})
}
fun editCard(card: Card) {
editCardUseCase.walletRequestValues = EditCard.RequestValues(
mLocalRepository.clientDetails.clientId.toInt(),
card
)
val requestValues = editCardUseCase.walletRequestValues
mUseCaseHandler.execute(editCardUseCase, requestValues,
object : UseCase.UseCaseCallback<EditCard.ResponseValue> {
override fun onSuccess(response: EditCard.ResponseValue) {
_cardState.value = CardsUiState.Success(CardsUiEvent.CARD_UPDATED_SUCCESSFULLY)
fetchSavedCards()
}
override fun onError(message: String) {
_cardState.value = CardsUiState.Error
}
})
}
fun deleteCard(cardId: Int) {
deleteCardUseCase.walletRequestValues = DeleteCard.RequestValues(
mLocalRepository.clientDetails.clientId.toInt(),
cardId
)
val requestValues = deleteCardUseCase.walletRequestValues
mUseCaseHandler.execute(deleteCardUseCase, requestValues,
object : UseCase.UseCaseCallback<DeleteCard.ResponseValue> {
override fun onSuccess(response: DeleteCard.ResponseValue) {
_cardState.value = CardsUiState.Success(CardsUiEvent.CARD_DELETED_SUCCESSFULLY)
fetchSavedCards()
}
override fun onError(message: String) {
_cardState.value = CardsUiState.Error
}
})
}
}
sealed interface CardsUiState {
data class CreditCardForm(
val cards: List<Card>
) : CardsUiState
data object Empty : CardsUiState
data object Error : CardsUiState
data object Loading : CardsUiState
data class Success(val cardsUiEvent: CardsUiEvent) : CardsUiState
}
enum class CardsUiEvent {
CARD_ADDED_SUCCESSFULLY,
CARD_UPDATED_SUCCESSFULLY,
CARD_DELETED_SUCCESSFULLY
}

View File

@ -0,0 +1,17 @@
package org.mifospay.savedcards
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -41,3 +41,6 @@ include(":feature:make-transfer")
include(":feature:send-money")
include(":feature:notification")
include(":feature:editpassword")
include(":feature:savedcards")