Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmp-android/prodRelease-badging.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package: name='org.mifospay' versionCode='1' versionName='2025.8.2-beta.0.3' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
package: name='org.mifospay' versionCode='1' versionName='2025.8.4-beta.0.11' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
minSdkVersion:'26'
targetSdkVersion:'34'
uses-permission: name='android.permission.INTERNET'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,16 @@ import org.mifospay.feature.savedcards.details.cardDetailRoute
import org.mifospay.feature.savedcards.details.navigateToCardDetails
import org.mifospay.feature.send.money.SendMoneyScreen
import org.mifospay.feature.send.money.navigation.SEND_MONEY_BASE_ROUTE
import org.mifospay.feature.send.money.navigation.SEND_MONEY_OPTIONS_ROUTE
import org.mifospay.feature.send.money.navigation.bankTransferScreen
import org.mifospay.feature.send.money.navigation.navigateToBankTransferScreen
import org.mifospay.feature.send.money.navigation.navigateToPayeeDetailsScreen
import org.mifospay.feature.send.money.navigation.navigateToSearchIfscScreen
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyOptionsScreen
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen
import org.mifospay.feature.send.money.navigation.payeeDetailsScreen
import org.mifospay.feature.send.money.navigation.searchIfscScreen
import org.mifospay.feature.send.money.navigation.sendMoneyOptionsScreen
import org.mifospay.feature.send.money.navigation.sendMoneyScreen
import org.mifospay.feature.settings.navigation.settingsScreen
import org.mifospay.feature.standing.instruction.StandingInstructionsScreen
Expand All @@ -97,6 +106,7 @@ internal fun MifosNavHost(
onBackClick = navController::navigateUp,
navigateToTransferScreen = navController::navigateToTransferScreen,
navigateToScanQrScreen = navController::navigateToScanQr,
navigateToPayeeDetails = navController::navigateToPayeeDetailsScreen,
showTopBar = false,
)
},
Expand Down Expand Up @@ -160,7 +170,7 @@ internal fun MifosNavHost(
onRequest = {
navController.navigateToShowQrScreen()
},
onPay = navController::navigateToSendMoneyScreen,
onPay = navController::navigateToSendMoneyOptionsScreen,
navigateToTransactionDetail = navController::navigateToSpecificTransaction,
navigateToAccountDetail = navController::navigateToSavingAccountDetails,
)
Expand Down Expand Up @@ -279,12 +289,67 @@ internal fun MifosNavHost(
navigateBack = navController::navigateUp,
)

sendMoneyOptionsScreen(
onBackClick = navController::popBackStack,
onScanQrClick = {
// This is now handled by the ViewModel using ML Kit scanner
},
onPayAnyoneClick = {
// TODO: Navigate to Pay Anyone screen
},
onBankTransferClick = {
navController.navigateToBankTransferScreen()
},
onFineractPaymentsClick = {
navController.navigateToSendMoneyScreen()
},
onQrCodeScanned = { qrData ->
navController.navigateToSendMoneyScreen(
requestData = qrData,
navOptions = navOptions {
popUpTo(SEND_MONEY_OPTIONS_ROUTE) {
inclusive = true
}
},
)
},
onNavigateToPayeeDetails = { qrCodeData ->
navController.navigateToPayeeDetailsScreen(qrCodeData)
},
)

sendMoneyScreen(
onBackClick = navController::popBackStack,
navigateToTransferScreen = navController::navigateToTransferScreen,
navigateToPayeeDetailsScreen = navController::navigateToPayeeDetailsScreen,
navigateToScanQrScreen = navController::navigateToScanQr,
)

bankTransferScreen(
onBackClick = navController::popBackStack,
onSearchIfscClick = {
navController.navigateToSearchIfscScreen()
},
)

searchIfscScreen(
onBackClick = navController::popBackStack,
onIfscSelected = { ifscCode ->
// The IFSC code will be handled by the BankTransferViewModel
// when the user returns to the Bank Transfer screen
},
)

payeeDetailsScreen(
onBackClick = navController::popBackStack,
onNavigateToUpiPayment = { state ->
// TODO: Handle UPI payment navigation
},
onNavigateToFineractPayment = { state ->
// TODO: Handle Fineract payment navigation
},
)

