Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-22 12:34:13 +05:00
parent 3752cb060c
commit 9e932a5bee
8 changed files with 506 additions and 1 deletions

View file

@ -30,6 +30,8 @@ import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.feature.tester.presentation.navigation.TesterScreen
import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersScreen
import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel
import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen
import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel
import com.tangem.features.tester.api.TesterRouter
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.collections.immutable.persistentSetOf
@ -66,7 +68,7 @@ internal class TesterActivity : ComposeActivity() {
TesterNavHost()
}
@Suppress("TopLevelComposableFunctions")
@Suppress("TopLevelComposableFunctions", "LongMethod")
@Composable
private fun TesterNavHost() {
val navController = rememberNavController().also { innerTesterRouter.setNavController(it) }
@ -82,6 +84,7 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.ENVIRONMENT_TOGGLES,
ButtonUM.BLOCKCHAIN_PROVIDERS,
ButtonUM.TESTER_ACTIONS,
ButtonUM.TEST_PUSHES,
),
onButtonClick = {
val route = when (it) {
@ -90,6 +93,7 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES
ButtonUM.BLOCKCHAIN_PROVIDERS -> TesterScreen.BLOCKCHAIN_PROVIDERS
ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS
ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES
}
innerTesterRouter.open(route)
@ -141,6 +145,15 @@ internal class TesterActivity : ComposeActivity() {
val state by viewModel.state.collectAsStateWithLifecycle()
BlockchainProvidersScreen(state)
}
composable(route = TesterScreen.TEST_PUSHES.name) {
val viewModel = hiltViewModel<TestPushViewModel>().apply {
setupNavigation(innerTesterRouter)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
TestPushScreen(state, viewModel)
}
}
}
}

View file

@ -23,6 +23,7 @@ data class TesterMenuUM(
ENVIRONMENT_TOGGLES(R.string.environment_toggles),
BLOCKCHAIN_PROVIDERS(R.string.blockchain_providers),
TESTER_ACTIONS(R.string.tester_actions),
TEST_PUSHES(R.string.test_push),
;
}
}

View file

@ -12,4 +12,5 @@ internal enum class TesterScreen {
TESTER_ACTIONS,
EXCLUDED_BLOCKCHAINS,
BLOCKCHAIN_PROVIDERS,
TEST_PUSHES,
}

View file

