Skip to content

Commit fa8e9af

Browse files
committed
refactor(send-money): enhance payment details UI/UX
1 parent 3c31541 commit fa8e9af

File tree

4 files changed

+154
-63
lines changed

4 files changed

+154
-63
lines changed

cmp-android/prodRelease-badging.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package: name='org.mifospay' versionCode='1' versionName='2025.8.3-beta.0.4' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
1+
package: name='org.mifospay' versionCode='1' versionName='2025.8.3-beta.0.5' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
22
minSdkVersion:'26'
33
targetSdkVersion:'34'
44
uses-permission: name='android.permission.INTERNET'

core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package org.mifospay.core.designsystem.icon
1111

1212
import androidx.compose.material.icons.Icons
1313
import androidx.compose.material.icons.automirrored.filled.ArrowBack
14+
import androidx.compose.material.icons.automirrored.filled.ArrowForward
1415
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
1516
import androidx.compose.material.icons.filled.ArrowOutward
1617
import androidx.compose.material.icons.filled.AttachMoney
@@ -130,5 +131,8 @@ object MifosIcons {
130131
val Scan = Icons.Outlined.QrCodeScanner
131132
val RadioButtonUnchecked = Icons.Default.RadioButtonUnchecked
132133
val RadioButtonChecked = Icons.Filled.RadioButtonChecked
133-
val Currency = Icons.Filled.CurrencyRupee
134+
135+
val ArrowForward = Icons.AutoMirrored.Filled.ArrowForward
136+
137+
val CurrencyRupee = Icons.Filled.CurrencyRupee
134138
}

feature/send-money/src/commonMain/kotlin/org/mifospay/feature/send/money/PayeeDetailsScreen.kt

