Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 18:55:37 +03:00
parent 330ae73357
commit e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions

1
data/feedback/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,46 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.feedback"
}
dependencies {
// region AndroidX libraries
implementation(deps.androidx.datastore)
// endregion
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Tangem libraries
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
// endregion
// Other libraries
implementation(deps.timber)
// endregion
// region Core modules
implementation(projects.core.datasource)
implementation(projects.core.navigation)
implementation(projects.core.utils)
// endregion
implementation(projects.domain.feedback)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.libs.blockchainSdk)
}

View file

@ -0,0 +1,107 @@
package com.tangem.data.feedback
import android.os.Build
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.navigation.email.EmailSender
import com.tangem.data.feedback.converters.BlockchainInfoConverter
import com.tangem.data.feedback.converters.CardInfoConverter
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.models.*
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.version.AppVersionProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
import java.io.File
/**
* Implementation of [FeedbackRepository]
*
* @property appLogsStore app logs store
* @property userWalletsListManager user wallets list manager
* @property walletManagersStore wallet managers store
* @property emailSender email sender
* @property appVersionProvider app version provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultFeedbackRepository(
private val appLogsStore: AppLogsStore,
private val userWalletsListManager: UserWalletsListManager,
private val walletManagersStore: WalletManagersStore,
private val emailSender: EmailSender,
private val appVersionProvider: AppVersionProvider,
) : FeedbackRepository {
private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap())
override fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse)
override fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo {
return UserWalletsInfo(
selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated",
totalUserWallets = userWalletsListManager.walletsCount,
)
}
override suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo> {
return walletManagersStore
.getAllSync(userWalletId = userWalletId)
.map(BlockchainInfoConverter::convert)
}
override suspend fun getBlockchainInfo(
userWalletId: UserWalletId,
blockchainId: String,
derivationPath: String?,
): BlockchainInfo? {
return walletManagersStore
.getSyncOrNull(
userWalletId = userWalletId,
blockchain = Blockchain.fromId(blockchainId),
derivationPath = derivationPath,
)
?.let(BlockchainInfoConverter::convert)
}
override fun getPhoneInfo(): PhoneInfo {
return PhoneInfo(
phoneModel = Build.MODEL,
osVersion = Build.VERSION.SDK_INT.toString(),
appVersion = appVersionProvider.versionName,
)
}
override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected")
blockchainsErrors.update {
it.toMutableMap().apply {
put(userWallet.walletId, error)
}
}
}
override fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? {
return blockchainsErrors.value[userWalletId].also {
if (it == null) Timber.e("Blockchain error info is null for $userWalletId")
}
}
override fun getLogFile(): File? = appLogsStore.getFile()
override fun sendEmail(feedbackEmail: FeedbackEmail) {
emailSender.send(
EmailSender.Email(
address = feedbackEmail.address,
subject = feedbackEmail.subject,
message = feedbackEmail.message,
attachment = feedbackEmail.file,
),
)
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.data.feedback.converters
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
import com.tangem.domain.feedback.models.BlockchainInfo
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses.Multiple.AddressInfo
import com.tangem.utils.converter.Converter
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
/**
* Converter from [WalletManager] to [BlockchainInfo]
*
[REDACTED_AUTHOR]
*/
internal object BlockchainInfoConverter : Converter<WalletManager, BlockchainInfo> {
override fun convert(value: WalletManager): BlockchainInfo {
return BlockchainInfo(
blockchain = value.wallet.blockchain.fullName,
derivationPath = value.wallet.publicKey.derivationPath?.rawPath ?: "",
outputsCount = value.outputsCount?.toString(),
host = value.currentHost,
addresses = value.wallet.mapAddresses(Address::value),
explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) },
tokens = value.cardTokens.map { token ->
BlockchainInfo.TokenInfo(
id = token.id,
name = token.name,
contractAddress = token.contractAddress,
decimals = token.decimals.toString(),
)
},
)
}
private inline fun Wallet.mapAddresses(map: (Address) -> String): BlockchainAddresses {
return if (addresses.size == 1) {
BlockchainAddresses.Single(value = map(addresses.first()))
} else {
BlockchainAddresses.Multiple(
addresses.map {
AddressInfo(type = it.type.name, value = map(it))
},
)
}
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.data.feedback.converters
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.util.getBackupCardsCount
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.converter.Converter
/**
* Converter from [ScanResponse] to [CardInfo]
*
[REDACTED_AUTHOR]
*/
internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
override fun convert(value: ScanResponse): CardInfo {
return with(value) {
CardInfo(
userWalletId = createUserWalletId(scanResponse = value),
cardId = card.cardId,
cardsCount = value.getBackupCardsCount()?.toString() ?: "0",
firmwareVersion = card.firmwareVersion.stringValue,
cardBlockchain = walletData?.blockchain,
signedHashesList = card.wallets.map {
CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
},
isImported = value.card.wallets.any(CardDTO.Wallet::isImported),
isStart2Coin = value.card.isStart2Coin,
)
}
}
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? {
return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build()
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.data.feedback.di
import com.tangem.core.navigation.email.EmailSender
import com.tangem.data.feedback.DefaultFeedbackRepository
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeedbackRepositoryModule {
@Provides
@Singleton
fun provideFeedbackRepository(
appLogsStore: AppLogsStore,
userWalletsListManager: UserWalletsListManager,
walletManagersStore: WalletManagersStore,
emailSender: EmailSender,
appVersionProvider: AppVersionProvider,
): FeedbackRepository {
return DefaultFeedbackRepository(
appLogsStore = appLogsStore,
userWalletsListManager = userWalletsListManager,
walletManagersStore = walletManagersStore,
emailSender = emailSender,
appVersionProvider = appVersionProvider,
)
}
}