@ -0,0 +1,13 @@
package com.tangem.feature.tester.presentation.testpush.entity
import androidx.compose.ui.text.input.TextFieldValue
interface TestPushClickIntents {
fun onTitleChange(value: TextFieldValue)
fun onMessageChange(value: TextFieldValue)
fun onDataKeyChange(index: Int, value: TextFieldValue)
fun onDataValueChange(index: Int, value: TextFieldValue)
fun onDataAdd()
fun onDataRemove(index: Int)
fun onBackClick()
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.tester.presentation.testpush.entity
import androidx.compose.ui.text.input.TextFieldValue
internal data class TestPushUM(
val fcmToken: String,
val title: TextFieldValue,
val message: TextFieldValue,
val data: List<Pair<TextFieldValue, TextFieldValue>>,
)

View file

@ -0,0 +1,372 @@
package com.tangem.feature.tester.presentation.testpush.ui
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context.NOTIFICATION_SERVICE
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.tangem.core.ui.R
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tester.presentation.testpush.entity.TestPushClickIntents
import com.tangem.feature.tester.presentation.testpush.entity.TestPushUM
private const val SWIPE_THRESHOLD = 0.25f
@Composable
internal fun TestPushScreen(testPushUM: TestPushUM, testPushClickIntents: TestPushClickIntents) {
Scaffold(
topBar = {
AppBarWithBackButton(onBackClick = testPushClickIntents::onBackClick)
},
bottomBar = {
SendLocalPushButton(testPushUM)
},
containerColor = TangemTheme.colors.background.secondary,
) {
LazyColumn(
modifier = Modifier
.padding(it)
.fillMaxSize(),
) {
tokenData(fcmToken = testPushUM.fcmToken)
contentData(testPushUM = testPushUM, testPushClickIntents = testPushClickIntents)
extraData(testPushUM = testPushUM, testPushClickIntents = testPushClickIntents)
}
}
}
private fun LazyListScope.tokenData(fcmToken: String) {
item("token_key") {
val clipboard = LocalClipboardManager.current
Column(
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.action)
.padding(16.dp)
.animateItem(),
) {
Text(
text = "FCM Token",
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.fillMaxWidth(),
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(TangemTheme.colors.background.tertiary)
.clickable {
clipboard.setText(AnnotatedString(fcmToken))
}
.padding(8.dp),
) {
Text(
text = fcmToken,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(end = 8.dp)
.weight(1f),
)
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_copy_24),
),
tint = TangemTheme.colors.icon.secondary,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
}
}
}
}
private fun LazyListScope.contentData(testPushUM: TestPushUM, testPushClickIntents: TestPushClickIntents) {
item("content_key") {
Column(
modifier = Modifier
.padding(top = 8.dp)
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.action)
.padding(16.dp)
.animateItem(),
) {
Text(
text = "Content",
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.fillMaxWidth(),
)
OutlineTextField(
label = "Title",
value = testPushUM.title,
onValueChange = testPushClickIntents::onTitleChange,
modifier = Modifier.fillMaxWidth(),
)
OutlineTextField(
label = "Message",
value = testPushUM.message,
onValueChange = testPushClickIntents::onMessageChange,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
private fun LazyListScope.extraData(testPushUM: TestPushUM, testPushClickIntents: TestPushClickIntents) {
item("data_key") {
Text(
text = "Data",
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(topEnd = 16.dp, topStart = 16.dp))
.background(TangemTheme.colors.background.action)
.padding(16.dp)
.animateItem(),
)
}
itemsIndexed(
items = testPushUM.data,
key = { index, _ -> index },
contentType = { _, item -> item::class },
) { index, (key, value) ->
EnterPushData(
key = key,
value = value,
index = index,
testPushClickIntents = testPushClickIntents,
modifier = Modifier
.background(TangemTheme.colors.background.action)
.padding(horizontal = 16.dp)
.animateItem(),
)
}
item("data_add_key") {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
.background(TangemTheme.colors.background.action)
.padding(top = 8.dp, bottom = 16.dp, start = 16.dp, end = 16.dp)
.animateItem(),
) {
PrimaryButton(
text = "Add param",
onClick = testPushClickIntents::onDataAdd,
size = TangemButtonSize.Action,
modifier = Modifier.align(Alignment.CenterEnd),
)
}
}
}
@Composable
private fun EnterPushData(
key: TextFieldValue,
value: TextFieldValue,
index: Int,
testPushClickIntents: TestPushClickIntents,
modifier: Modifier = Modifier,
) {
val swipeToDelete = rememberSwipeToDismissBoxState(
positionalThreshold = { it * SWIPE_THRESHOLD },
)
SwipeToDismissBox(
modifier = modifier,
state = swipeToDelete,
enableDismissFromStartToEnd = false,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.background(Color.Red),
) {
Text(
text = "Remove",
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.constantWhite,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(8.dp),
)
}
},
) {
Column(
modifier = Modifier.background(TangemTheme.colors.background.action),
) {
if (index != 0) {
HorizontalDivider(
modifier = Modifier.padding(top = 8.dp),
)
}
OutlineTextField(
label = "Key",
value = key,
onValueChange = {
testPushClickIntents.onDataKeyChange(index, it)
},
modifier = Modifier.fillMaxWidth(),
)
OutlineTextField(
label = "Value",
value = value,
onValueChange = {
testPushClickIntents.onDataValueChange(index, it)
},
modifier = Modifier.fillMaxWidth(),
)
}
}
when (swipeToDelete.currentValue) {
SwipeToDismissBoxValue.EndToStart -> {
LaunchedEffect(swipeToDelete) {
testPushClickIntents.onDataRemove(index)
swipeToDelete.snapTo(SwipeToDismissBoxValue.Settled)
}
}
SwipeToDismissBoxValue.StartToEnd -> {}
SwipeToDismissBoxValue.Settled -> {}
}
}
@Composable
private fun SendLocalPushButton(testPushUM: TestPushUM) {
val context = LocalContext.current
PrimaryButton(
text = "Send test push",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
onClick = {
val channelId = "Test Tangem Channel"
val intent = Intent(context, Class.forName("com.tangem.tap.MainActivity")).apply {
testPushUM.data.forEach { (key, value) ->
putExtra(key.text, value.text)
}
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ 1,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
val notificationBuilder =
NotificationCompat.Builder(context.applicationContext, channelId)
.setSmallIcon(com.tangem.feature.tester.impl.R.drawable.ic_tangem_24)
.setContentTitle(testPushUM.title.text)
.setContentText(testPushUM.message.text)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
val notificationManager =
context.applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(
context.applicationContext,
R.string.common_tangem_wallet,
),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
},
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TestPushScreen_Preview(@PreviewParameter(TestPushScreenPreviewProvider::class) params: TestPushUM) {
TangemThemePreview {
TestPushScreen(
testPushUM = params,
testPushClickIntents = object : TestPushClickIntents {
override fun onTitleChange(value: TextFieldValue) {}
override fun onMessageChange(value: TextFieldValue) {}
override fun onDataKeyChange(index: Int, value: TextFieldValue) {}
override fun onDataValueChange(index: Int, value: TextFieldValue) {}
override fun onDataAdd() {}
override fun onDataRemove(index: Int) {}
override fun onBackClick() {}
},
)
}
}
private class TestPushScreenPreviewProvider : PreviewParameterProvider<TestPushUM> {
override val values: Sequence<TestPushUM>
get() = sequenceOf(
TestPushUM(
fcmToken = "qwertyuiopoiuytrewqwertyuiopoiuytrewqertyuiop",
title = TextFieldValue("Test push"),
message = TextFieldValue("Test push"),
data = listOf(
TextFieldValue("deeplink") to TextFieldValue("tangem://main"),
TextFieldValue("") to TextFieldValue(""),
),
),
)
}
// endregion

View file

@ -0,0 +1,94 @@
package com.tangem.feature.tester.presentation.testpush.viewmodel
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.feature.tester.presentation.testpush.entity.TestPushClickIntents
import com.tangem.feature.tester.presentation.testpush.entity.TestPushUM
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class TestPushViewModel @Inject constructor(
private val pushNotificationsTokenProvider: PushNotificationsTokenProvider,
) : ViewModel(), TestPushClickIntents {
val uiState: StateFlow<TestPushUM>
field = MutableStateFlow(
TestPushUM(
fcmToken = "",
title = TextFieldValue(""),
message = TextFieldValue(""),
data = listOf(TextFieldValue("") to TextFieldValue("")),
),
)
private var router: InnerTesterRouter? = null
init {
viewModelScope.launch {
val fcmToken = pushNotificationsTokenProvider.getToken()
uiState.update { it.copy(fcmToken = fcmToken) }
}
}
fun setupNavigation(router: InnerTesterRouter) {
this.router = router
}
override fun onTitleChange(value: TextFieldValue) {
uiState.update { it.copy(title = value) }
}
override fun onMessageChange(value: TextFieldValue) {
uiState.update { it.copy(message = value) }
}
override fun onDataKeyChange(index: Int, value: TextFieldValue) {
val mutableData = uiState.value.data.toMutableList()
val updated = mutableData[index].copy(first = value)
mutableData.set(index = index, updated)
uiState.update {
it.copy(data = mutableData)
}
}
override fun onDataValueChange(index: Int, value: TextFieldValue) {
val mutableData = uiState.value.data.toMutableList()
val updated = mutableData[index].copy(second = value)
mutableData.set(index = index, updated)
uiState.update {
it.copy(data = mutableData)
}
}
override fun onDataAdd() {
uiState.update {
it.copy(
data = it.data.plus(TextFieldValue("") to TextFieldValue("")),
)
}
}
override fun onDataRemove(index: Int) {
uiState.update {
it.copy(
data = it.data.toMutableList().apply {
removeAt(index = index)
},
)
}
}
override fun onBackClick() {
router?.back()
}
}

View file

@ -14,4 +14,5 @@
<string name="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
<string name="blockchain_providers" translatable="false">Blockchain providers</string>
<string name="share_logs" translatable="false">Share logs</string>
<string name="test_push" translatable="false">Test push</string>
</resources>