transferScreen(
navigateBack = navController::popBackStack,
onTransferSuccess = {
Expand Down Expand Up @@ -322,6 +387,16 @@ internal fun MifosNavHost(
},
)
},
navigateToPayeeDetailsScreen = {
navController.navigateToPayeeDetailsScreen(
qrCodeData = it,
navOptions = navOptions {
popUpTo(SCAN_QR_ROUTE) {
inclusive = true
}
},
)
},
)

merchantTransferScreen(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.util

import org.mifospay.core.model.utils.PaymentQrData
import org.mifospay.core.model.utils.StandardUpiQrData

/**
* Standard UPI QR Code Processor
* Handles parsing of standard UPI QR codes according to UPI specification
*/
object StandardUpiQrCodeProcessor {

/**
* Checks if the given string is a valid UPI QR code
* @param qrData The QR code data string
* @return true if it's a valid UPI QR code, false otherwise
*/
fun isValidUpiQrCode(qrData: String): Boolean {
return qrData.startsWith("upi://") || qrData.startsWith("UPI://")
}

/**
* Parses a standard UPI QR code string
* @param qrData The QR code data string
* @return StandardUpiQrData object with parsed information
* @throws IllegalArgumentException if the QR code is invalid
*/
fun parseUpiQrCode(qrData: String): StandardUpiQrData {
if (!isValidUpiQrCode(qrData)) {
throw IllegalArgumentException("Invalid UPI QR code format")
}

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

val payeeVpa = params["pa"] ?: run {
throw IllegalArgumentException("Missing payee VPA (pa) in UPI QR code")
}
val payeeName = params["pn"] ?: "Unknown"

val vpaParts = payeeVpa.split("@", limit = 2)
val actualVpa = if (vpaParts.size == 2) payeeVpa else payeeVpa

return StandardUpiQrData(
payeeName = payeeName,
payeeVpa = actualVpa,
amount = params["am"] ?: "",
currency = params["cu"] ?: StandardUpiQrData.DEFAULT_CURRENCY,
transactionNote = params["tn"] ?: "",
merchantCode = params["mc"] ?: "",
transactionReference = params["tr"] ?: "",
url = params["url"] ?: "",
mode = params["mode"] ?: "02",
)
}

/**
* Parses URL parameters into a map
* @param paramsString The parameters string
* @return Map of parameter keys and values
*/
private fun parseParams(paramsString: String): Map<String, String> {
return paramsString
.split("&")
.associate { param ->
val keyValue = param.split("=", limit = 2)
if (keyValue.size == 2) {
keyValue[0] to keyValue[1]
} else {
param to ""
}
}
}

/**
* Converts StandardUpiQrData to PaymentQrData for compatibility with existing code
* @param standardData Standard UPI QR data
* @return PaymentQrData object
* Note: clientId and accountId not available in standard UPI
*/
fun toPaymentQrData(standardData: StandardUpiQrData): PaymentQrData {
return PaymentQrData(
clientId = 0,
clientName = standardData.payeeName,
accountNo = standardData.payeeVpa,
amount = standardData.amount,
accountId = 0,
currency = standardData.currency,
officeId = 1,
accountTypeId = 2,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package org.mifospay.core.designsystem.icon

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.ArrowOutward
import androidx.compose.material.icons.filled.AttachMoney
Expand All @@ -22,6 +23,7 @@ import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.CurrencyRupee
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Edit
Expand Down Expand Up @@ -129,4 +131,8 @@ object MifosIcons {
val Scan = Icons.Outlined.QrCodeScanner
val RadioButtonUnchecked = Icons.Default.RadioButtonUnchecked
val RadioButtonChecked = Icons.Filled.RadioButtonChecked

val ArrowForward = Icons.AutoMirrored.Filled.ArrowForward

val CurrencyRupee = Icons.Filled.CurrencyRupee
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.model.utils

import kotlinx.serialization.Serializable

/**
* Data class representing standard UPI QR code data
* Based on UPI QR code specification
*/
@Serializable
data class StandardUpiQrData(
val payeeName: String,
val payeeVpa: String,
val amount: String = "",
val currency: String = "INR",
val transactionNote: String = "",
val merchantCode: String = "",
val transactionReference: String = "",
val url: String = "",
// 02 for QR code
val mode: String = "02",
) {
companion object {
const val DEFAULT_CURRENCY = "INR"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import mobile_wallet.feature.make_transfer.generated.resources.Res
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_empty_amount
import mobile_wallet.feature.make_transfer.generated.resources.feature_make_transfer_error_empty_description
Expand Down Expand Up @@ -207,7 +208,7 @@ internal data class MakeTransferState(
val amount: String = toClientData.amount,
val description: String = "",
val selectedAccount: Account? = null,
val dialogState: DialogState? = null,
@Transient val dialogState: DialogState? = null,
) {
val amountIsValid: Boolean
get() = amount.isNotEmpty() && amount.toDoubleOrNull() != null
Expand All @@ -232,12 +233,9 @@ internal data class MakeTransferState(
transferDate = DateHelper.formattedShortDate,
)

@Serializable
sealed interface DialogState {
@Serializable
data object Loading : DialogState

@Serializable
sealed interface Error : DialogState {
data class StringMessage(val message: String) : Error
data class ResourceMessage(val message: StringResource) : Error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.mifospay.core.designsystem.component.MifosScaffold
internal fun ScanQrCodeScreen(
navigateBack: () -> Unit,
navigateToSendScreen: (String) -> Unit,
navigateToPayeeDetailsScreen: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: ScanQrViewModel = koinViewModel(),
) {
Expand All @@ -44,6 +45,10 @@ internal fun ScanQrCodeScreen(
navigateToSendScreen.invoke((eventFlow as ScanQrEvent.OnNavigateToSendScreen).data)
}

is ScanQrEvent.OnNavigateToPayeeDetails -> {
navigateToPayeeDetailsScreen.invoke((eventFlow as ScanQrEvent.OnNavigateToPayeeDetails).data)
}

is ScanQrEvent.ShowToast -> {
scope.launch {
snackbarHostState.showSnackbar((eventFlow as ScanQrEvent.ShowToast).message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.update
import org.mifospay.core.data.util.StandardUpiQrCodeProcessor
import org.mifospay.core.data.util.UpiQrCodeProcessor

class ScanQrViewModel : ViewModel() {
Expand All @@ -22,10 +23,24 @@ class ScanQrViewModel : ViewModel() {

fun onScanned(data: String): Boolean {
return try {
UpiQrCodeProcessor.decodeUpiString(data)
val isUpiQr = try {
UpiQrCodeProcessor.decodeUpiString(data)
true
} catch (e: Exception) {
if (StandardUpiQrCodeProcessor.isValidUpiQrCode(data)) {
StandardUpiQrCodeProcessor.parseUpiQrCode(data)
true
} else {
false
}
}

_eventFlow.update {
ScanQrEvent.OnNavigateToSendScreen(data)
if (isUpiQr) {
ScanQrEvent.OnNavigateToPayeeDetails(data)
} else {
ScanQrEvent.OnNavigateToSendScreen(data)
}
}

true
Expand All @@ -40,5 +55,6 @@ class ScanQrViewModel : ViewModel() {

sealed interface ScanQrEvent {
data class OnNavigateToSendScreen(val data: String) : ScanQrEvent
data class OnNavigateToPayeeDetails(val data: String) : ScanQrEvent
data class ShowToast(val message: String) : ScanQrEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ fun NavController.navigateToScanQr(navOptions: NavOptions? = null) =
fun NavGraphBuilder.scanQrScreen(
navigateBack: () -> Unit,
navigateToSendScreen: (String) -> Unit,
navigateToPayeeDetailsScreen: (String) -> Unit,
) {
composableWithSlideTransitions(route = SCAN_QR_ROUTE) {
ScanQrCodeScreen(
navigateBack = navigateBack,
navigateToSendScreen = navigateToSendScreen,
navigateToPayeeDetailsScreen = navigateToPayeeDetailsScreen,
)
}
}
Loading
Loading