Skip to content

Commit fb4ed20

Browse files
committed
feat(feature:send-money): implement upi pin screen
1 parent 532ff99 commit fb4ed20

File tree

13 files changed

+1052
-108
lines changed

13 files changed

+1052
-108
lines changed

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,23 @@ import org.mifospay.feature.savedcards.createOrUpdate.addEditCardScreen
7070
import org.mifospay.feature.savedcards.createOrUpdate.navigateToCardAddEdit
7171
import org.mifospay.feature.savedcards.details.cardDetailRoute
7272
import org.mifospay.feature.savedcards.details.navigateToCardDetails
73+
import org.mifospay.feature.send.money.AmountUtils
7374
import org.mifospay.feature.send.money.SendMoneyScreen
75+
import org.mifospay.feature.send.money.navigation.PAYMENT_SUCCESS_ROUTE
7476
import org.mifospay.feature.send.money.navigation.SEND_MONEY_BASE_ROUTE
7577
import org.mifospay.feature.send.money.navigation.SEND_MONEY_OPTIONS_ROUTE
7678
import org.mifospay.feature.send.money.navigation.navigateToPayeeDetailsScreen
7779
import org.mifospay.feature.send.money.navigation.navigateToPaymentProcessingScreen
7880
import org.mifospay.feature.send.money.navigation.navigateToPaymentSuccessScreen
7981
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyOptionsScreen
8082
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen
83+
import org.mifospay.feature.send.money.navigation.navigateToUpiPinScreen
8184
import org.mifospay.feature.send.money.navigation.payeeDetailsScreen
8285
import org.mifospay.feature.send.money.navigation.paymentProcessingScreen
8386
import org.mifospay.feature.send.money.navigation.paymentSuccessScreen
8487
import org.mifospay.feature.send.money.navigation.sendMoneyOptionsScreen
8588
import org.mifospay.feature.send.money.navigation.sendMoneyScreen
89+
import org.mifospay.feature.send.money.navigation.upiPinScreen
8690
import org.mifospay.feature.settings.navigation.settingsScreen
8791
import org.mifospay.feature.standing.instruction.StandingInstructionsScreen
8892
import org.mifospay.feature.standing.instruction.createOrUpdate.addEditSIScreen
@@ -324,18 +328,34 @@ internal fun MifosNavHost(
324328
navigateToPayeeDetailsScreen = navController::navigateToPayeeDetailsScreen,
325329
navigateToScanQrScreen = navController::navigateToScanQr,
326330
)
327-
331+
// Already in paise from PayeeDetailsState
328332
payeeDetailsScreen(
329333
onBackClick = navController::popBackStack,
330-
onNavigateToPaymentProcessing = { state ->
331-
navController.navigateToPaymentProcessingScreen(
334+
onNavigateToUpiPin = { state ->
335+
navController.navigateToUpiPinScreen(
332336
payeeName = state.payeeName,
333337
amount = state.amount,
334338
isUpiCode = state.isUpiCode,
339+
bankName = state.selectedAccount?.bankName ?: "Bank",
340+
accountNo = state.selectedAccount?.accountNumber ?: "1234567890123456",
341+
refId = state.refId,
335342
)
336343
},
337344
)
338345

346+
upiPinScreen(
347+
onBackClick = navController::popBackStack,
348+
onNavigateToPaymentProcessing = { payeeName, amount, isUpiCode ->
349+
// Convert rupees to paise for navigation
350+
val amountInPaise = AmountUtils.rupeesToPaise(amount)
351+
navController.navigateToPaymentProcessingScreen(
352+
payeeName = payeeName,
353+
amount = amountInPaise,
354+
isUpiCode = isUpiCode,
355+
)
356+
},
357+
)
358+
// Already in paise from PaymentProcessingViewModel
339359
paymentProcessingScreen(
340360
onPaymentComplete = { payeeName, amount, upiName, transactionTimestamp ->
341361
navController.navigateToPaymentSuccessScreen(
@@ -362,6 +382,16 @@ internal fun MifosNavHost(
362382
launchSingleTop = true
363383
}
364384
},
385+
onNavigateToSendMoneyOptions = {
386+
navController.navigateToSendMoneyOptionsScreen(
387+
navOptions {
388+
popUpTo(PAYMENT_SUCCESS_ROUTE) {
389+
inclusive = true
390+
}
391+
launchSingleTop = true
392+
},
393+
)
394+
},
365395
)
366396

367397
transferScreen(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ import androidx.compose.material.icons.filled.CurrencyRupee
2727
import androidx.compose.material.icons.filled.Delete
2828
import androidx.compose.material.icons.filled.Description
2929
import androidx.compose.material.icons.filled.Edit
30+
import androidx.compose.material.icons.filled.ExpandLess
31+
import androidx.compose.material.icons.filled.ExpandMore
3032
import androidx.compose.material.icons.filled.FlashOff
3133
import androidx.compose.material.icons.filled.FlashOn
3234
import androidx.compose.material.icons.filled.Info
3335
import androidx.compose.material.icons.filled.KeyboardArrowDown
36+
import androidx.compose.material.icons.filled.KeyboardArrowUp
3437
import androidx.compose.material.icons.filled.Person
3538
import androidx.compose.material.icons.filled.Photo
3639
import androidx.compose.material.icons.filled.PhotoLibrary
@@ -89,6 +92,9 @@ object MifosIcons {
8992
val Visibility: ImageVector = Icons.Filled.Visibility
9093
val Check: ImageVector = Icons.Default.Check
9194
val KeyboardArrowDown: ImageVector = Icons.Default.KeyboardArrowDown
95+
val KeyboardArrowUp: ImageVector = Icons.Default.KeyboardArrowUp
96+
val DropDown: ImageVector = Icons.Default.ExpandMore
97+
val DropUp: ImageVector = Icons.Default.ExpandLess
9298
val Home = Icons.Outlined.Home
9399
val HomeBoarder = Icons.Rounded.Home
94100
val Payment = Icons.Rounded.SwapHoriz

feature/send-money/src/commonMain/composeResources/values/strings.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
<string name="feature_send_money_amount_below_minimum">Amount must be at least ₹ 1</string>
5858
<string name="feature_send_money_rupee_icon">Rupee Icon</string>
5959
<string name="feature_send_money_add_note">Add note</string>
60-
<string name="feature_send_money_pay_amount">Pay %1$s</string>
60+
<string name="feature_send_money_pay_amount">Pay %1$s</string>
6161
<string name="feature_send_money_choose_account">Choose account to pay with</string>
6262
<string name="feature_send_money_bank_icon">Bank Icon</string>
6363
<string name="feature_send_money_balance">Balance:</string>
@@ -69,7 +69,6 @@
6969
<string name="feature_send_money_paying_securely">Paying securely %1$s to</string>
7070
<string name="feature_send_money_paid_to">Paid to</string>
7171

72-
<!-- Payment Success Screen -->
7372
<string name="feature_send_money_payment_success">Payment Successful</string>
7473
<string name="feature_send_money_banking_name">Banking name: %1$s</string>
7574
<string name="feature_send_money_powered_by_upi">Powered by UPI</string>
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.feature.send.money
11+
12+
import org.mifospay.core.common.CurrencyFormatter
13+
14+
/**
15+
* Utility functions for converting between paise and rupees
16+
* Navigation parameters use paise (stored as string) for precision
17+
* UI displays use rupees with 2 decimal places
18+
*/
19+
object AmountUtils {
20+
21+
/**
22+
* Converts rupees (as string) to paise (as string)
23+
* @param rupees Amount in rupees as string (e.g., "100.50")
24+
* @return Amount in paise as string (e.g., "10050")
25+
*/
26+
fun rupeesToPaise(rupees: String): String {
27+
return try {
28+
val amount = rupees.toDoubleOrNull() ?: 0.0
29+
(amount * 100).toLong().toString()
30+
} catch (e: NumberFormatException) {
31+
"0"
32+
}
33+
}
34+
35+
/**
36+
* Converts paise (as string) to rupees (as string) with 2 decimal places
37+
* @param paise Amount in paise as string (e.g., "10050")
38+
* @return Amount in rupees as string with 2 decimal places (e.g., "100.50")
39+
*/
40+
fun paiseToRupees(paise: String): String {
41+
return try {
42+
val amount = paise.toLongOrNull() ?: 0L
43+
val rupees = amount / 100.0
44+
val formatted = CurrencyFormatter.format(
45+
balance = rupees,
46+
currencyCode = "INR",
47+
maximumFractionDigits = 2,
48+
)
49+
formatted.replace("", "").trim()
50+
} catch (e: NumberFormatException) {
51+
"0.00"
52+
}
53+
}
54+
55+
/**
56+
* Formats rupees amount for UI display with proper formatting
57+
* @param rupees Amount in rupees as string (e.g., "100.50")
58+
* @return Formatted amount for UI (e.g., "₹100.50")
59+
*/
60+
fun formatRupeesForUI(rupees: String): String {
61+
return try {
62+
val amount = rupees.toDoubleOrNull() ?: 0.0
63+
return CurrencyFormatter.format(
64+
balance = amount,
65+
currencyCode = "INR",
66+
maximumFractionDigits = 2,
67+
)
68+
} catch (e: NumberFormatException) {
69+
"₹0.00"
70+
}
71+
}
72+
73+
/**
74+
* Formats paise amount for UI display by converting to rupees first
75+
* @param paise Amount in paise as string (e.g., "10050")
76+
* @return Formatted amount for UI (e.g., "₹100.50")
77+
*/
78+
fun formatPaiseForUI(paise: String): String {
79+
val rupees = paiseToRupees(paise)
80+
return formatRupeesForUI(rupees)
81+
}
82+
83+
/**
84+
* Validates if a paise amount is valid
85+
* @param paise Amount in paise as string
86+
* @return true if valid, false otherwise
87+
*/
88+
fun isValidPaise(paise: String): Boolean {
89+
return try {
90+
val amount = paise.toLongOrNull()
91+
amount != null && amount >= 0
92+
} catch (e: NumberFormatException) {
93+
false
94+
}
95+
}
96+
97+
/**
98+
* Validates if a rupees amount is valid
99+
* @param rupees Amount in rupees as string
100+
* @return true if valid, false otherwise
101+
*/
102+
fun isValidRupees(rupees: String): Boolean {
103+
return try {
104+
val amount = rupees.toDoubleOrNull()
105+
amount != null && amount >= 0
106+
} catch (e: NumberFormatException) {
107+
false
108+
}
109+
}
110+
111+
/**
112+
* Validates and formats amount input for the new input system
113+
* Ensures amount starts with single digit, allows only one decimal point
114+
* @param input Raw input string from user
115+
* @return Validated and formatted amount string
116+
*/
117+
fun validateAndFormatAmountInput(input: String): String {
118+
if (input.isEmpty()) return ""
119+
120+
val cleanInput = input.replace(",", "")
121+
122+
if (cleanInput == ".") return "0."
123+
if (cleanInput.startsWith(".")) return "0$cleanInput"
124+
125+
val parts = cleanInput.split(".")
126+
if (parts.size > 2) return parts[0] + "." + parts[1]
127+
128+
val integerPart = parts[0]
129+
val decimalPart = if (parts.size > 1) parts[1] else ""
130+
131+
if (integerPart.isEmpty()) return "0."
132+
if (integerPart.length > 6) return integerPart.take(6) + (if (parts.size > 1) ".$decimalPart" else "")
133+
if (decimalPart.length > 2) return "$integerPart.${decimalPart.take(2)}"
134+
135+
return cleanInput
136+
}
137+
138+
/**
139+
* Checks if the amount input is valid
140+
* @param input Raw input string from user
141+
* @return true if input is valid, false otherwise
142+
*/
143+
fun isValidAmountInput(input: String): Boolean {
144+
if (input.isEmpty()) return true
145+
146+
val cleanInput = input.replace(",", "")
147+
148+
if (cleanInput == "." || cleanInput == "0.") return true
149+
if (cleanInput.startsWith(".")) return false
150+
151+
val parts = cleanInput.split(".")
152+
if (parts.size > 2) return false
153+
154+
val integerPart = parts[0]
155+
val decimalPart = if (parts.size > 1) parts[1] else ""
156+
157+
if (integerPart.isEmpty()) return false
158+
if (integerPart.length > 6) return false
159+
if (decimalPart.length > 2) return false
160+
161+
val validIntegerPart = integerPart.all { it.isDigit() }
162+
val validDecimalPart = decimalPart.isEmpty() || decimalPart.all { it.isDigit() }
163+
164+
return validIntegerPart && validDecimalPart
165+
}
166+
167+
/**
168+
* Formats input amount for display
169+
* Shows only the numeric part without currency symbol
170+
* @param amount Amount in rupees as string
171+
* @return Formatted amount for display
172+
*/
173+
174+
// TODO handle edge cases for example decimal point is entered first.
175+
fun formatAmountForInput(amount: String): String {
176+
if (amount.isEmpty()) return ""
177+
178+
val cleanAmount = amount.replace(",", "")
179+
return try {
180+
val amountValue = cleanAmount.toDoubleOrNull() ?: 0.0
181+
if (amountValue == 0.0) return ""
182+
183+
val parts = amountValue.toString().split(".")
184+
val integerPart = parts[0]
185+
val decimalPart = if (parts.size > 1) parts[1] else ""
186+
187+
if (decimalPart.isEmpty() || decimalPart == "0") {
188+
integerPart
189+
} else if (decimalPart == "00") {
190+
integerPart
191+
} else if (decimalPart.endsWith("0")) {
192+
"$integerPart.${decimalPart.dropLast(1)}"
193+
} else {
194+
"$integerPart.$decimalPart"
195+
}
196+
} catch (e: NumberFormatException) {
197+
amount
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)