Skip to content

Commit 4f8eb56

Browse files
committed
feat(feature:send-money): add UPI QR code processor
1 parent c8e1d1a commit 4f8eb56

File tree

7 files changed

+171
-7
lines changed

7 files changed

+171
-7
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.core.data.util
11+
12+
import org.mifospay.core.model.utils.PaymentQrData
13+
import org.mifospay.core.model.utils.StandardUpiQrData
14+
15+
/**
16+
* Standard UPI QR Code Processor
17+
* Handles parsing of standard UPI QR codes according to UPI specification
18+
*/
19+
object StandardUpiQrCodeProcessor {
20+
21+
/**
22+
* Checks if the given string is a valid UPI QR code
23+
* @param qrData The QR code data string
24+
* @return true if it's a valid UPI QR code, false otherwise
25+
*/
26+
fun isValidUpiQrCode(qrData: String): Boolean {
27+
return qrData.startsWith("upi://") || qrData.startsWith("UPI://")
28+
}
29+
30+
/**
31+
* Parses a standard UPI QR code string
32+
* @param qrData The QR code data string
33+
* @return StandardUpiQrData object with parsed information
34+
* @throws IllegalArgumentException if the QR code is invalid
35+
*/
36+
fun parseUpiQrCode(qrData: String): StandardUpiQrData {
37+
if (!isValidUpiQrCode(qrData)) {
38+
throw IllegalArgumentException("Invalid UPI QR code format")
39+
}
40+
41+
val paramsString = qrData.substringAfter("upi://").substringAfter("UPI://")
42+
43+
val parts = paramsString.split("?", limit = 2)
44+
val params = if (parts.size > 1) parseParams(parts[1]) else emptyMap()
45+
46+
val payeeVpa = params["pa"] ?: throw IllegalArgumentException("Missing payee VPA (pa) in UPI QR code")
47+
val payeeName = params["pn"] ?: "Unknown"
48+
49+
val vpaParts = payeeVpa.split("@", limit = 2)
50+
val actualVpa = if (vpaParts.size == 2) payeeVpa else payeeVpa
51+
52+
return StandardUpiQrData(
53+
payeeName = payeeName,
54+
payeeVpa = actualVpa,
55+
amount = params["am"] ?: "",
56+
currency = params["cu"] ?: StandardUpiQrData.DEFAULT_CURRENCY,
57+
transactionNote = params["tn"] ?: "",
58+
merchantCode = params["mc"] ?: "",
59+
transactionReference = params["tr"] ?: "",
60+
url = params["url"] ?: "",
61+
mode = params["mode"] ?: "02",
62+
)
63+
}
64+
65+
/**
66+
* Parses URL parameters into a map
67+
* @param paramsString The parameters string
68+
* @return Map of parameter keys and values
69+
*/
70+
private fun parseParams(paramsString: String): Map<String, String> {
71+
return paramsString
72+
.split("&")
73+
.associate { param ->
74+
val keyValue = param.split("=", limit = 2)
75+
if (keyValue.size == 2) {
76+
keyValue[0] to keyValue[1]
77+
} else {
78+
param to ""
79+
}
80+
}
81+
}
82+
83+
/**
84+
* Converts StandardUpiQrData to PaymentQrData for compatibility with existing code
85+
* @param standardData Standard UPI QR data
86+
* @return PaymentQrData object
87+
* Note: clientId and accountId not available in standard UPI
88+
*/
89+
fun toPaymentQrData(standardData: StandardUpiQrData): PaymentQrData {
90+
return PaymentQrData(
91+
clientId = 0,
92+
clientName = standardData.payeeName,
93+
accountNo = standardData.payeeVpa,
94+
amount = standardData.amount,
95+
accountId = 0,
96+
currency = standardData.currency,
97+
officeId = 1,
98+
accountTypeId = 2,
99+
)
100+
}
101+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.core.model.utils
11+
12+
import kotlinx.serialization.Serializable
13+
14+
/**
15+
* Data class representing standard UPI QR code data
16+
* Based on UPI QR code specification
17+
*/
18+
@Serializable
19+
data class StandardUpiQrData(
20+
val payeeName: String,
21+
val payeeVpa: String,
22+
val amount: String = "",
23+
val currency: String = "INR",
24+
val transactionNote: String = "",
25+
val merchantCode: String = "",
26+
val transactionReference: String = "",
27+
val url: String = "",
28+
// 02 for QR code
29+
val mode: String = "02",
30+
) {
31+
companion object {
32+
const val DEFAULT_CURRENCY = "INR"
33+
}
34+
}

feature/qr/src/commonMain/kotlin/org/mifospay/feature/qr/ScanQrViewModel.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModel
1313
import kotlinx.coroutines.flow.MutableStateFlow
1414
import kotlinx.coroutines.flow.asSharedFlow
1515
import kotlinx.coroutines.flow.update
16+
import org.mifospay.core.data.util.StandardUpiQrCodeProcessor
1617
import org.mifospay.core.data.util.UpiQrCodeProcessor
1718

1819
class ScanQrViewModel : ViewModel() {
@@ -22,7 +23,15 @@ class ScanQrViewModel : ViewModel() {
2223

2324
fun onScanned(data: String): Boolean {
2425
return try {
25-
UpiQrCodeProcessor.decodeUpiString(data)
26+
try {
27+
UpiQrCodeProcessor.decodeUpiString(data)
28+
} catch (e: Exception) {
29+
if (StandardUpiQrCodeProcessor.isValidUpiQrCode(data)) {
30+
StandardUpiQrCodeProcessor.parseUpiQrCode(data)
31+
} else {
32+
throw e
33+
}
34+
}
2635

2736
_eventFlow.update {
2837
ScanQrEvent.OnNavigateToSendScreen(data)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@
3838
<string name="feature_send_money_error_account_cannot_be_empty">Account cannot be empty</string>
3939
<string name="feature_send_money_error_requesting_payment_qr_but_found">Requesting payment QR but found - %1$s</string>
4040
<string name="feature_send_money_error_requesting_payment_qr_data_missing">Failed to request payment QR: required data is missing</string>
41+
<string name="feature_send_money_upi_qr_parsed_successfully">UPI QR code parsed successfully</string>
42+
<string name="feature_send_money_external_upi_payment">External UPI Payment</string>
4143
</resources>

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import androidx.compose.animation.fadeOut
1616
import androidx.compose.animation.slideInVertically
1717
import androidx.compose.animation.slideOutVertically
1818
import androidx.compose.foundation.BorderStroke
19-
import androidx.compose.foundation.ExperimentalFoundationApi
2019
import androidx.compose.foundation.clickable
2120
import androidx.compose.foundation.layout.Arrangement
2221
import androidx.compose.foundation.layout.Box
@@ -109,6 +108,11 @@ fun SendMoneyScreen(
109108
}
110109

111110
is SendMoneyEvent.NavigateToScanQrScreen -> navigateToScanQrScreen.invoke()
111+
112+
is SendMoneyEvent.ShowToast -> {
113+
// TODO: Implement toast message display
114+
// For now, we'll just ignore it
115+
}
112116
}
113117
}
114118

@@ -130,7 +134,6 @@ fun SendMoneyScreen(
130134
)
131135
}
132136

133-
@OptIn(ExperimentalFoundationApi::class)
134137
@Composable
135138
private fun SendMoneyScreen(
136139
state: SendMoneyState,

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ import mobile_wallet.feature.send_money.generated.resources.feature_send_money_e
3333
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_error_invalid_amount
3434
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_error_requesting_payment_qr_but_found
3535
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_error_requesting_payment_qr_data_missing
36+
import mobile_wallet.feature.send_money.generated.resources.feature_send_money_upi_qr_parsed_successfully
3637
import org.jetbrains.compose.resources.StringResource
3738
import org.mifospay.core.common.DataState
3839
import org.mifospay.core.common.getSerialized
3940
import org.mifospay.core.common.setSerialized
4041
import org.mifospay.core.data.repository.AccountRepository
42+
import org.mifospay.core.data.util.StandardUpiQrCodeProcessor
4143
import org.mifospay.core.data.util.UpiQrCodeProcessor
4244
import org.mifospay.core.model.search.AccountResult
4345
import org.mifospay.core.model.utils.PaymentQrData
@@ -176,7 +178,16 @@ class SendMoneyViewModel(
176178
private fun handleRequestData(action: HandleRequestData) {
177179
viewModelScope.launch {
178180
try {
179-
val requestData = UpiQrCodeProcessor.decodeUpiString(action.requestData)
181+
val requestData = try {
182+
UpiQrCodeProcessor.decodeUpiString(action.requestData)
183+
} catch (e: Exception) {
184+
if (StandardUpiQrCodeProcessor.isValidUpiQrCode(action.requestData)) {
185+
val standardData = StandardUpiQrCodeProcessor.parseUpiQrCode(action.requestData)
186+
StandardUpiQrCodeProcessor.toPaymentQrData(standardData)
187+
} else {
188+
throw e
189+
}
190+
}
180191

181192
mutableStateFlow.update { state ->
182193
state.copy(
@@ -185,6 +196,8 @@ class SendMoneyViewModel(
185196
selectedAccount = requestData.toAccount(),
186197
)
187198
}
199+
200+
sendEvent(SendMoneyEvent.ShowToast(Res.string.feature_send_money_upi_qr_parsed_successfully))
188201
} catch (e: Exception) {
189202
val errorState = if (action.requestData.isNotEmpty()) {
190203
Error.GenericResourceMessage(
@@ -260,6 +273,7 @@ sealed interface SendMoneyEvent {
260273
data object OnNavigateBack : SendMoneyEvent
261274
data class NavigateToTransferScreen(val data: String) : SendMoneyEvent
262275
data object NavigateToScanQrScreen : SendMoneyEvent
276+
data class ShowToast(val message: StringResource) : SendMoneyEvent
263277
}
264278

265279
sealed interface SendMoneyAction {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.navigation.NavGraphBuilder
1414
import androidx.navigation.NavOptions
1515
import androidx.navigation.NavType
1616
import androidx.navigation.navArgument
17+
import androidx.navigation.navOptions
1718
import org.mifospay.core.ui.composableWithSlideTransitions
1819
import org.mifospay.feature.send.money.SendMoneyScreen
1920

@@ -54,9 +55,9 @@ fun NavController.navigateToSendMoneyScreen(
5455
navOptions: NavOptions? = null,
5556
) {
5657
val route = "$SEND_MONEY_ROUTE?$SEND_MONEY_ARG=$requestData"
57-
val options = navOptions ?: NavOptions.Builder()
58-
.setPopUpTo(SEND_MONEY_ROUTE, inclusive = true)
59-
.build()
58+
val options = navOptions ?: navOptions {
59+
popUpTo(SEND_MONEY_ROUTE) { inclusive = true }
60+
}
6061

6162
navigate(route, options)
6263
}

0 commit comments

Comments
 (0)