From 9e932a5bee5c91758fe51d995420a14d6d4d3263 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 22 May 2025 12:34:13 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tester/presentation/TesterActivity.kt | 15 +- .../presentation/menu/state/TesterMenuUM.kt | 1 + .../presentation/navigation/TesterScreen.kt | 1 + .../testpush/entity/TestPushClickIntents.kt | 13 + .../testpush/entity/TestPushUM.kt | 10 + .../testpush/ui/TestPushScreen.kt | 372 ++++++++++++++++++ .../testpush/viewmodel/TestPushViewModel.kt | 94 +++++ .../impl/src/main/res/values/strings.xml | 1 + 8 files changed, 506 insertions(+), 1 deletion(-) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushClickIntents.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushUM.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushScreen.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/TestPushViewModel.kt diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 23e833f935..60ea5abd62 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -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().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + TestPushScreen(state, viewModel) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index 67750c8620..5815a07d58 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -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), ; } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index 4e5ba7097d..8b45b44ea4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -12,4 +12,5 @@ internal enum class TesterScreen { TESTER_ACTIONS, EXCLUDED_BLOCKCHAINS, BLOCKCHAIN_PROVIDERS, + TEST_PUSHES, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushClickIntents.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushClickIntents.kt new file mode 100644 index 0000000000..0672c2ffa0 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushClickIntents.kt @@ -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() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushUM.kt new file mode 100644 index 0000000000..198b984aa9 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/entity/TestPushUM.kt @@ -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>, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushScreen.kt new file mode 100644 index 0000000000..aa66c0c22a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushScreen.kt @@ -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 { + override val values: Sequence + 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 \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/TestPushViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/TestPushViewModel.kt new file mode 100644 index 0000000000..28a60d57e9 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/TestPushViewModel.kt @@ -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 + 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() + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 12d59827a6..98bd0a1df4 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -14,4 +14,5 @@ Filter by name or symbol Blockchain providers Share logs + Test push