refactor: Redesign payment screen (#1773)

* refactor: Redesign payment screen

* resolved detekt error

* refactor : changed current theme instead of using NewUi

* resolved spotless errors
This commit is contained in:
Nagarjuna 2024-10-02 02:46:12 +05:30 committed by GitHub
parent 8bd08c248e
commit da4106fd95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 549 additions and 213 deletions

View File

@ -5,9 +5,11 @@ import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.named
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
internal fun Project.configureDetekt(extension: DetektExtension) = extension.apply {
tasks.named<Detekt>("detekt") {
jvmTarget = "17"
reports {
xml.required.set(true)
html.required.set(true)

View File

@ -12,6 +12,7 @@ package org.mifospay.core.designsystem.component
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@ -21,7 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mifospay.core.designsystem.icon.MifosIcons
import org.mifospay.core.designsystem.theme.MifosTheme
import org.mifospay.core.designsystem.theme.NewUi
@Composable
fun IconBox(
@ -33,7 +33,7 @@ fun IconBox(
onClick = onClick,
modifier = modifier,
shape = RoundedCornerShape(12.dp),
border = BorderStroke(2.dp, NewUi.onSurface.copy(alpha = 0.1f)),
border = BorderStroke(2.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)),
) {
Icon(
imageVector = icon,

View File

@ -12,6 +12,7 @@ package org.mifospay.core.designsystem.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -43,6 +44,7 @@ fun MifosScaffold(
onClick = content.onClick,
contentColor = content.contentColor,
content = content.content,
containerColor = MaterialTheme.colorScheme.primary,
)
}
},

View File

@ -9,12 +9,17 @@
*/
package org.mifospay.core.designsystem.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
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.unit.dp
@Composable
fun MifosTab(
@ -22,18 +27,25 @@ fun MifosTab(
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
selectedContentColor: Color = MaterialTheme.colorScheme.onSurface,
unselectedContentColor: Color = Color.LightGray,
selectedContentColor: Color = MaterialTheme.colorScheme.primary,
unselectedContentColor: Color = MaterialTheme.colorScheme.primaryContainer,
) {
Tab(
text = {
Text(
text = text,
color = MaterialTheme.colorScheme.onSurface,
color = if (selected) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onSurface
},
)
},
selected = selected,
modifier = modifier,
modifier = modifier
.clip(RoundedCornerShape(25.dp))
.background(if (selected) selectedContentColor else unselectedContentColor)
.padding(horizontal = 20.dp),
selectedContentColor = selectedContentColor,
unselectedContentColor = unselectedContentColor,
onClick = onClick,

View File

@ -14,6 +14,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
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.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -57,7 +58,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.mifospay.core.designsystem.theme.MifosTheme
import org.mifospay.core.designsystem.theme.NewUi
@Composable
fun MfOutlinedTextField(
@ -84,18 +84,15 @@ fun MfOutlinedTextField(
},
singleLine = singleLine,
trailingIcon = trailingIcon,
keyboardActions =
KeyboardActions {
keyboardActions = KeyboardActions {
onKeyboardActions?.invoke()
},
keyboardOptions = keyboardOptions,
colors =
OutlinedTextFieldDefaults.colors(
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.onSurface,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
),
textStyle =
LocalDensity.current.run {
textStyle = LocalDensity.current.run {
TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface)
},
)
@ -118,8 +115,7 @@ fun MfPasswordTextField(
onValueChange = onPasswordChange,
label = { Text(label) },
isError = isError,
visualTransformation =
if (isPasswordVisible) {
visualTransformation = if (isPasswordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
@ -157,14 +153,12 @@ fun MifosOutlinedTextField(
onValueChange = onValueChange,
label = { Text(stringResource(id = label)) },
modifier = modifier,
leadingIcon =
if (icon != null) {
leadingIcon = if (icon != null) {
{
Image(
painter = painterResource(id = icon),
contentDescription = null,
colorFilter =
ColorFilter.tint(
colorFilter = ColorFilter.tint(
MaterialTheme.colorScheme.onSurface,
),
)
@ -175,13 +169,11 @@ fun MifosOutlinedTextField(
trailingIcon = trailingIcon,
maxLines = maxLines,
singleLine = singleLine,
colors =
OutlinedTextFieldDefaults.colors(
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.onSurface,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
),
textStyle =
LocalDensity.current.run {
textStyle = LocalDensity.current.run {
TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
@ -209,6 +201,9 @@ fun MifosTextField(
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
trailingIcon: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
indicatorColor: Color? = null,
) {
var isFocused by rememberSaveable { mutableStateOf(false) }
@ -232,31 +227,56 @@ fun MifosTextField(
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
cursorBrush = SolidColor(NewUi.primaryColor),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = { innerTextField ->
Column {
Text(
text = label,
color = NewUi.primaryColor,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.align(alignment = Alignment.Start),
)
Spacer(modifier = Modifier.height(5.dp))
innerTextField()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
if (leadingIcon != null) {
leadingIcon()
}
Spacer(modifier = Modifier.height(5.dp))
HorizontalDivider(
thickness = 1.dp,
color = if (isFocused) {
NewUi.secondaryColor
} else {
NewUi.onSurface.copy(alpha = 0.05f)
},
)
Box(modifier = Modifier.weight(1f)) {
innerTextField()
}
if (trailingIcon != null) {
trailingIcon()
}
}
indicatorColor?.let { color ->
HorizontalDivider(
thickness = 1.dp,
color = if (isFocused) {
color
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f)
},
)
} ?: run {
HorizontalDivider(
thickness = 1.dp,
color = if (isFocused) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f)
},
)
}
}
},
)
}

View File

@ -11,9 +11,9 @@ package org.mifospay.core.designsystem.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF000000)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFD9E2)
val md_theme_light_primary = Color(0xFF0673BA) // primary
val md_theme_light_onPrimary = Color(0xFFFFFFFF) // gradientOne
val md_theme_light_primaryContainer = Color(0xFFF5F5F5) // container color
val md_theme_light_onPrimaryContainer = Color(0xFF3E001D)
val md_theme_light_secondary = Color(0xFF984061)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
@ -30,7 +30,7 @@ val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF330045)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF000000)
val md_theme_light_onSurface = Color(0xFF333333) // onSurface
val md_theme_light_surfaceVariant = Color(0xFFF2DDE1)
val md_theme_light_onSurfaceVariant = Color(0xFF514347)
val md_theme_light_outline = Color(0xFF837377)

View File

@ -29,9 +29,9 @@ fun MifosScrollableTabRow(
tabContents: List<TabContent>,
pagerState: PagerState,
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.surface,
selectedContentColor: Color = MaterialTheme.colorScheme.onSurface,
unselectedContentColor: Color = Color.LightGray,
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
selectedContentColor: Color = MaterialTheme.colorScheme.primary,
unselectedContentColor: Color = MaterialTheme.colorScheme.primaryContainer,
edgePadding: Dp = 8.dp,
) {
val scope = rememberCoroutineScope()
@ -41,6 +41,8 @@ fun MifosScrollableTabRow(
containerColor = containerColor,
selectedTabIndex = pagerState.currentPage,
edgePadding = edgePadding,
indicator = {},
divider = {},
) {
tabContents.forEachIndexed { index, currentTab ->
MifosTab(

View File

@ -10,6 +10,7 @@
package org.mifospay.feature.history
import android.widget.Toast
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -21,6 +22,8 @@ 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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -46,7 +49,6 @@ import org.mifospay.core.designsystem.component.MifosBottomSheet
import org.mifospay.core.designsystem.component.MifosButton
import org.mifospay.core.designsystem.component.MifosLoadingWheel
import org.mifospay.core.designsystem.icon.MifosIcons
import org.mifospay.core.designsystem.theme.lightGrey
import org.mifospay.core.ui.EmptyContentScreen
import org.mifospay.core.ui.TransactionItemScreen
import org.mifospay.feature.transaction.detail.TransactionDetailScreen
@ -141,6 +143,8 @@ private fun HistoryScreen(
modifier = Modifier
.clickable { transactionDetailState = it },
)
HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(5.dp))
Spacer(modifier = Modifier.height(15.dp))
}
}
}
@ -178,9 +182,19 @@ private fun Chip(
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val backgroundColor = if (selected) MaterialTheme.colorScheme.primary else lightGrey
val backgroundColor = MaterialTheme.colorScheme.onPrimary
MifosButton(
modifier = modifier,
modifier = modifier.then(
if (selected) {
Modifier.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(25.dp),
)
} else {
Modifier
},
),
onClick = {
onClick()
Toast.makeText(context, label, Toast.LENGTH_SHORT).show()
@ -190,7 +204,7 @@ private fun Chip(
Text(
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 16.dp),
text = label,
color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface,
color = MaterialTheme.colorScheme.onSurface,
)
}
}

View File

@ -10,11 +10,13 @@
package org.mifospay.feature.payments
import androidx.annotation.VisibleForTesting
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.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -66,56 +68,45 @@ internal fun RequestScreenContent(
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
Row(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp),
verticalAlignment = Alignment.CenterVertically,
.padding(start = 20.dp, top = 20.dp, end = 15.dp)
.weight(1f),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp)
.weight(1f),
) {
Column {
Text(text = stringResource(id = R.string.feature_payments_virtual_payment_address_vpa))
Text(
text = vpa,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
Column(modifier = Modifier.padding(top = 10.dp)) {
Text(text = stringResource(id = R.string.feature_payments_mobile_number))
Text(
text = mobile,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = stringResource(id = R.string.feature_payments_virtual_payment_address_vpa))
Text(
text = vpa,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
IconButton(
onClick = { showQr(vpa) },
) {
Icon(
imageVector = MifosIcons.QrCode,
tint = MaterialTheme.colorScheme.primary,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = stringResource(id = R.string.feature_payments_show_code),
)
}
}
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy
(alpha = 0.05f),
)
Column(modifier = Modifier.padding(top = 10.dp)) {
Text(text = stringResource(id = R.string.feature_payments_mobile_number))
Text(
text = stringResource(id = R.string.feature_payments_show_code),
text = mobile,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}

View File

@ -10,7 +10,7 @@
-->
<resources>
<string name="feature_payments_virtual_payment_address_vpa">Virtual Payment Address (VPA)</string>
<string name="feature_payments_mobile_number">Mobile Number</string>
<string name="feature_payments_mobile_number">Phone Number</string>
<string name="feature_payments_receive">Receive</string>
<string name="feature_payments_show_code">Show code</string>

View File

@ -26,7 +26,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.mifospay.core.designsystem.theme.NewUi
@Composable
fun ProfileDetailsCard(
@ -45,7 +44,7 @@ fun ProfileDetailsCard(
),
shape = RoundedCornerShape(15.dp),
colors = CardDefaults.cardColors(
containerColor = NewUi.containerColor,
containerColor = MaterialTheme.colorScheme.primaryContainer,
),
) {
Column(
@ -79,7 +78,7 @@ fun ProfileItem(
) {
Text(
text = label,
color = NewUi.primaryColor,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
)
Spacer(modifier = Modifier.height(10.dp))
@ -92,7 +91,7 @@ fun ProfileItem(
Spacer(modifier = Modifier.height(4.dp))
HorizontalDivider(
thickness = 1.dp,
color = NewUi.onSurface.copy(alpha = 0.05f),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
)
}
}

View File

@ -15,6 +15,7 @@ import android.provider.ContactsContract
import android.util.Log
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -22,13 +23,15 @@ 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.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -42,7 +45,6 @@ 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.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
@ -53,15 +55,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.mifos.library.countrycodepicker.CountryCodePicker
import com.mifos.library.countrycodepicker.CountryCodePickerPayment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.androidx.compose.koinViewModel
import org.mifospay.core.designsystem.component.MfOutlinedTextField
import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel
import org.mifospay.core.designsystem.component.MifosButton
import org.mifospay.core.designsystem.component.MifosNavigationTopAppBar
import org.mifospay.core.designsystem.component.MifosTextField
import org.mifospay.core.designsystem.icon.MifosIcons
import org.mifospay.core.designsystem.theme.MifosBlue
import org.mifospay.core.designsystem.theme.styleMedium16sp
import org.mifospay.core.designsystem.theme.styleNormal18sp
@ -182,7 +185,11 @@ internal fun SendMoneyScreen(
}
}
Box(modifier) {
Box(
modifier
.padding(top = 5.dp)
.imePadding(),
) {
Column(Modifier.fillMaxSize()) {
if (showToolBar) {
MifosNavigationTopAppBar(
@ -190,113 +197,125 @@ internal fun SendMoneyScreen(
onNavigationClick = onBackClick,
)
}
MifosTextField(
value = amount,
label = stringResource(id = R.string.feature_send_money_amount),
onValueChange = {
amount = it
validateInfo()
},
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
leadingIcon = {
Icon(
imageVector = Icons.Default.AttachMoney,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 8.dp),
)
},
indicatorColor = MifosBlue,
)
when (sendMethodType) {
SendMethodType.VPA -> {
MifosTextField(
value = vpa,
label = stringResource(id = R.string.feature_send_money_virtual_payment_address),
onValueChange = {
vpa = it
validateInfo()
},
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
trailingIcon = {
IconButton(
onClick = { startScan() },
) {
Icon(
imageVector = MifosIcons.QrCode2,
contentDescription = "Scan QR",
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
indicatorColor = MaterialTheme.colorScheme.primary,
)
}
SendMethodType.MOBILE -> {
EnterPhoneScreen(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
initialPhoneNumber = mobileNumber,
onNumberUpdated = { _, fullPhone, valid ->
if (valid) {
mobileNumber = fullPhone
}
isValidMobileNumber = valid
validateInfo()
},
)
}
}
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier.padding(start = 20.dp, top = 20.dp),
text = stringResource(id = R.string.feature_send_money_select_transfer_method),
style = styleNormal18sp,
)
Column(modifier = Modifier.padding(16.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 20.dp),
) {
VpaMobileChip(
label = stringResource(id = R.string.feature_send_money_vpa),
selected = sendMethodType == SendMethodType.VPA,
onClick = { sendMethodType = SendMethodType.VPA },
)
Spacer(modifier = Modifier.width(8.dp))
VpaMobileChip(
label = stringResource(id = R.string.feature_send_money_mobile),
selected = sendMethodType == SendMethodType.MOBILE,
onClick = { sendMethodType = SendMethodType.MOBILE },
)
}
MfOutlinedTextField(
value = amount,
label = stringResource(id = R.string.feature_send_money_amount),
onValueChange = {
amount = it
validateInfo()
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxWidth()
.padding(16.dp),
) {
VpaMobileChip(
label = stringResource(id = R.string.feature_send_money_vpa),
selected = sendMethodType == SendMethodType.VPA,
onClick = { sendMethodType = SendMethodType.VPA },
)
when (sendMethodType) {
SendMethodType.VPA -> {
MfOutlinedTextField(
value = vpa,
label = stringResource(id = R.string.feature_send_money_virtual_payment_address),
onValueChange = {
vpa = it
validateInfo()
},
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
IconButton(
onClick = {
startScan()
},
) {
Icon(
imageVector = MifosIcons.QrCode2,
contentDescription = "Scan QR",
tint = MaterialTheme.colorScheme.primary,
)
}
},
)
}
Spacer(modifier = Modifier.width(8.dp))
VpaMobileChip(
label = stringResource(id = R.string.feature_send_money_mobile),
selected = sendMethodType == SendMethodType.MOBILE,
onClick = { sendMethodType = SendMethodType.MOBILE },
)
}
SendMethodType.MOBILE -> {
EnterPhoneScreen(
modifier =
Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
initialPhoneNumber = mobileNumber,
onNumberUpdated = { _, fullPhone, valid ->
if (valid) {
mobileNumber = fullPhone
}
isValidMobileNumber = valid
validateInfo()
},
)
}
}
Spacer(modifier = Modifier.height(16.dp))
MifosButton(
modifier =
Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onSurface,
enabled = isValidInfo,
onClick = {
if (!isValidInfo) return@MifosButton
onSubmit(
amount,
when (sendMethodType) {
SendMethodType.VPA -> vpa
SendMethodType.MOBILE -> mobileNumber
},
sendMethodType,
)
// TODO: Navigate to MakeTransferScreenRoute
},
contentPadding = PaddingValues(12.dp),
) {
Text(
stringResource(id = R.string.feature_send_money_submit),
style = styleMedium16sp.copy(color = MaterialTheme.colorScheme.surface),
MifosButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
color = MaterialTheme.colorScheme.primary,
enabled = isValidInfo,
onClick = {
if (!isValidInfo) return@MifosButton
onSubmit(
amount,
when (sendMethodType) {
SendMethodType.VPA -> vpa
SendMethodType.MOBILE -> mobileNumber
},
sendMethodType,
)
}
// TODO: Navigate to MakeTransferScreenRoute
},
contentPadding = PaddingValues(18.dp),
) {
Text(
stringResource(id = R.string.feature_send_money_submit),
style = styleMedium16sp.copy(color = MaterialTheme.colorScheme.surface),
)
}
}
@ -315,9 +334,8 @@ private fun EnterPhoneScreen(
initialPhoneNumber: String? = null,
) {
val keyboardController = LocalSoftwareKeyboardController.current
CountryCodePicker(
CountryCodePickerPayment(
modifier = modifier,
shape = RoundedCornerShape(8.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
),
@ -325,8 +343,15 @@ private fun EnterPhoneScreen(
onValueChange = { (code, phone), isValid ->
onNumberUpdated(phone, code + phone, isValid)
},
label = { Text(stringResource(id = R.string.feature_send_money_phone_number)) },
label = {
Text(
stringResource(id = R.string.feature_send_money_phone_number),
color = MaterialTheme.colorScheme.primary,
)
},
keyboardActions = KeyboardActions { keyboardController?.hide() },
indicatorColor = MaterialTheme.colorScheme.primary,
errorIndicatorColor = MaterialTheme.colorScheme.error,
)
}
@ -339,14 +364,26 @@ private fun VpaMobileChip(
) {
MifosButton(
onClick = onClick,
color = if (selected) MaterialTheme.colorScheme.primary else Color.LightGray,
color = MaterialTheme.colorScheme.onPrimary,
modifier =
modifier
.wrapContentSize()
.padding(4.dp)
.wrapContentSize(),
.then(
if (selected) {
Modifier.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(25.dp),
)
} else {
Modifier
},
),
) {
Text(
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp),
color = MaterialTheme.colorScheme.onSurface,
text = label,
)
}

View File

@ -13,12 +13,12 @@
<string name="feature_send_money_error_fetching_balance">Error fetching balance</string>
<string name="feature_send_money_not_allowed">Self Account transfer is not allowed</string>
<string name="feature_send_money_send">Send</string>
<string name="feature_send_money_select_transfer_method">Select transfer method</string>
<string name="feature_send_money_select_transfer_method">Select Method</string>
<string name="feature_send_money_vpa">VPA</string>
<string name="feature_send_money_mobile">Mobile number</string>
<string name="feature_send_money_amount">Amount</string>
<string name="feature_send_money_amount">Enter your Amount</string>
<string name="feature_send_money_virtual_payment_address">Virtual Payment Address</string>
<string name="feature_send_money_submit">Submit</string>
<string name="feature_send_money_submit">Proceed</string>
<string name="feature_send_money_please_wait">Please wait…</string>
<string name="feature_send_money_phone_number">Phone Number</string>
<string name="feature_send_money_phone_number">Enter Mobile Number</string>
</resources>

View File

@ -57,7 +57,7 @@ internal fun StandingInstructionScreen(
) {
val floatingActionButtonContent = FloatingActionButtonContent(
onClick = onNewSI,
contentColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
content = {
Icon(
imageVector = MifosIcons.Add,
@ -70,8 +70,7 @@ internal fun StandingInstructionScreen(
backPress = onBackPress,
floatingActionButtonContent = floatingActionButtonContent,
modifier = modifier,
scaffoldContent = {
},
scaffoldContent = {},
) {
when (standingInstructionsUiState) {
StandingInstructionsUiState.Empty -> {

View File

@ -15,12 +15,19 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
@ -37,6 +44,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
@ -279,6 +287,246 @@ fun CountryCodePicker(
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongMethod", "LongParameterList", "ComplexMethod")
@Composable
fun CountryCodePickerPayment(
onValueChange: (Pair<PhoneCode, String>, Boolean) -> Unit,
modifier: Modifier = Modifier,
autoDetectCode: Boolean = false,
enabled: Boolean = true,
showCountryCode: Boolean = true,
showCountryFlag: Boolean = true,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
fallbackCountry: CountryData = CountryData.UnitedStates,
showPlaceholder: Boolean = true,
includeOnly: ImmutableSet<String>? = null,
clearIcon: ImageVector? = Icons.Filled.Clear,
initialPhoneNumber: String? = null,
initialCountryIsoCode: Iso31661alpha2? = null,
initialCountryPhoneCode: PhoneCode? = null,
label: @Composable (() -> Unit)? = null,
textStyle: TextStyle = LocalTextStyle.current,
keyboardOptions: KeyboardOptions? = null,
keyboardActions: KeyboardActions? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
showError: Boolean = true,
indicatorColor: Color? = null,
errorIndicatorColor: Color? = null,
) {
val context = LocalContext.current
val focusRequester = remember { FocusRequester() }
val countryCode = autoDetectedCountryCode(
autoDetectCode = autoDetectCode,
initialPhoneNumber = initialPhoneNumber,
)
val phoneNumberWithoutCode = if (countryCode != null) {
initialPhoneNumber?.replace(countryCode, "")
} else {
initialPhoneNumber
}
var phoneNumber by remember {
mutableStateOf(
TextFieldValue(
text = phoneNumberWithoutCode.orEmpty(),
selection = TextRange(phoneNumberWithoutCode?.length ?: 0),
),
)
}
val keyboardController = LocalSoftwareKeyboardController.current
var country: CountryData by rememberSaveable(
context,
countryCode,
initialCountryPhoneCode,
initialCountryIsoCode,
) {
mutableStateOf(
configureInitialCountry(
initialCountryPhoneCode = countryCode ?: initialCountryPhoneCode,
context = context,
initialCountryIsoCode = initialCountryIsoCode,
fallbackCountry = fallbackCountry,
),
)
}
val phoneNumberTransformation = remember(country) {
PhoneNumberTransformation(country.countryIso, context)
}
val validatePhoneNumber = remember(context) { ValidatePhoneNumber(context) }
var isNumberValid: Boolean by rememberSaveable(country, phoneNumber) {
mutableStateOf(
validatePhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
),
)
}
val coroutineScope = rememberCoroutineScope()
BasicTextField(
value = phoneNumber,
onValueChange = { enteredPhoneNumber ->
val preFilteredPhoneNumber = phoneNumberTransformation.preFilter(enteredPhoneNumber)
phoneNumber = TextFieldValue(
text = preFilteredPhoneNumber,
selection = TextRange(preFilteredPhoneNumber.length),
)
isNumberValid = validatePhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
)
onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid)
},
modifier = modifier
.fillMaxWidth()
.focusable()
.autofill(
autofillTypes = listOf(AutofillType.PhoneNumberNational),
onFill = { filledPhoneNumber ->
val preFilteredPhoneNumber =
phoneNumberTransformation.preFilter(filledPhoneNumber)
phoneNumber = TextFieldValue(
text = preFilteredPhoneNumber,
selection = TextRange(preFilteredPhoneNumber.length),
)
isNumberValid = validatePhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
)
onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid)
keyboardController?.hide()
coroutineScope.launch {
focusRequester.safeFreeFocus()
}
},
focusRequester = focusRequester,
)
.focusRequester(focusRequester = focusRequester),
enabled = enabled,
textStyle = textStyle,
decorationBox = { innerTextField ->
Column {
if (label != null) {
label()
}
Spacer(modifier = Modifier.height(5.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
CountryCodeDialogWrapper(
country = country,
onCountryChange = { countryData ->
country = countryData
isNumberValid = validatePhoneNumber(
fullPhoneNumber = country.countryPhoneCode + phoneNumber.text,
)
onValueChange(
country.countryPhoneCode to phoneNumber.text,
isNumberValid,
)
},
includeOnly = includeOnly,
showCountryCode = showCountryCode,
showFlag = showCountryFlag,
textStyle = textStyle,
)
Box(modifier = Modifier.weight(1f)) {
innerTextField()
if (showPlaceholder && phoneNumber.text.isEmpty()) {
PlaceholderNumberHint(country.countryIso)
}
}
if (clearIcon != null) {
ClearIconButtonWrapper(
clearIcon = clearIcon,
colors = colors,
isNumberValid = !showError || isNumberValid,
onClearClick = {
phoneNumber = TextFieldValue("")
isNumberValid = false
onValueChange(
country.countryPhoneCode to phoneNumber.text,
isNumberValid,
)
},
)
}
}
if (showError && !isNumberValid) {
errorIndicatorColor?.let {
HorizontalDivider(
thickness = 1.dp,
color = it,
)
}
} else {
indicatorColor?.let {
HorizontalDivider(
thickness = 1.dp,
color = it,
)
}
}
}
},
interactionSource = interactionSource,
visualTransformation = phoneNumberTransformation,
keyboardOptions = keyboardOptions ?: KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Phone,
autoCorrect = true,
imeAction = ImeAction.Done,
),
keyboardActions = keyboardActions ?: KeyboardActions(
onDone = {
keyboardController?.hide()
coroutineScope.launch {
focusRequester.safeFreeFocus()
}
},
),
singleLine = true,
)
}
@Composable
fun CountryCodeDialogWrapper(
country: CountryData,
onCountryChange: (CountryData) -> Unit,
includeOnly: ImmutableSet<String>?,
showCountryCode: Boolean,
showFlag: Boolean,
textStyle: TextStyle,
) {
CountryCodeDialog(
selectedCountry = country,
includeOnly = includeOnly,
onCountryChange = onCountryChange,
showCountryCode = showCountryCode,
showFlag = showFlag,
textStyle = textStyle,
)
}
@Composable
fun ClearIconButtonWrapper(
clearIcon: ImageVector,
colors: TextFieldColors,
isNumberValid: Boolean,
onClearClick: () -> Unit,
) {
ClearIconButton(
imageVector = clearIcon,
colors = colors,
isNumberValid = isNumberValid,
onClick = onClearClick,
)
}
private fun configureInitialCountry(
initialCountryPhoneCode: PhoneCode?,
context: Context,

View File

@ -53,6 +53,9 @@ class PhoneNumberTransformation(countryCode: String, context: Context) : VisualT
@Suppress("AvoidMutableCollections", "AvoidVarsExceptWithDelegate")
private fun reformat(s: CharSequence, cursor: Int): Transformation {
if (s.isEmpty()) {
return Transformation("", listOf(0), listOf(0))
}
phoneNumberFormatter.clear()
val curIndex = cursor - 1
@ -79,17 +82,24 @@ class PhoneNumberTransformation(countryCode: String, context: Context) : VisualT
val originalToTransformed = mutableListOf<Int>()
val transformedToOriginal = mutableListOf<Int>()
var specialCharsCount = 0
formatted?.forEachIndexed { index, char ->
if (!PhoneNumberUtils.isNonSeparator(char)) {
specialCharsCount++
} else {
originalToTransformed.add(index)
if (formatted != null) {
formatted?.forEachIndexed { index, char ->
if (!PhoneNumberUtils.isNonSeparator(char)) {
specialCharsCount++
} else {
originalToTransformed.add(index)
}
transformedToOriginal.add(index - specialCharsCount)
}
transformedToOriginal.add(index - specialCharsCount)
originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(1) ?: 0)
transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(1) ?: 0)
} else {
originalToTransformed.add(0)
transformedToOriginal.add(0)
}
if (transformedToOriginal.any { it < 0 }) {
transformedToOriginal.replaceAll { if (it < 0) 0 else it }
}
originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(1) ?: 0)
transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(1) ?: 0)
return Transformation(formatted, originalToTransformed, transformedToOriginal)
}

View File

@ -1,4 +1,4 @@
package: name='org.mifospay' versionCode='1' versionName='0.0.4-beta.0.6' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14'
package: name='org.mifospay' versionCode='1' versionName='0.0.1-beta.0.836' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14'
sdkVersion:'26'
targetSdkVersion:'34'
uses-permission: name='android.permission.INTERNET'