Skip to content

Commit 3c31541

Browse files
committed
fix(feature:send-money): fetch UPI data in payee details screen
1 parent e20f6f8 commit 3c31541

File tree

5 files changed

+368
-56
lines changed

5 files changed

+368
-56
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.3' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
1+
package: name='org.mifospay' versionCode='1' versionName='2025.8.3-beta.0.4' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
22
minSdkVersion:'26'
33
targetSdkVersion:'34'
44
uses-permission: name='android.permission.INTERNET'

core/data/src/commonMain/kotlin/org/mifospay/core/data/util/StandardUpiQrCodeProcessor.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ object StandardUpiQrCodeProcessor {
3939
}
4040

4141
val paramsString = qrData.substringAfter("upi://").substringAfter("UPI://")
42-
4342
val parts = paramsString.split("?", limit = 2)
4443
val params = if (parts.size > 1) parseParams(parts[1]) else emptyMap()
4544

46-
val payeeVpa = params["pa"] ?: throw IllegalArgumentException("Missing payee VPA (pa) in UPI QR code")
45+
val payeeVpa = params["pa"] ?: run {
46+
throw IllegalArgumentException("Missing payee VPA (pa) in UPI QR code")
47+
}
4748
val payeeName = params["pn"] ?: "Unknown"
4849

4950
val vpaParts = payeeVpa.split("@", limit = 2)

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

Lines changed: 233 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,28 @@
99
*/
1010
package org.mifospay.feature.send.money
1111

