Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 19:21:37 +03:00
parent 6312034cf9
commit 17d7fb3c02
36 changed files with 311 additions and 301 deletions

View file

@ -1,21 +0,0 @@
package com.tangem.domain.feedback.models
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
data class CardInfo(
val userWalletId: UserWalletId?,
val cardId: String,
val firmwareVersion: String,
val cardsCount: String,
val cardBlockchain: String?,
val signedHashesList: List<SignedHashes>,
val isImported: Boolean,
val isStart2Coin: Boolean,
val isVisa: Boolean,
) {
@Serializable
data class SignedHashes(val curve: String, val total: String?)
}

View file

@ -9,32 +9,32 @@ import com.tangem.domain.visa.model.VisaTxDetails
*/
sealed interface FeedbackEmailType {
val cardInfo: CardInfo?
val walletMetaInfo: WalletMetaInfo?
/** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */
data class DirectUserRequest(override val cardInfo: CardInfo) : FeedbackEmailType
data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User rate the app as "can be better" */
data class RateCanBeBetter(override val cardInfo: CardInfo) : FeedbackEmailType
data class RateCanBeBetter(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User has problem with scanning */
data object ScanningProblem : FeedbackEmailType {
override val cardInfo: CardInfo? = null
override val walletMetaInfo: WalletMetaInfo? = null
}
/** User has problem with sending transaction */
data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType
data class TransactionSendingProblem(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
/** User has problem with staking */
data class StakingProblem(
override val cardInfo: CardInfo,
override val walletMetaInfo: WalletMetaInfo,
val validatorName: String?,
val transactionTypes: List<String>,
val unsignedTransactions: List<String?>,
) : FeedbackEmailType
data class SwapProblem(
override val cardInfo: CardInfo,
override val walletMetaInfo: WalletMetaInfo,
val providerName: String,
val txId: String,
) : FeedbackEmailType
@ -46,23 +46,23 @@ sealed interface FeedbackEmailType {
* @property currencyName currency name
*/
data class CurrencyDescriptionError(val currencyId: String, val currencyName: String) : FeedbackEmailType {
override val cardInfo: CardInfo? = null
override val walletMetaInfo: WalletMetaInfo? = null
}
data class PreActivatedWallet(override val cardInfo: CardInfo) : FeedbackEmailType
data class PreActivatedWallet(override val walletMetaInfo: WalletMetaInfo) : FeedbackEmailType
data object CardAttestationFailed : FeedbackEmailType {
override val cardInfo: CardInfo? = null
override val walletMetaInfo: WalletMetaInfo? = null
}
sealed class Visa : FeedbackEmailType {
data class DirectUserRequest(override val cardInfo: CardInfo) : Visa()
data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : Visa()
data class Activation(override val cardInfo: CardInfo) : Visa()
data class Activation(override val walletMetaInfo: WalletMetaInfo) : Visa()
data class Dispute(
val visaTxDetails: VisaTxDetails,
override val cardInfo: CardInfo,
override val walletMetaInfo: WalletMetaInfo,
) : Visa()
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.feedback.models
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
data class WalletMetaInfo(
val userWalletId: UserWalletId?,
val hotWalletIsBackedUp: Boolean? = null,
val cardId: String? = null,
val firmwareVersion: String? = null,
val cardsCount: String? = null,
val cardBlockchain: String? = null,
val signedHashesList: List<SignedHashes>? = null,
val isImported: Boolean? = null,
val isStart2Coin: Boolean? = null,
val isVisa: Boolean? = null,
) {
@Serializable
data class SignedHashes(val curve: String, val total: String?)
}

View file

@ -44,13 +44,14 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString())
}
fun addCardInfo(cardInfo: CardInfo) {
builder.appendKeyValue("Card ID", cardInfo.cardId)
builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion)
builder.appendKeyValue("Linked cards count", cardInfo.cardsCount)
builder.appendKeyValue("Has seed phrase", cardInfo.isImported.toString())
builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain)
builder.appendSignedHashes(cardInfo.signedHashesList)
fun addUserWalletMetaInfo(walletMetaInfo: WalletMetaInfo) {
builder.appendKeyValue("Mobile Wallet is backed up", walletMetaInfo.hotWalletIsBackedUp?.toString())
builder.appendKeyValue("Card ID", walletMetaInfo.cardId)
builder.appendKeyValue("Firmware version", walletMetaInfo.firmwareVersion)
builder.appendKeyValue("Linked cards count", walletMetaInfo.cardsCount)
builder.appendKeyValue("Has seed phrase", walletMetaInfo.isImported?.toString())
builder.appendKeyValue("Card Blockchain", walletMetaInfo.cardBlockchain)
walletMetaInfo.signedHashesList?.let { builder.appendSignedHashes(it) }
}
fun addBlockchainInfoList(blockchainInfoList: List<BlockchainInfo>) {
@ -146,7 +147,7 @@ internal class FeedbackDataBuilder {
append("$keyValuePrefix$value\n")
}
private fun StringBuilder.appendSignedHashes(signedHashesList: List<CardInfo.SignedHashes>) {
private fun StringBuilder.appendSignedHashes(signedHashesList: List<WalletMetaInfo.SignedHashes>) {
signedHashesList.forEach {
appendKeyValue("Signed hashes [${it.curve}]", it.total)
}

View file

@ -1,23 +0,0 @@
package com.tangem.domain.feedback
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse
/**
* UseCase for creating 'CardInfo'
*
* @property feedbackRepository feedback repository
*
[REDACTED_AUTHOR]
*/
class GetCardInfoUseCase(
private val feedbackRepository: FeedbackRepository,
) {
operator fun invoke(scanResponse: ScanResponse): Either<Throwable, CardInfo> = catch {
feedbackRepository.getCardInfo(scanResponse)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.domain.feedback
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
/**
* UseCase for creating 'UserWalletMetaInfo' from [UserWalletId] or [ScanResponse]
*
* @property feedbackRepository feedback repository
*
[REDACTED_AUTHOR]
*/
class GetWalletMetaInfoUseCase(
private val feedbackRepository: FeedbackRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, WalletMetaInfo> = catch {
feedbackRepository.getUserWalletMetaInfo(userWalletId)
}
operator fun invoke(scanResponse: ScanResponse): Either<Throwable, WalletMetaInfo> = catch {
feedbackRepository.getUserWalletMetaInfo(scanResponse)
}
}

View file

@ -37,8 +37,8 @@ class SendFeedbackEmailUseCase(
private fun getAddress(type: FeedbackEmailType): String {
return when {
type is FeedbackEmailType.Visa || type.cardInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL
type.cardInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL
type is FeedbackEmailType.Visa || type.walletMetaInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL
type.walletMetaInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL
else -> TANGEM_SUPPORT_EMAIL
}
}

View file

@ -7,7 +7,9 @@ import java.io.File
interface FeedbackRepository {
fun getCardInfo(scanResponse: ScanResponse): CardInfo
suspend fun getUserWalletMetaInfo(userWalletId: UserWalletId): WalletMetaInfo
fun getUserWalletMetaInfo(scanResponse: ScanResponse): WalletMetaInfo
fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo

View file

@ -1,7 +1,7 @@
package com.tangem.domain.feedback.utils
import com.tangem.domain.feedback.FeedbackDataBuilder
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.visa.model.VisaTxDetails
@ -20,37 +20,40 @@ internal class EmailMessageBodyResolver(
/** Resolve email message body by [type] */
suspend fun resolve(type: FeedbackEmailType): String = with(FeedbackDataBuilder()) {
when (type) {
is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo)
is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo)
is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.walletMetaInfo)
is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.walletMetaInfo)
is FeedbackEmailType.StakingProblem -> addStakingProblemBody(type)
is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type)
is FeedbackEmailType.CurrencyDescriptionError -> addTokenInfo(type)
is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.ScanningProblem,
is FeedbackEmailType.CardAttestationFailed,
-> addPhoneInfoBody()
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.cardInfo, type.visaTxDetails)
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails)
}
return build()
}
private suspend fun FeedbackDataBuilder.addVisaRequestBody(cardInfo: CardInfo, visaTxDetails: VisaTxDetails) {
addUserRequestBody(cardInfo)
private suspend fun FeedbackDataBuilder.addVisaRequestBody(
walletMetaInfo: WalletMetaInfo,
visaTxDetails: VisaTxDetails,
) {
addUserRequestBody(walletMetaInfo)
addDelimiter()
addVisaTxInfo(visaTxDetails)
}
private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId))
private suspend fun FeedbackDataBuilder.addUserRequestBody(walletMetaInfo: WalletMetaInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(walletMetaInfo.userWalletId))
addDelimiter()
addCardInfo(cardInfo)
addUserWalletMetaInfo(walletMetaInfo)
addDelimiter()
val userWalletId = cardInfo.userWalletId
val userWalletId = walletMetaInfo.userWalletId
if (userWalletId != null) {
val blockchainInfoList = feedbackRepository.getBlockchainInfoList(userWalletId)
@ -68,11 +71,11 @@ internal class EmailMessageBodyResolver(
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(cardInfo: CardInfo) {
addCardInfo(cardInfo)
private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(walletMetaInfo: WalletMetaInfo) {
addUserWalletMetaInfo(walletMetaInfo)
addDelimiter()
val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" }
val userWalletId = requireNotNull(walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
@ -91,10 +94,10 @@ internal class EmailMessageBodyResolver(
}
private suspend fun FeedbackDataBuilder.addStakingProblemBody(type: FeedbackEmailType.StakingProblem) {
addCardInfo(type.cardInfo)
addUserWalletMetaInfo(type.walletMetaInfo)
addDelimiter()
val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" }
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
@ -120,10 +123,10 @@ internal class EmailMessageBodyResolver(
}
private suspend fun FeedbackDataBuilder.addSwapProblemBody(type: FeedbackEmailType.SwapProblem) {
addCardInfo(type.cardInfo)
addUserWalletMetaInfo(type.walletMetaInfo)
addDelimiter()
val userWalletId = requireNotNull(type.cardInfo.userWalletId) { "UserWalletId must be not null" }
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
@ -144,8 +147,8 @@ internal class EmailMessageBodyResolver(
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) {
addCardInfo(cardInfo)
private fun FeedbackDataBuilder.addCardAndPhoneInfo(walletMetaInfo: WalletMetaInfo) {
addUserWalletMetaInfo(walletMetaInfo)
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}

View file

@ -18,7 +18,7 @@ internal class EmailSubjectResolver(private val resources: Resources) {
fun resolve(type: FeedbackEmailType): String {
return when (type) {
is FeedbackEmailType.DirectUserRequest -> {
if (type.cardInfo.isStart2Coin) {
if (type.walletMetaInfo.isStart2Coin == true) {
resources.getStringSafe(R.string.feedback_subject_support)
} else {
resources.getStringSafe(R.string.feedback_subject_support_tangem)