Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-24 14:16:45 +05:00
parent db08399066
commit ae809ece30
28 changed files with 939 additions and 10 deletions

View file

@ -80,4 +80,29 @@ fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.ti
fun String.orMaskWithStars(maskWithStars: Boolean): String {
return if (maskWithStars) THREE_STARS else this
}
/**
* Returns a masked representation of the string for safe display.
*
* Rules:
* - empty string: returned unchanged
* - length <= 2: all characters are replaced with '*'
* - length <= 4: keep the first and last characters, mask the middle
* - length > 4: keep the first two and last two characters, mask the middle with "**"
*
* @return masked string according to the rules above
*/
@Suppress("MagicNumber")
fun String.mask(): String {
return when {
this.isEmpty() -> this
this.length <= 2 -> "*".repeat(this.length) // mask all if too short
this.length <= 4 -> this.first() + "*".repeat(this.length - 2) + this.last()
else -> {
val prefix = this.take(2)
val suffix = this.takeLast(2)
"$prefix**$suffix"
}
}
}

View file

@ -147,6 +147,8 @@ data class DialogMessage(
}
}
data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage
/**
* Shows a bottom sheet.
*

View file

@ -2,13 +2,23 @@ package com.tangem.core.ui.message
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
@ -17,6 +27,7 @@ import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalEventMessageHandler
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
@Composable
fun EventMessageEffect(
@ -33,6 +44,7 @@ fun EventMessageEffect(
var dialogMessage: DialogMessage? by remember { mutableStateOf(value = null) }
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) }
var loadingMessage: GlobalLoadingMessage? by remember { mutableStateOf(value = null) }
EventEffect(event = messageEvent) { message ->
when (message) {
@ -51,6 +63,13 @@ fun EventMessageEffect(
is ToastMessage -> {
onShowToast(message, context)
}
is GlobalLoadingMessage -> {
loadingMessage = if (message.isShow) {
message
} else {
null
}
}
}
}
@ -80,6 +99,34 @@ fun EventMessageEffect(
onDismissRequest = { bottomSheetMessageV2 = null },
)
}
loadingMessage?.let {
LoadingDialog()
}
}
@Composable
private fun LoadingDialog() {
Dialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
SpacerHMax()
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.icon.primary1,
)
SpacerHMax()
}
}
}
@Composable

View file

@ -0,0 +1,91 @@
package com.tangem.core.ui.extensions
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class StringMaskTest {
@Test
fun GIVEN_empty_string_WHEN_mask_THEN_returns_empty_string() {
// GIVEN
val input = ""
// WHEN
val actual = input.mask()
// THEN
assertThat(actual).isEqualTo("")
}
@Test
fun GIVEN_one_char_WHEN_mask_THEN_returns_asterisk() {
// GIVEN
val input = "a"
// WHEN
val actual = input.mask()
// THEN
assertThat(actual).isEqualTo("*")
}
@Test
fun GIVEN_two_chars_WHEN_mask_THEN_returns_two_asterisks() {
// GIVEN
val input = "ab"
// WHEN
val actual = input.mask()
// THEN
assertThat(actual).isEqualTo("**")
}
@Test
fun GIVEN_three_chars_WHEN_mask_THEN_masks_middle_only() {
// GIVEN
val input = "abc"
// WHEN
val actual = input.mask()
// THEN
assertThat(actual).isEqualTo("a*c")
}
@Test
fun GIVEN_four_chars_WHEN_mask_THEN_masks_middle_two() {
// GIVEN
val input = "abcd"
// WHEN
val actual = input.mask()
// THEN
assertThat(actual).isEqualTo("a**d")
}
@Test
fun GIVEN_five_chars_WHEN_mask_THEN_keeps_edges_and_two_stars_in_middle() {
// GIVEN
val input = "abcde"
// WHEN
val actual = input.mask()
// THEN
assertThat(actual).isEqualTo("ab**de")
}
@Test
fun GIVEN_seven_chars_WHEN_mask_THEN_keeps_two_on_each_side_and_two_stars() {
// GIVEN
val input = "abcdefg"
// WHEN
val actual = input.mask()
// THEN
assertThat(actual).isEqualTo("ab**fg")
}
}