Lines changed: 122 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import androidx.compose.foundation.background
1919
import androidx.compose.foundation.layout.Arrangement
2020
import androidx.compose.foundation.layout.Box
2121
import androidx.compose.foundation.layout.Column
22+
import androidx.compose.foundation.layout.PaddingValues
2223
import androidx.compose.foundation.layout.Row
2324
import androidx.compose.foundation.layout.Spacer
25+
import androidx.compose.foundation.layout.fillMaxSize
2426
import androidx.compose.foundation.layout.fillMaxWidth
2527
import androidx.compose.foundation.layout.height
2628
import androidx.compose.foundation.layout.padding
@@ -41,13 +43,17 @@ import androidx.compose.material3.ExperimentalMaterial3Api
4143
import androidx.compose.material3.Icon
4244
import androidx.compose.material3.Text
4345
import androidx.compose.runtime.Composable
46+
import androidx.compose.runtime.LaunchedEffect
4447
import androidx.compose.runtime.getValue
48+
import androidx.compose.runtime.mutableStateOf
4549
import androidx.compose.runtime.remember
50+
import androidx.compose.runtime.setValue
4651
import androidx.compose.ui.Alignment
4752
import androidx.compose.ui.Modifier
4853
import androidx.compose.ui.draw.clip
4954
import androidx.compose.ui.focus.FocusRequester
5055
import androidx.compose.ui.focus.focusRequester
56+
import androidx.compose.ui.focus.onFocusChanged
5157
import androidx.compose.ui.graphics.graphicsLayer
5258
import androidx.compose.ui.text.TextStyle
5359
import androidx.compose.ui.text.font.FontWeight
@@ -95,40 +101,52 @@ fun PayeeDetailsScreen(
95101
)
96102
},
97103
) { paddingValues ->
98-
Column(
104+
Box(
99105
modifier = Modifier
100-
.fillMaxWidth()
101-
.padding(paddingValues)
102-
.padding(horizontal = KptTheme.spacing.lg)
103-
.verticalScroll(rememberScrollState()),
104-
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.lg),
106+
.fillMaxSize()
107+
.padding(paddingValues),
105108
) {
106-
Spacer(modifier = Modifier.height(KptTheme.spacing.md))
109+
Column(
110+
modifier = Modifier
111+
.fillMaxWidth()
112+
.padding(horizontal = KptTheme.spacing.lg)
113+
.verticalScroll(rememberScrollState()),
114+
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.lg),
115+
) {
116+
Spacer(modifier = Modifier.height(KptTheme.spacing.md))
107117

108-
PayeeProfileSection(state)
118+
PayeeProfileSection(state)
109119

110-
Spacer(modifier = Modifier.height(KptTheme.spacing.xl))
120+
Spacer(modifier = Modifier.height(KptTheme.spacing.xs))
111121

112-
PaymentDetailsSection(
113-
state = state,
114-
onAmountChange = { amount ->
115-
viewModel.trySendAction(PayeeDetailsAction.UpdateAmount(amount))
116-
},
117-
onNoteChange = { note ->
118-
viewModel.trySendAction(PayeeDetailsAction.UpdateNote(note))
119-
},
120-
)
122+
PaymentDetailsSection(
123+
state = state,
124+
onAmountChange = { amount ->
125+
viewModel.trySendAction(PayeeDetailsAction.UpdateAmount(amount))
126+
},
127+
onNoteChange = { note ->
128+
viewModel.trySendAction(PayeeDetailsAction.UpdateNote(note))
129+
},
130+
onNoteFieldFocused = {
131+
viewModel.trySendAction(PayeeDetailsAction.NoteFieldFocused)
132+
},
133+
)
121134

122-
Spacer(modifier = Modifier.height(KptTheme.spacing.xl))
135+
Spacer(modifier = Modifier.height(KptTheme.spacing.xl))
136+
}
123137

124138
ProceedButton(
125139
state = state,
126140
onProceedClick = {
127141
viewModel.trySendAction(PayeeDetailsAction.ProceedToPayment)
128142
},
143+
modifier = Modifier
144+
.align(Alignment.BottomEnd)
145+
.padding(
146+
end = KptTheme.spacing.lg,
147+
bottom = KptTheme.spacing.lg,
148+
),
129149
)
130-
131-
Spacer(modifier = Modifier.height(KptTheme.spacing.lg))
132150
}
133151
}
134152
}
@@ -236,6 +254,7 @@ private fun PaymentDetailsSection(
236254
state: PayeeDetailsState,
237255
onAmountChange: (String) -> Unit,
238256
onNoteChange: (String) -> Unit,
257+
onNoteFieldFocused: () -> Unit,
239258
modifier: Modifier = Modifier,
240259
) {
241260
Column(
@@ -283,11 +302,13 @@ private fun PaymentDetailsSection(
283302
ExpandableNoteInput(
284303
value = state.note,
285304
onValueChange = onNoteChange,
305+
onFieldFocused = onNoteFieldFocused,
286306
modifier = Modifier.wrapContentWidth(),
287307
)
288308
}
289309
}
290310

311+
// TODO improve amount validation and UI/UX
291312
@Composable
292313
private fun ExpandableAmountInput(
293314
value: String,
@@ -298,6 +319,31 @@ private fun ExpandableAmountInput(
298319
val focusRequester = remember { FocusRequester() }
299320
val displayValue = value.ifEmpty { "0" }
300321

322+
/**
323+
* Calculate width based on the display value
324+
* When showing "0" (single digit), use minimal width
325+
* When user enters decimal or additional digits, expand dynamically
326+
* Maximum amount is ₹5,00,000 (6 digits + decimal + up to 2 decimal places = max 9 characters)
327+
*/
328+
val textFieldWidth = when {
329+
displayValue == "0" -> 24.dp
330+
displayValue.length == 2 -> 32.dp
331+
displayValue.length == 3 -> 48.dp
332+
displayValue.length == 4 -> 64.dp
333+
displayValue.length == 5 -> 80.dp
334+
displayValue.length == 6 -> 96.dp
335+
displayValue.length == 7 -> 112.dp
336+
displayValue.length == 8 -> 128.dp
337+
displayValue.length == 9 -> 144.dp
338+
else -> 144.dp // Maximum width for ₹5,00,000.00
339+
}
340+
341+
LaunchedEffect(enabled) {
342+
if (enabled) {
343+
focusRequester.requestFocus()
344+
}
345+
}
346+
301347
Column(modifier = modifier) {
302348
Row(
303349
modifier = Modifier
@@ -314,59 +360,55 @@ private fun ExpandableAmountInput(
314360
verticalAlignment = Alignment.CenterVertically,
315361
horizontalArrangement = Arrangement.Center,
316362
) {
317-
Text(
318-
text = "",
319-
style = TextStyle(
320-
fontSize = 24.sp,
321-
fontWeight = FontWeight.Medium,
322-
color = KptTheme.colorScheme.onSurface,
323-
),
363+
Icon(
364+
imageVector = MifosIcons.CurrencyRupee,
365+
contentDescription = "Rupee Icon",
366+
tint = KptTheme.colorScheme.onSurface,
324367
)
325368

326369
Spacer(modifier = Modifier.width(KptTheme.spacing.sm))
327370

328371
BasicTextField(
329372
value = displayValue,
330373
onValueChange = { newValue ->
331-
val cleanValue = newValue.replace(",", "").replace(".", "")
332-
if (cleanValue.isEmpty() || cleanValue.toLongOrNull() != null) {
333-
val amount = cleanValue.toLongOrNull() ?: 0L
334-
if (amount <= 500000) {
335-
onValueChange(cleanValue)
336-
}
374+
val cleanValue = newValue.replace(",", "")
375+
if (cleanValue.isEmpty() || cleanValue.toDoubleOrNull() != null) {
376+
val amount = cleanValue.toDoubleOrNull() ?: 0.0
377+
378+
/**
379+
* Allow the input to be processed by ViewModel for error handling
380+
* The ViewModel will show error message briefly for invalid amounts
381+
*/
382+
onValueChange(cleanValue)
337383
}
338384
},
339385
enabled = enabled,
340-
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
386+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
341387
textStyle = TextStyle(
342388
fontSize = 24.sp,
343389
fontWeight = FontWeight.Medium,
344390
color = KptTheme.colorScheme.onSurface,
345391
textAlign = TextAlign.Center,
346392
),
347393
modifier = Modifier
348-
.width(
349-
when {
350-
displayValue.length <= 1 -> 24.dp
351-
displayValue.length <= 3 -> displayValue.length * 16.dp
352-
displayValue.length <= 6 -> displayValue.length * 14.dp
353-
else -> displayValue.length * 12.dp
354-
},
355-
)
394+
.width(textFieldWidth)
356395
.focusRequester(focusRequester),
357396
singleLine = true,
358397
)
359398
}
360399
}
361400
}
362401

402+
// TODO improve add note UI/UX
363403
@Composable
364404
private fun ExpandableNoteInput(
365405
value: String,
366406
onValueChange: (String) -> Unit,
407+
onFieldFocused: () -> Unit,
367408
modifier: Modifier = Modifier,
368409
) {
369410
val focusRequester = remember { FocusRequester() }
411+
var isFocused by remember { mutableStateOf(false) }
370412

371413
Column(modifier = modifier) {
372414
Row(
@@ -406,7 +448,13 @@ private fun ExpandableNoteInput(
406448
else -> 28 * 12.dp
407449
},
408450
)
409-
.focusRequester(focusRequester),
451+
.focusRequester(focusRequester)
452+
.onFocusChanged { focusState ->
453+
if (focusState.isFocused && !isFocused) {
454+
isFocused = true
455+
onFieldFocused()
456+
}
457+
},
410458
singleLine = value.length <= 28,
411459
maxLines = if (value.length > 28) 2 else 1,
412460
decorationBox = { innerTextField ->
@@ -428,33 +476,51 @@ private fun ExpandableNoteInput(
428476
}
429477
}
430478

479+
// TODO improve UI/UX of proceed button
431480
@Composable
432481
private fun ProceedButton(
433482
state: PayeeDetailsState,
434483
onProceedClick: () -> Unit,
435484
modifier: Modifier = Modifier,
436485
) {
437-
val isAmountValid = state.amount.isNotEmpty() &&
438-
state.amount.toLongOrNull() != null &&
439-
state.amount.toLong() > 0 &&
440-
!state.isAmountExceedingMax
486+
val isAmountValid = if (state.isUpiCode) {
487+
state.amount.isNotEmpty() &&
488+
state.amount.toDoubleOrNull() != null &&
489+
state.amount.toDouble() >= 0 &&
490+
!state.isAmountExceedingMax
491+
} else {
492+
state.amount.isNotEmpty() &&
493+
state.amount.toDoubleOrNull() != null &&
494+
state.amount.toDouble() > 0 &&
495+
!state.isAmountExceedingMax
496+
}
441497
val isContactValid = state.upiId.isNotEmpty() || state.phoneNumber.isNotEmpty()
498+
val isAmountPrefilled = !state.isAmountEditable
499+
val showCheckMark = isAmountValid && isContactValid && (isAmountPrefilled || state.hasNoteFieldBeenFocused)
442500

443501
Button(
444502
onClick = onProceedClick,
445503
enabled = isAmountValid && isContactValid,
446-
modifier = modifier.fillMaxWidth(),
504+
modifier = modifier.size(56.dp),
447505
colors = ButtonDefaults.buttonColors(
448-
containerColor = KptTheme.colorScheme.primary,
449-
contentColor = KptTheme.colorScheme.onPrimary,
506+
containerColor = if (isAmountValid && isContactValid) {
507+
KptTheme.colorScheme.primary
508+
} else {
509+
KptTheme.colorScheme.surfaceVariant
510+
},
511+
contentColor = if (isAmountValid && isContactValid) {
512+
KptTheme.colorScheme.onPrimary
513+
} else {
514+
KptTheme.colorScheme.onSurfaceVariant
515+
},
450516
),
451517
shape = RoundedCornerShape(KptTheme.spacing.sm),
518+
contentPadding = PaddingValues(0.dp),
452519
) {
453-
Text(
454-
text = if (state.isUpiCode) "Proceed to UPI Payment" else "Proceed to Payment",
455-
style = KptTheme.typography.titleMedium,
456-
fontWeight = FontWeight.SemiBold,
457-
modifier = Modifier.padding(vertical = KptTheme.spacing.sm),
520+
Icon(
521+
imageVector = if (showCheckMark) MifosIcons.Check else MifosIcons.ArrowForward,
522+
contentDescription = if (showCheckMark) "Proceed" else "Next",
523+
modifier = Modifier.size(32.dp),
458524
)
459525
}
460526
}

0 commit comments

Comments
 (0)