mirror of
https://github.com/openMF/mobile-wallet.git
synced 2026-02-06 11:07:02 +00:00
moved savedcards to feature module (#1660)
This commit is contained in:
parent
a7787c2601
commit
a6bf5f3da4
1
feature/savedcards/.gitignore
vendored
Normal file
1
feature/savedcards/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
13
feature/savedcards/build.gradle.kts
Normal file
13
feature/savedcards/build.gradle.kts
Normal 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)
|
||||
}
|
||||
0
feature/savedcards/consumer-rules.pro
Normal file
0
feature/savedcards/consumer-rules.pro
Normal file
21
feature/savedcards/proguard-rules.pro
vendored
Normal file
21
feature/savedcards/proguard-rules.pro
vendored
Normal 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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
4
feature/savedcards/src/main/AndroidManifest.xml
Normal file
4
feature/savedcards/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@ -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 = {})
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -41,3 +41,6 @@ include(":feature:make-transfer")
|
||||
include(":feature:send-money")
|
||||
include(":feature:notification")
|
||||
include(":feature:editpassword")
|
||||
|
||||
|
||||
include(":feature:savedcards")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user