diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index d138f32aa7..a9b9f295a9 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -287,6 +287,7 @@ フィードバック Tangemへのフィードバック 取引を送信できません + コインの説明エラー 取引 ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 選択したトークンの承認制限を指定します diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 7a77b4cb67..41c22e6cbd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -71,7 +71,7 @@ fun Notification( title = config.title, subtitle = config.subtitle, subtitleColor = subtitleColor, - isClickableComponent = isEnabled && config.onClick != null, + showArrowIcon = isEnabled && config.showArrowIcon, ) } } @@ -130,7 +130,7 @@ private fun MainContent( title: TextReference?, subtitle: TextReference, subtitleColor: Color, - isClickableComponent: Boolean, + showArrowIcon: Boolean, ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( @@ -145,7 +145,7 @@ private fun MainContent( TextsBlock(title = title, subtitle = subtitle, subtitleColor = subtitleColor) - if (isClickableComponent) { + if (showArrowIcon) { SpacerWMax() Icon( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index 8241017b2b..aad1efde67 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -24,6 +24,7 @@ data class NotificationConfig( val buttonsState: ButtonsState? = null, val onClick: (() -> Unit)? = null, val onCloseClick: (() -> Unit)? = null, + val showArrowIcon: Boolean = onClick != null, ) { sealed class ButtonsState { diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 654e910d2d..c2726a604f 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -37,8 +37,7 @@ internal class FeedbackDataBuilder { if (tokens.isNotEmpty()) { builder.breakLine() tokens.forEach { token -> - builder.appendKeyValue("Token ID", token.id ?: "[custom token]") - builder.appendKeyValue("Name", token.name) + addTokenShortInfo(id = token.id, name = token.name) builder.appendKeyValue("Contract address", token.contractAddress) builder.appendKeyValue("Decimals", token.decimals) @@ -100,6 +99,11 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("Transaction ID", txId) } + fun addTokenShortInfo(id: String?, name: String) { + builder.appendKeyValue("Token ID", id ?: "[custom token]") + builder.appendKeyValue("Name", name) + } + fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun build(): String = builder.trimEnd().toString() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 9f11e6ed73..2f9578ed40 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -54,11 +54,19 @@ class SendFeedbackEmailUseCase( } private fun StringBuilder.appendDisclaimerIfNeeded(type: FeedbackEmailType): StringBuilder { - return if (type is FeedbackEmailType.ScanningProblem) { - this - } else { - append(resources.getString(R.string.feedback_data_collection_message)) - skipLine() + return when (type) { + is FeedbackEmailType.ScanningProblem, + is FeedbackEmailType.CurrencyDescriptionError, + -> this + is FeedbackEmailType.DirectUserRequest, + is FeedbackEmailType.RateCanBeBetter, + is FeedbackEmailType.StakingProblem, + is FeedbackEmailType.SwapProblem, + is FeedbackEmailType.TransactionSendingProblem, + -> { + append(resources.getString(R.string.feedback_data_collection_message)) + skipLine() + } } } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 6abec03efc..70d4017993 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -36,4 +36,14 @@ sealed interface FeedbackEmailType { val providerName: String, val txId: String, ) : FeedbackEmailType + + /** + * Error in currency description + * + * @property currencyId currency id + * @property currencyName currency name + */ + data class CurrencyDescriptionError(val currencyId: String, val currencyName: String) : FeedbackEmailType { + override val cardInfo: CardInfo? = null + } } \ No newline at end of file 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 5f9e52a0e2..c6d5bd52eb 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 @@ -23,13 +23,9 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) is FeedbackEmailType.ScanningProblem -> addScanningProblemBody() is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) - is FeedbackEmailType.StakingProblem -> addStakingProblemBody( - type.cardInfo, - type.validatorName, - type.transactionTypes, - type.unsignedTransactions, - ) - is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type.cardInfo, type.providerName, type.txId) + is FeedbackEmailType.StakingProblem -> addStakingProblemBody(type) + is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type) + is FeedbackEmailType.CurrencyDescriptionError -> addTokenInfo(type) } return build() @@ -79,16 +75,11 @@ internal class EmailMessageBodyResolver( addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } - private suspend fun FeedbackDataBuilder.addStakingProblemBody( - cardInfo: CardInfo, - validatorName: String?, - transactionTypes: List, - unsignedTransactions: List, - ) { - addCardInfo(cardInfo) + private suspend fun FeedbackDataBuilder.addStakingProblemBody(type: FeedbackEmailType.StakingProblem) { + addCardInfo(type.cardInfo) addDelimiter() - val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) val blockchainInfo = blockchainError?.let { feedbackRepository.getBlockchainInfo( @@ -103,21 +94,21 @@ internal class EmailMessageBodyResolver( addDelimiter() } - addStakingInfo(validatorName, transactionTypes, unsignedTransactions) + addStakingInfo( + validatorName = type.validatorName, + transactionTypes = type.transactionTypes, + unsignedTransactions = type.unsignedTransactions, + ) addDelimiter() addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } - private suspend fun FeedbackDataBuilder.addSwapProblemBody( - cardInfo: CardInfo, - providerName: String, - txId: String, - ) { - addCardInfo(cardInfo) + private suspend fun FeedbackDataBuilder.addSwapProblemBody(type: FeedbackEmailType.SwapProblem) { + addCardInfo(type.cardInfo) addDelimiter() - val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) val blockchainInfo = blockchainError?.let { feedbackRepository.getBlockchainInfo( @@ -132,7 +123,7 @@ internal class EmailMessageBodyResolver( addDelimiter() } - addSwapInfo(providerName, txId) + addSwapInfo(providerName = type.providerName, txId = type.txId) addDelimiter() addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) @@ -143,4 +134,8 @@ internal class EmailMessageBodyResolver( addDelimiter() addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } + + private fun FeedbackDataBuilder.addTokenInfo(type: FeedbackEmailType.CurrencyDescriptionError) { + addTokenShortInfo(id = type.currencyId, name = type.currencyName) + } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 5f052face7..d0768aff1f 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -16,7 +16,9 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { /** Resolve email message title by [type] */ fun resolve(type: FeedbackEmailType): String { return when (type) { - is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support + is FeedbackEmailType.DirectUserRequest, + is FeedbackEmailType.CurrencyDescriptionError, + -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed is FeedbackEmailType.TransactionSendingProblem, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index ba9c710413..a833d1b6d3 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -29,6 +29,7 @@ internal class EmailSubjectResolver(private val resources: Resources) { is FeedbackEmailType.StakingProblem, is FeedbackEmailType.SwapProblem, -> R.string.feedback_subject_tx_failed + is FeedbackEmailType.CurrencyDescriptionError -> R.string.feedback_token_description_error } .let(resources::getString) } diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index bd7451e801..94012bfcb8 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) + implementation(projects.domain.feedback) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) implementation(projects.domain.staking.models) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 08813a2998..b956de8357 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -20,6 +20,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.markets.* import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent @@ -57,6 +59,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase, private val getTokenExchangesUseCase: GetTokenExchangesUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -109,6 +112,16 @@ internal class MarketsTokenDetailsModel @Inject constructor( // === Analytics === analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked()) }, + onGeneratedAINotificationClick = { + modelScope.launch { + sendFeedbackEmailUseCase( + type = FeedbackEmailType.CurrencyDescriptionError( + currencyId = params.token.id, + currencyName = params.token.name, + ), + ) + } + }, ) private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) { @@ -268,52 +281,56 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } - val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval) - - chart.onRight { - chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.Data( - y = it.priceY.toImmutableList(), - x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(), - ).sorted() - - updateLook { + chart + .onRight { updateTokenChart(it) } + .onLeft { + state.update { it.copy( - xAxisFormatter = xAxisFormatter, - type = state.value.priceChangeType.toChartType(), + chartState = it.chartState.copy( + status = MarketsTokenDetailsUM.ChartState.Status.ERROR, + ), + body = if (it.body is MarketsTokenDetailsUM.Body.Error) { + MarketsTokenDetailsUM.Body.Nothing + } else { + it.body + }, ) } } - - state.update { - it.copy( - chartState = it.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.DATA, - ), - body = if (it.body is MarketsTokenDetailsUM.Body.Nothing) { - MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked) - } else { - it.body - }, - ) - } - }.onLeft { - state.update { - it.copy( - chartState = it.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.ERROR, - ), - body = if (it.body is MarketsTokenDetailsUM.Body.Error) { - MarketsTokenDetailsUM.Body.Nothing - } else { - it.body - }, - ) - } - } }.saveIn(loadChartJobHolder) } + private suspend fun updateTokenChart(tokenChart: TokenChart) { + val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval) + + chartDataProducer.runTransactionSuspend { + chartData = MarketChartData.Data( + y = tokenChart.priceY.toImmutableList(), + x = tokenChart.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted() + + updateLook { + it.copy( + xAxisFormatter = xAxisFormatter, + type = state.value.priceChangeType.toChartType(), + ) + } + } + + state.update { + it.copy( + chartState = it.chartState.copy( + status = MarketsTokenDetailsUM.ChartState.Status.DATA, + ), + body = if (it.body is MarketsTokenDetailsUM.Body.Nothing) { + MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked) + } else { + it.body + }, + ) + } + } + private fun loadInfo() { state.update { it.copy( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt index 7b16182d58..e2e8e6f985 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt @@ -12,7 +12,8 @@ import com.tangem.utils.converter.Converter @Stable internal class DescriptionConverter( - val onReadModeClicked: (InfoBottomSheetContent) -> Unit, + private val onReadModeClicked: (InfoBottomSheetContent) -> Unit, + private val onGeneratedAINotificationClick: () -> Unit, ) : Converter { override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? { @@ -32,7 +33,9 @@ internal class DescriptionConverter( ), ), body = stringReference(value.fullDescription ?: ""), - showGeneratedAINotification = true, + generatedAINotificationUM = InfoBottomSheetContent.GeneratedAINotificationUM( + onClick = onGeneratedAINotificationClick, + ), ), ) }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt index 594d0f81df..23e16539b1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt @@ -27,14 +27,14 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { config = config, addBottomInsets = false, title = { TangemBottomSheetTitle(title = it.title) }, - content = { + content = { content -> Column( modifier = Modifier .verticalScroll(rememberScrollState()) .padding(horizontal = TangemTheme.dimens.spacing16), ) { MarkdownText( - markdown = it.body.resolveReference(), + markdown = content.body.resolveReference(), disableLinkMovementMethod = true, linkifyMask = 0, syntaxHighlightColor = TangemTheme.colors.text.secondary, @@ -43,8 +43,9 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { ), ) - if (it.showGeneratedAINotification) { + if (content.generatedAINotificationUM != null) { AdditionalInfoNotification( + onClick = content.generatedAINotificationUM.onClick, modifier = Modifier .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16) .fillMaxWidth(), @@ -58,11 +59,13 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { } @Composable -private fun AdditionalInfoNotification(modifier: Modifier = Modifier) { +private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) { Notification( config = NotificationConfig( subtitle = TextReference.Res(id = R.string.information_generated_with_ai), iconResId = R.drawable.ic_magic_28, + onClick = onClick, + showArrowIcon = false, ), modifier = modifier, subtitleColor = TangemTheme.colors.text.primary1, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt index c68995460f..d6af118609 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt @@ -6,5 +6,8 @@ import com.tangem.core.ui.extensions.TextReference internal data class InfoBottomSheetContent( val title: TextReference, val body: TextReference, - val showGeneratedAINotification: Boolean = false, -) : TangemBottomSheetConfigContent \ No newline at end of file + val generatedAINotificationUM: GeneratedAINotificationUM? = null, +) : TangemBottomSheetConfigContent { + + data class GeneratedAINotificationUM(val onClick: () -> Unit) +} \ No newline at end of file