Fix Cashu payment review issues

This commit is contained in:
a1denvalu3 2026-07-27 11:48:58 +02:00
parent 6f4f6b988b
commit 199f95fdbd
6 changed files with 70 additions and 2 deletions

View File

@ -47,7 +47,7 @@ object CashuTokenDecoder {
}
if (token.length !in 12..MAX_TOKEN_LENGTH) return null
if (!token.startsWith("cashuA") && !token.startsWith("cashuB")) return null
if (token.any { !it.isLetterOrDigit() && it !in "-_+/=." }) return null
if (token.any { !it.isLetterOrDigit() && it !in "-_+/=" }) return null
return token
}
@ -198,7 +198,7 @@ object CashuTokenDecoder {
}.getOrNull()?.takeIf { it >= 0 }
}
private val TOKEN_REGEX = Regex("""(?i:cashu:(?://)?)?cashu[AB][A-Za-z0-9_+/.=%-]{6,}""")
private val TOKEN_REGEX = Regex("""(?i:cashu:(?://)?)?cashu[AB][A-Za-z0-9_+/%=-]{6,}""")
}
private sealed interface CborValue {

View File

@ -920,6 +920,20 @@ class ChatViewModel(
mesh.myPeerID,
state.getNicknameValue()
)
} else if (channel != null && channelManager.hasChannelKey(channel)) {
channelManager.sendEncryptedChannelMessage(
messageContent,
mentions,
channel,
state.getNicknameValue(),
mesh.myPeerID,
onEncryptedPayload = {
mesh.sendMessage(messageContent, mentions, channel)
},
onFallback = {
mesh.sendMessage(messageContent, mentions, channel)
}
)
} else {
mesh.sendMessage(messageContent, mentions, channel)
}

View File

@ -438,11 +438,18 @@ class CommandProcessor(
isRelay = false
)
val selectedPeer = state.getSelectedPrivateChatPeerValue()
val selectedLocationChannel = state.selectedLocationChannel.value
val channel = state.getCurrentChannelValue()
when {
selectedPeer != null -> {
messageManager.addPrivateMessageNoUnread(selectedPeer, message.copy(isPrivate = true))
}
selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location -> {
messageManager.addChannelMessage(
"geo:${selectedLocationChannel.channel.geohash}",
message
)
}
channel != null -> channelManager.addChannelMessage(channel, message, null)
else -> messageManager.addMessage(message)
}

View File

@ -1,5 +1,11 @@
package com.bitchat.android.ui
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.animation.AnimatedContent
@ -15,7 +21,9 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -25,13 +33,17 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -53,6 +65,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp

View File

@ -94,6 +94,14 @@ class CashuTokenDecoderTest {
assertEquals(first, CashuTokenDecoder.bareToken("cashu%3A$first"))
}
@Test
fun `sentence punctuation is not included in extracted token`() {
val token = v3Token("""{"token":[{"proofs":[{"amount":1}]}]}""")
assertEquals(listOf(token), CashuTokenDecoder.extractTokens("Redeem $token."))
assertNull(CashuTokenDecoder.bareToken("$token."))
}
@Test
fun `oversized and deeply nested input fails closed`() {
assertNull(CashuTokenDecoder.decode("cashuA" + "A".repeat(CashuTokenDecoder.MAX_TOKEN_LENGTH)))

View File

@ -2,6 +2,9 @@ package com.bitchat.android.ui
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.bitchat.android.geohash.ChannelID
import com.bitchat.android.geohash.GeohashChannel
import com.bitchat.android.geohash.GeohashChannelLevel
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import junit.framework.TestCase.assertEquals
@ -136,4 +139,26 @@ class CommandProcessorTest() {
assertTrue(locallyRead.contains(message.id))
}
@Test
fun `pay feedback is added to active geohash channel`() {
val geohash = "u0nd"
chatState.setSelectedLocationChannel(
ChannelID.Location(GeohashChannel(GeohashChannelLevel.PROVINCE, geohash))
)
commandProcessor.processCommand(
command = "/pay invalid",
meshService = meshService,
myPeerID = "peer-id",
onSendMessage = { _, _, _ -> },
viewModel = null
)
assertEquals(
"invalid cashu token — not sending it",
chatState.getChannelMessagesValue()["geo:$geohash"]?.single()?.content
)
assertEquals(0, chatState.getMessagesValue().size)
}
}