diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 5647ad7cfc..29b9efc572 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -75,6 +75,10 @@
+
+
+
+
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt
index ec9d994636..f9307e7914 100644
--- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt
@@ -13,7 +13,7 @@ import com.tangem.domain.visa.model.VisaTxDetails
*
[REDACTED_AUTHOR]
*/
-internal class EmailMessageBodyResolver(
+class EmailMessageBodyResolver(
private val feedbackRepository: FeedbackRepository,
) {
diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts
index 57fe70672c..68cab10425 100644
--- a/features/tester/impl/build.gradle.kts
+++ b/features/tester/impl/build.gradle.kts
@@ -3,6 +3,7 @@ plugins {
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
+ alias(deps.plugins.kotlin.serialization)
id("configuration")
}
@@ -40,6 +41,7 @@ dependencies {
implementation(projects.domain.manageTokens)
implementation(projects.domain.wallets.models)
implementation(projects.domain.wallets)
+ implementation(projects.domain.feedback.models)
implementation(projects.data.common)
@@ -68,4 +70,5 @@ dependencies {
implementation(projects.common.ui)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)
+ implementation(deps.kotlin.serialization)
}
\ No newline at end of file
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 e59702c4e2..28d6fe9135 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
@@ -2,6 +2,7 @@ package com.tangem.feature.tester.presentation
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -20,6 +21,8 @@ import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen
import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel
import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel
+import com.tangem.feature.tester.presentation.addresses.ui.AddressesInfoScreen
+import com.tangem.feature.tester.presentation.addresses.viewmodels.AddressesInfoViewModel
import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen
import com.tangem.feature.tester.presentation.environments.viewmodels.EnvironmentsTogglesViewModel
import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockchainsScreen
@@ -82,6 +85,7 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TESTER_ACTIONS,
ButtonUM.TEST_PUSHES,
ButtonUM.ACCOUNTS,
+ ButtonUM.ADDRESSES_INFO,
),
onButtonClick = { buttonUM ->
val route = when (buttonUM) {
@@ -92,6 +96,7 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS
ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES
ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS
+ ButtonUM.ADDRESSES_INFO -> TesterScreen.ADDRESSES_INFO
}
innerTesterRouter.open(route)
@@ -162,6 +167,18 @@ internal class TesterActivity : ComposeActivity() {
AccountsScreen(state)
}
+
+ composable(route = TesterScreen.ADDRESSES_INFO.name) {
+ val viewModel = hiltViewModel()
+
+ LaunchedEffect(viewModel) {
+ viewModel.setupNavigation(innerTesterRouter)
+ }
+
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ AddressesInfoScreen(state)
+ }
}
}
}
\ No newline at end of file
diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/model/TesterAddressJson.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/model/TesterAddressJson.kt
new file mode 100644
index 0000000000..bf672d5a1a
--- /dev/null
+++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/model/TesterAddressJson.kt
@@ -0,0 +1,11 @@
+package com.tangem.feature.tester.presentation.addresses.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TesterAddressJson(
+ val addresses: List,
+ val blockchain: String,
+ val derivationPath: String,
+ val token: String? = null,
+)
\ No newline at end of file
diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/state/AddressesInfoScreenUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/state/AddressesInfoScreenUM.kt
new file mode 100644
index 0000000000..859e8faa05
--- /dev/null
+++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/state/AddressesInfoScreenUM.kt
@@ -0,0 +1,24 @@
+package com.tangem.feature.tester.presentation.addresses.state
+
+import androidx.annotation.StringRes
+
+/**
+ * Content state of addresses info screen
+ *
+ * @property title title
+ * @property addressesText addresses in text format
+ * @property addressesJson addresses in JSON format
+ * @property onBackClick the lambda to be invoked when back button is pressed
+ */
+internal data class AddressesInfoScreenUM(
+ @StringRes val title: Int,
+ val addressesText: String,
+ val addressesJson: String,
+ val onBackClick: () -> Unit,
+ val onCopyClick: (CopyType) -> Unit,
+)
+
+internal enum class CopyType {
+ TEXT,
+ JSON,
+}
\ No newline at end of file
diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/ui/AddressesInfoScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/ui/AddressesInfoScreen.kt
new file mode 100644
index 0000000000..00d593ecba
--- /dev/null
+++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/ui/AddressesInfoScreen.kt
@@ -0,0 +1,142 @@
+package com.tangem.feature.tester.presentation.addresses.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.*
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.appbar.AppBarWithBackButton
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.tester.presentation.addresses.state.AddressesInfoScreenUM
+import com.tangem.feature.tester.presentation.addresses.state.CopyType
+import androidx.compose.foundation.text.selection.SelectionContainer
+
+/**
+ * Screen with addresses info in text and JSON format
+ *
+ * @param uiModel screen state
+ */
+@Suppress("LongMethod")
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun AddressesInfoScreen(uiModel: AddressesInfoScreenUM) {
+ var selectedTab by remember { mutableIntStateOf(0) }
+ val tabs = listOf("Text", "JSON")
+
+ Scaffold(
+ topBar = {
+ AppBarWithBackButton(
+ onBackClick = uiModel.onBackClick,
+ text = stringResourceSafe(id = uiModel.title),
+ modifier = Modifier.statusBarsPadding(),
+ )
+ },
+ containerColor = TangemTheme.colors.background.secondary,
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ ) {
+ TabRow(
+ selectedTabIndex = selectedTab,
+ containerColor = TangemTheme.colors.background.secondary,
+ contentColor = TangemTheme.colors.text.primary1,
+ indicator = { tabPositions ->
+ TabRowDefaults.Indicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTab]),
+ color = TangemTheme.colors.text.accent,
+ )
+ },
+ ) {
+ tabs.forEachIndexed { index, title ->
+ Tab(
+ selected = selectedTab == index,
+ onClick = { selectedTab = index },
+ text = {
+ Text(
+ text = title,
+ style = TangemTheme.typography.subtitle2,
+ color = if (selectedTab == index) {
+ TangemTheme.colors.text.primary1
+ } else {
+ TangemTheme.colors.text.secondary
+ },
+ )
+ },
+ selectedContentColor = TangemTheme.colors.text.primary1,
+ unselectedContentColor = TangemTheme.colors.text.secondary,
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ horizontalArrangement = Arrangement.End,
+ ) {
+ IconButton(
+ onClick = {
+ uiModel.onCopyClick(
+ if (selectedTab == 0) CopyType.TEXT else CopyType.JSON,
+ )
+ },
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(R.drawable.ic_copy_24),
+ contentDescription = "Copy",
+ tint = TangemTheme.colors.icon.primary1,
+ )
+ }
+ }
+
+ // Tab content
+ when (selectedTab) {
+ 0 -> {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ item {
+ SelectionContainer {
+ Text(
+ text = uiModel.addressesText,
+ style = TangemTheme.typography.body1,
+ color = TangemTheme.colors.text.primary1,
+ )
+ }
+ }
+ }
+ }
+
+ 1 -> {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ item {
+ SelectionContainer {
+ Text(
+ text = uiModel.addressesJson,
+ style = TangemTheme.typography.body1,
+ color = TangemTheme.colors.text.primary1,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/viewmodels/AddressesInfoViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/viewmodels/AddressesInfoViewModel.kt
new file mode 100644
index 0000000000..13602b4794
--- /dev/null
+++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/addresses/viewmodels/AddressesInfoViewModel.kt
@@ -0,0 +1,175 @@
+package com.tangem.feature.tester.presentation.addresses.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.tangem.core.ui.clipboard.ClipboardManager
+import com.tangem.domain.feedback.models.BlockchainInfo
+import com.tangem.domain.feedback.models.FeedbackEmailType
+import com.tangem.domain.feedback.models.WalletMetaInfo
+import com.tangem.domain.feedback.repository.FeedbackRepository
+import com.tangem.domain.feedback.utils.EmailMessageBodyResolver
+import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
+import com.tangem.feature.tester.impl.R
+import com.tangem.feature.tester.presentation.addresses.model.TesterAddressJson
+import com.tangem.feature.tester.presentation.addresses.state.AddressesInfoScreenUM
+import com.tangem.feature.tester.presentation.addresses.state.CopyType
+import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.Json
+import javax.inject.Inject
+import kotlin.coroutines.cancellation.CancellationException
+
+/**
+ * Tester screen for displaying addresses and tokens.
+ * Uses the same data source as feedback emails to ensure identical output.
+ */
+
+@HiltViewModel
+internal class AddressesInfoViewModel @Inject constructor(
+ private val feedbackRepository: FeedbackRepository,
+ private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
+ private val clipboardManager: ClipboardManager,
+) : ViewModel() {
+
+ private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository)
+
+ private val _uiState = MutableStateFlow(getInitialState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ fun setupNavigation(router: InnerTesterRouter) {
+ _uiState.update { state ->
+ state.copy(
+ onBackClick = router::back,
+ onCopyClick = ::onCopyClicked,
+ )
+ }
+
+ loadAddresses()
+ }
+
+ private fun getInitialState(): AddressesInfoScreenUM = AddressesInfoScreenUM(
+ title = R.string.addresses_info,
+ addressesText = "Loading...",
+ addressesJson = "[]",
+ onBackClick = {},
+ onCopyClick = {},
+ )
+
+ private fun loadAddresses() {
+ viewModelScope.launch {
+ try {
+ val wallet = getSelectedWalletSyncUseCase()
+ .getOrNull() ?: run {
+ setEmptyState()
+ return@launch
+ }
+
+ val walletMetaInfo = WalletMetaInfo(
+ userWalletId = wallet.walletId,
+ )
+
+ val emailType = FeedbackEmailType.DirectUserRequest(
+ walletMetaInfo = walletMetaInfo,
+ )
+
+ val emailBody = emailMessageBodyResolver.resolve(emailType)
+
+ val blockchainInfos = feedbackRepository.getBlockchainInfoList(wallet.walletId)
+
+ val json = formatAddressesJson(blockchainInfos)
+
+ _uiState.update { state ->
+ state.copy(
+ addressesText = emailBody,
+ addressesJson = json,
+ )
+ }
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Throwable) {
+ _uiState.update { state ->
+ state.copy(
+ addressesText = "Error loading addresses",
+ addressesJson = errorJson(e),
+ )
+ }
+ }
+ }
+ }
+
+ private fun setEmptyState() {
+ _uiState.update { state ->
+ state.copy(
+ addressesText = "No active addresses found",
+ addressesJson = "[]",
+ )
+ }
+ }
+
+ @Suppress("JSON_FORMAT_REDUNDANT")
+ private fun formatAddressesJson(list: List): String {
+ val jsonList = list
+ .flatMap { info ->
+ val addresses = info
+ .extractAddresses()
+ .sortedDescending()
+
+ val tokens = if (info.tokens.isNotEmpty()) {
+ info.tokens.map { it.name }
+ } else {
+ listOf(info.blockchain)
+ }
+
+ tokens.map { token ->
+ TesterAddressJson(
+ addresses = addresses,
+ blockchain = info.blockchain,
+ derivationPath = info.derivationPath,
+ token = token,
+ )
+ }
+ }
+ .sortedWith(
+ compareBy { it.blockchain }
+ .thenBy { it.derivationPath }
+ .thenBy { it.token },
+ )
+
+ return Json { prettyPrint = true }
+ .encodeToString(jsonList)
+ }
+
+ private fun BlockchainInfo.extractAddresses(): List = when (val a = addresses) {
+ is BlockchainInfo.Addresses.Single ->
+ listOf(a.value)
+
+ is BlockchainInfo.Addresses.Multiple ->
+ a.values.map { it.value }
+ }
+
+ private fun errorJson(error: Throwable): String = Json.encodeToString(
+ mapOf("error" to (error.message ?: "Unknown error")),
+ )
+
+ private fun onCopyClicked(type: CopyType) {
+ val state = _uiState.value
+
+ val text = when (type) {
+ CopyType.TEXT -> state.addressesText
+ CopyType.JSON -> state.addressesJson
+ }
+
+ if (text.isNotBlank()) {
+ clipboardManager.setText(
+ text = text,
+ isSensitive = false,
+ label = type.name,
+ )
+ }
+ }
+}
\ 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 fa30058309..2d2f610f44 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
@@ -25,5 +25,6 @@ data class TesterMenuUM(
TESTER_ACTIONS(R.string.tester_actions),
TEST_PUSHES(R.string.test_push),
ACCOUNTS(R.string.accounts),
+ ADDRESSES_INFO(R.string.addresses_info),
}
}
\ 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 e87d942c19..ccf4b0acd0 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
@@ -14,4 +14,5 @@ internal enum class TesterScreen {
BLOCKCHAIN_PROVIDERS,
TEST_PUSHES,
ACCOUNTS,
+ ADDRESSES_INFO,
}
\ 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 901f76c14e..9aadd20368 100644
--- a/features/tester/impl/src/main/res/values/strings.xml
+++ b/features/tester/impl/src/main/res/values/strings.xml
@@ -21,4 +21,5 @@
News
News details
News details (Bottom Sheet)
+ Addresses info