Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ internal fun MpayQrScreen(
MpayQrEvent.OnNavigateBack -> navigateBack.invoke()
MpayQrEvent.QrDownloaded -> snackbarHostState.showSnackbar(downloadedMessage)
is MpayQrEvent.ShowSnackbar -> {
clipboardManager.setText(AnnotatedString(event.message))
snackbarHostState.showSnackbar(event.message)
}

is MpayQrEvent.CopyToClipboard -> {
clipboardManager.setText(AnnotatedString(event.text))
snackbarHostState.showSnackbar(copiedMessage)
}
}
Expand Down Expand Up @@ -292,6 +296,7 @@ private fun MpayQrScreenContent(
modifier = Modifier.padding(horizontal = KptTheme.spacing.sm),
)
}

1 -> {
// Inter-Bank - show QR or placeholder
if (contentState.interBankData != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,35 +126,35 @@ class MpayQrViewModel(

private fun loadAccounts() {
viewModelScope.launch {
accountRepository.getSelfAccounts(state.client.id)
.collect { result ->
when (result) {
is DataState.Success -> {
val accounts = result.data
val defaultAcc = accounts.find { it.id == state.defaultAccount.accountId }
accountRepository.getSelfAccounts(state.client.id).collect { result ->
when (result) {
is DataState.Success -> {
val accounts = result.data
val defaultAcc =
accounts.find { it.id == state.defaultAccount.accountId }
?: accounts.firstOrNull()

mutableStateFlow.update {
it.copy(
accounts = accounts,
selectedAccount = defaultAcc,
accountExternalId = defaultAcc?.externalId ?: "",
)
}
sendAction(MpayQrAction.Internal.GenerateQr)
mutableStateFlow.update {
it.copy(
accounts = accounts,
selectedAccount = defaultAcc,
accountExternalId = defaultAcc?.externalId ?: "",
)
}
sendAction(MpayQrAction.Internal.GenerateQr)
}

is DataState.Error -> {
Logger.e { "Failed to load accounts: ${result.exception.message}" }
// Still generate QR with default account
sendAction(MpayQrAction.Internal.GenerateQr)
}
is DataState.Error -> {
Logger.e { "Failed to load accounts: ${result.exception.message}" }
// Still generate QR with default account
sendAction(MpayQrAction.Internal.GenerateQr)
}

is DataState.Loading -> {
// Loading state handled by viewState
}
is DataState.Loading -> {
// Loading state handled by viewState
}
}
}
}
}

Expand Down Expand Up @@ -227,8 +227,12 @@ class MpayQrViewModel(
}
}

is MpayQrAction.ShowSnackbar -> {
sendEvent(MpayQrEvent.ShowSnackbar(action.message))
}

is MpayQrAction.CopyToClipboard -> {
sendEvent(MpayQrEvent.ShowSnackbar(action.text))
sendEvent(MpayQrEvent.CopyToClipboard(action.text))
}

is MpayQrAction.ShowAccountPicker -> {
Expand Down Expand Up @@ -303,26 +307,31 @@ class MpayQrViewModel(

private fun initiateSetAmount() {
viewModelScope.launch {
val intraBankData = withContext(ioDispatcher) {
MpayQrCodeProcessor.encodeMpayString(state.qrData)
}
try {
val intraBankData = withContext(ioDispatcher) {
MpayQrCodeProcessor.encodeMpayString(state.qrData)
}

val interBankData = if (state.accountExternalId.isNotBlank()) {
withContext(ioDispatcher) {
MpayQrCodeProcessor.encodeMpayString(state.interBankQrData)
val interBankData = if (state.accountExternalId.isNotBlank()) {
withContext(ioDispatcher) {
MpayQrCodeProcessor.encodeMpayString(state.interBankQrData)
}
} else {
null
}
} else {
null
}

updateContent {
it.copy(
intraBankData = intraBankData,
interBankData = interBankData,
)
}
updateContent {
it.copy(
intraBankData = intraBankData,
interBankData = interBankData,
)
}

mutableStateFlow.update { it.copy(dialogState = null) }
mutableStateFlow.update { it.copy(dialogState = null) }
} catch (e: IllegalArgumentException) {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(MpayQrEvent.ShowSnackbar(e.message ?: "Invalid amount entered"))
}
Comment on lines +331 to +334
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalid amount is left in state, so a crash path still exists.

After Line 333, the invalid value remains in state.qrData.amount. A later QR regeneration (e.g., account switch) can still hit encodeMpayString(state.qrData) and throw again. Clear or rollback invalid amount in this catch path.

Proposed fix
             } catch (e: IllegalArgumentException) {
-                mutableStateFlow.update { it.copy(dialogState = null) }
+                mutableStateFlow.update {
+                    it.copy(
+                        dialogState = null,
+                        qrData = it.qrData.copy(amount = ""),
+                    )
+                }
                 sendEvent(MpayQrEvent.ShowSnackbar(e.message ?: "Invalid amount entered"))
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e: IllegalArgumentException) {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(MpayQrEvent.ShowSnackbar(e.message ?: "Invalid amount entered"))
}
} catch (e: IllegalArgumentException) {
mutableStateFlow.update {
it.copy(
dialogState = null,
qrData = it.qrData.copy(amount = ""),
)
}
sendEvent(MpayQrEvent.ShowSnackbar(e.message ?: "Invalid amount entered"))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/mpay-qr/src/commonMain/kotlin/org/mifospay/feature/mpay/qr/MpayQrViewModel.kt`
around lines 331 - 334, In the IllegalArgumentException catch inside
MpayQrViewModel, you're currently only clearing dialogState and showing a
snackbar but leaving the invalid value in state.qrData.amount; update the state
there to remove or rollback the bad amount so future calls to
encodeMpayString(state.qrData) won't rethrow (e.g., call mutableStateFlow.update
and set qrData.amount to null or to the previous valid value), ensuring the
catch block resets qrData.amount as well as dialogState before emitting the
snackbar.

}
}

Expand All @@ -340,9 +349,8 @@ class MpayQrViewModel(
) -> MpayQrState.ViewState.Content?,
) {
val currentViewState = state.viewState
val updatedContent = (currentViewState as? MpayQrState.ViewState.Content)
?.let(block)
?: return
val updatedContent =
(currentViewState as? MpayQrState.ViewState.Content)?.let(block) ?: return
mutableStateFlow.update { it.copy(viewState = updatedContent) }
}
}
Expand All @@ -356,21 +364,18 @@ data class MpayQrState(
/**
* All accounts available for the user. Loaded from AccountRepository.
*/
@Transient
val accounts: List<Account> = emptyList(),
@Transient val accounts: List<Account> = emptyList(),

/**
* Currently selected account for QR generation.
* Defaults to the default account on initial load.
*/
@Transient
val selectedAccount: Account? = null,
@Transient val selectedAccount: Account? = null,

/**
* Whether the account picker bottom sheet is visible.
*/
@Transient
val isAccountPickerVisible: Boolean = false,
@Transient val isAccountPickerVisible: Boolean = false,

/**
* The FSP ID (bank/tenant identifier) used for routing.
Expand All @@ -384,8 +389,7 @@ data class MpayQrState(
*/
val accountExternalId: String = "",

@Transient
val viewState: ViewState = ViewState.Loading,
@Transient val viewState: ViewState = ViewState.Loading,

// 0=Intra-bank, 1=Inter-bank
val selectedPage: Int = 0,
Expand All @@ -403,8 +407,7 @@ data class MpayQrState(
currency = "USD",
amount = "",
),
@Transient
val dialogState: DialogState? = null,
@Transient val dialogState: DialogState? = null,
) {
/**
* The currently active external ID, prioritizing selectedAccount over the stored accountExternalId.
Expand Down Expand Up @@ -441,8 +444,7 @@ data class MpayQrState(
) : ViewState {

private val logo: QrLogo
@Composable
get() = QrLogo(
@Composable get() = QrLogo(
painter = painterResource(Res.drawable.logo),
padding = QrLogoPadding.Natural(.1f),
shape = QrLogoShape.circle(),
Expand All @@ -464,17 +466,15 @@ data class MpayQrState(
* fastest and most reliable scanning across all devices.
*/
private val colors: QrColors
@Composable
get() = QrColors(
@Composable get() = QrColors(
light = QrBrush.solid(KptTheme.colorScheme.qrBackground),
dark = QrBrush.solid(KptTheme.colorScheme.qrForeground),
ball = QrBrush.solid(KptTheme.colorScheme.qrForeground),
frame = QrBrush.solid(KptTheme.colorScheme.qrForeground),
)

val options: QrOptions
@Composable
get() = QrOptions(
@Composable get() = QrOptions(
shapes = shapes,
colors = colors,
logo = logo,
Expand All @@ -493,6 +493,7 @@ sealed interface MpayQrEvent {
data object OnNavigateBack : MpayQrEvent
data object QrDownloaded : MpayQrEvent
data class ShowSnackbar(val message: String) : MpayQrEvent
data class CopyToClipboard(val text: String) : MpayQrEvent
}

sealed interface MpayQrAction {
Expand Down Expand Up @@ -542,6 +543,7 @@ sealed interface MpayQrAction {
}
}

data class ShowSnackbar(val message: String) : MpayQrAction
data class CopyToClipboard(val text: String) : MpayQrAction

sealed interface Internal : MpayQrAction {
Expand Down