12+
import androidx.compose.animation.AnimatedVisibility
13+
import androidx.compose.animation.core.animateFloatAsState
14+
import androidx.compose.animation.core.repeatable
15+
import androidx.compose.animation.core.tween
16+
import androidx.compose.animation.fadeIn
17+
import androidx.compose.animation.fadeOut
1218
import androidx.compose.foundation.background
1319
import androidx.compose.foundation.layout.Arrangement
1420
import androidx.compose.foundation.layout.Box
1521
import androidx.compose.foundation.layout.Column
22+
import androidx.compose.foundation.layout.Row
1623
import androidx.compose.foundation.layout.Spacer
1724
import androidx.compose.foundation.layout.fillMaxWidth
1825
import androidx.compose.foundation.layout.height
1926
import androidx.compose.foundation.layout.padding
2027
import androidx.compose.foundation.layout.size
28+
import androidx.compose.foundation.layout.width
29+
import androidx.compose.foundation.layout.wrapContentWidth
2130
import androidx.compose.foundation.rememberScrollState
2231
import androidx.compose.foundation.shape.CircleShape
2332
import androidx.compose.foundation.shape.RoundedCornerShape
33+
import androidx.compose.foundation.text.BasicTextField
2434
import androidx.compose.foundation.text.KeyboardOptions
2535
import androidx.compose.foundation.verticalScroll
2636
import androidx.compose.material3.Button
@@ -29,16 +39,23 @@ import androidx.compose.material3.Card
2939
import androidx.compose.material3.CardDefaults
3040
import androidx.compose.material3.ExperimentalMaterial3Api
3141
import androidx.compose.material3.Icon
32-
import androidx.compose.material3.OutlinedTextField
3342
import androidx.compose.material3.Text
3443
import androidx.compose.runtime.Composable
3544
import androidx.compose.runtime.getValue
45+
import androidx.compose.runtime.remember
3646
import androidx.compose.ui.Alignment
3747
import androidx.compose.ui.Modifier
48+
import androidx.compose.ui.draw.clip
49+
import androidx.compose.ui.focus.FocusRequester
50+
import androidx.compose.ui.focus.focusRequester
51+
import androidx.compose.ui.graphics.graphicsLayer
52+
import androidx.compose.ui.text.TextStyle
3853
import androidx.compose.ui.text.font.FontWeight
3954
import androidx.compose.ui.text.input.KeyboardType
4055
import androidx.compose.ui.text.style.TextAlign
4156
import androidx.compose.ui.unit.dp
57+
import androidx.compose.ui.unit.sp
58+
import androidx.compose.ui.unit.times
4259
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4360
import org.koin.compose.viewmodel.koinViewModel
4461
import org.mifospay.core.designsystem.component.MifosGradientBackground
@@ -90,7 +107,7 @@ fun PayeeDetailsScreen(
90107

91108
PayeeProfileSection(state)
92109

93-
Spacer(modifier = Modifier.height(KptTheme.spacing.lg))
110+
Spacer(modifier = Modifier.height(KptTheme.spacing.xl))
94111

95112
PaymentDetailsSection(
96113
state = state,
@@ -146,17 +163,48 @@ private fun PayeeProfileSection(
146163
),
147164
contentAlignment = Alignment.Center,
148165
) {
149-
Icon(
150-
imageVector = MifosIcons.Person,
151-
contentDescription = "Payee Profile",
152-
modifier = Modifier.size(40.dp),
153-
tint = KptTheme.colorScheme.onPrimaryContainer,
154-
)
166+
if (state.payeeName.isNotEmpty() && state.payeeName != "UNKNOWN") {
167+
val firstLetter = state.payeeName
168+
.replace("%20", " ")
169+
.trim()
170+
.firstOrNull()
171+
?.uppercase()
172+
173+
if (firstLetter != null) {
174+
Text(
175+
text = firstLetter,
176+
style = KptTheme.typography.headlineLarge.copy(
177+
fontSize = 32.sp,
178+
fontWeight = FontWeight.Bold,
179+
),
180+
color = KptTheme.colorScheme.onPrimaryContainer,
181+
textAlign = TextAlign.Center,
182+
)
183+
} else {
184+
Icon(
185+
imageVector = MifosIcons.Person,
186+
contentDescription = "Payee Profile",
187+
modifier = Modifier.size(40.dp),
188+
tint = KptTheme.colorScheme.onPrimaryContainer,
189+
)
190+
}
191+
} else {
192+
Icon(
193+
imageVector = MifosIcons.Person,
194+
contentDescription = "Payee Profile",
195+
modifier = Modifier.size(40.dp),
196+
tint = KptTheme.colorScheme.onPrimaryContainer,
197+
)
198+
}
155199
}
156200

157-
if (state.payeeName.isNotEmpty()) {
201+
if (state.payeeName.isNotEmpty() && state.payeeName != "UNKNOWN") {
202+
val decodedName = state.payeeName
203+
.replace("%20", " ")
204+
.trim()
205+
158206
Text(
159-
text = state.payeeName,
207+
text = "Paying ${decodedName.uppercase()}",
160208
style = KptTheme.typography.headlineSmall,
161209
fontWeight = FontWeight.SemiBold,
162210
color = KptTheme.colorScheme.onSurface,
@@ -165,7 +213,7 @@ private fun PayeeProfileSection(
165213
}
166214

167215
val contactInfo = if (state.isUpiCode) {
168-
state.upiId
216+
"UPI ID: ${state.upiId}"
169217
} else {
170218
state.phoneNumber
171219
}
@@ -190,54 +238,191 @@ private fun PaymentDetailsSection(
190238
onNoteChange: (String) -> Unit,
191239
modifier: Modifier = Modifier,
192240
) {
193-
Card(
241+
Column(
194242
modifier = modifier.fillMaxWidth(),
195-
colors = CardDefaults.cardColors(
196-
containerColor = KptTheme.colorScheme.surface,
197-
),
198-
shape = RoundedCornerShape(KptTheme.spacing.md),
199-
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
243+
horizontalAlignment = Alignment.CenterHorizontally,
244+
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.lg),
200245
) {
201-
Column(
246+
ExpandableAmountInput(
247+
value = state.formattedAmount,
248+
onValueChange = onAmountChange,
249+
enabled = state.isAmountEditable,
250+
modifier = Modifier.wrapContentWidth(),
251+
)
252+
253+
AnimatedVisibility(
254+
visible = state.showMaxAmountMessage,
255+
enter = fadeIn(animationSpec = tween(300)),
256+
exit = fadeOut(animationSpec = tween(300)),
257+
) {
258+
val vibrationOffset by animateFloatAsState(
259+
targetValue = if (state.showMaxAmountMessage) 1f else 0f,
260+
animationSpec = repeatable(
261+
iterations = 3,
262+
animation = tween(100, delayMillis = 0),
263+
),
264+
label = "vibration",
265+
)
266+
267+
Text(
268+
text = "Amount cannot be more than ₹ 5,00,000",
269+
style = KptTheme.typography.bodySmall,
270+
color = KptTheme.colorScheme.error,
271+
modifier = Modifier
272+
.padding(top = KptTheme.spacing.xs)
273+
.graphicsLayer {
274+
translationX = if (state.showMaxAmountMessage) {
275+
(vibrationOffset * 10f * (if (vibrationOffset % 2 == 0f) 1f else -1f))
276+
} else {
277+
0f
278+
}
279+
},
280+
)
281+
}
282+
283+
ExpandableNoteInput(
284+
value = state.note,
285+
onValueChange = onNoteChange,
286+
modifier = Modifier.wrapContentWidth(),
287+
)
288+
}
289+
}
290+
291+
@Composable
292+
private fun ExpandableAmountInput(
293+
value: String,
294+
onValueChange: (String) -> Unit,
295+
enabled: Boolean,
296+
modifier: Modifier = Modifier,
297+
) {
298+
val focusRequester = remember { FocusRequester() }
299+
val displayValue = value.ifEmpty { "0" }
300+
301+
Column(modifier = modifier) {
302+
Row(
202303
modifier = Modifier
203-
.fillMaxWidth()
204-
.padding(KptTheme.spacing.lg),
205-
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.lg),
304+
.wrapContentWidth()
305+
.clip(RoundedCornerShape(KptTheme.spacing.sm))
306+
.background(
307+
color = KptTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
308+
shape = RoundedCornerShape(KptTheme.spacing.sm),
309+
)
310+
.padding(
311+
horizontal = KptTheme.spacing.md,
312+
vertical = KptTheme.spacing.sm,
313+
),
314+
verticalAlignment = Alignment.CenterVertically,
315+
horizontalArrangement = Arrangement.Center,
206316
) {
207317
Text(
208-
text = "Payment Details",
209-
style = KptTheme.typography.titleLarge,
210-
fontWeight = FontWeight.SemiBold,
211-
color = KptTheme.colorScheme.onSurface,
318+
text = "",
319+
style = TextStyle(
320+
fontSize = 24.sp,
321+
fontWeight = FontWeight.Medium,
322+
color = KptTheme.colorScheme.onSurface,
323+
),
212324
)
213325

214-
OutlinedTextField(
215-
value = state.amount,
216-
onValueChange = onAmountChange,
217-
label = { Text("Amount") },
218-
enabled = state.isAmountEditable,
326+
Spacer(modifier = Modifier.width(KptTheme.spacing.sm))
327+
328+
BasicTextField(
329+
value = displayValue,
330+
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+
}
337+
}
338+
},
339+
enabled = enabled,
219340
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
220-
modifier = Modifier.fillMaxWidth(),
221-
leadingIcon = {
222-
Icon(
223-
imageVector = MifosIcons.Currency,
224-
contentDescription = "Amount",
225-
tint = KptTheme.colorScheme.onSurfaceVariant,
341+
textStyle = TextStyle(
342+
fontSize = 24.sp,
343+
fontWeight = FontWeight.Medium,
344+
color = KptTheme.colorScheme.onSurface,
345+
textAlign = TextAlign.Center,
346+
),
347+
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+
},
226355
)
227-
},
356+
.focusRequester(focusRequester),
357+
singleLine = true,
228358
)
359+
}
360+
}
361+
}
362+
363+
@Composable
364+
private fun ExpandableNoteInput(
365+
value: String,
366+
onValueChange: (String) -> Unit,
367+
modifier: Modifier = Modifier,
368+
) {
369+
val focusRequester = remember { FocusRequester() }
229370

230-
OutlinedTextField(
231-
value = state.note,
371+
Column(modifier = modifier) {
372+
Row(
373+
modifier = Modifier
374+
.wrapContentWidth()
375+
.clip(RoundedCornerShape(KptTheme.spacing.sm))
376+
.background(
377+
color = KptTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
378+
shape = RoundedCornerShape(KptTheme.spacing.sm),
379+
)
380+
.padding(
381+
horizontal = KptTheme.spacing.md,
382+
vertical = KptTheme.spacing.sm,
383+
),
384+
verticalAlignment = Alignment.CenterVertically,
385+
) {
386+
BasicTextField(
387+
value = value,
232388
onValueChange = { newValue ->
233389
if (newValue.length <= 50) {
234-
onNoteChange(newValue)
390+
onValueChange(newValue)
391+
}
392+
},
393+
enabled = true,
394+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
395+
textStyle = TextStyle(
396+
fontSize = 16.sp,
397+
fontWeight = FontWeight.Normal,
398+
color = if (value.isEmpty()) KptTheme.colorScheme.onSurfaceVariant else KptTheme.colorScheme.onSurface,
399+
textAlign = TextAlign.Center,
400+
),
401+
modifier = Modifier
402+
.width(
403+
when {
404+
value.length <= 7 -> 7 * 12.dp
405+
value.length <= 28 -> (value.length + 1) * 12.dp
406+
else -> 28 * 12.dp
407+
},
408+
)
409+
.focusRequester(focusRequester),
410+
singleLine = value.length <= 28,
411+
maxLines = if (value.length > 28) 2 else 1,
412+
decorationBox = { innerTextField ->
413+
if (value.isEmpty()) {
414+
Text(
415+
text = "Add note",
416+
style = TextStyle(
417+
fontSize = 16.sp,
418+
fontWeight = FontWeight.Normal,
419+
color = KptTheme.colorScheme.onSurfaceVariant,
420+
textAlign = TextAlign.Center,
421+
),
422+
)
235423
}
424+
innerTextField()
236425
},
237-
placeholder = { Text("Add note") },
238-
modifier = Modifier.fillMaxWidth(),
239-
maxLines = 2,
240-
singleLine = false,
241426
)
242427
}
243428
}
@@ -249,7 +434,10 @@ private fun ProceedButton(
249434
onProceedClick: () -> Unit,
250435
modifier: Modifier = Modifier,
251436
) {
252-
val isAmountValid = state.amount.isNotEmpty() && state.amount.toDoubleOrNull() != null
437+
val isAmountValid = state.amount.isNotEmpty() &&
438+
state.amount.toLongOrNull() != null &&
439+
state.amount.toLong() > 0 &&
440+
!state.isAmountExceedingMax
253441
val isContactValid = state.upiId.isNotEmpty() || state.phoneNumber.isNotEmpty()
254442

255443
Button(

0 commit comments

Comments
 (0)