Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-09 09:51:23 +08:00
parent eee206bec1
commit 675d13ab6f
15 changed files with 316 additions and 20 deletions

View file

@ -94,6 +94,7 @@ dependencies {
implementation(projects.data.visa) implementation(projects.data.visa)
implementation(projects.data.promo) implementation(projects.data.promo)
implementation(projects.data.onboarding) implementation(projects.data.onboarding)
implementation(projects.data.feedback)
/** Features */ /** Features */
implementation(projects.features.onboarding) implementation(projects.features.onboarding)

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

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,42 @@
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(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
// endregion
// Other libraries
implementation(deps.timber)
// endregion
// region Core modules
implementation(projects.core.datasource)
// endregion
implementation(projects.core.utils)
implementation(projects.domain.feedback)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
}

View file

@ -0,0 +1,104 @@
package com.tangem.data.feedback
import android.content.Context
import android.content.pm.PackageInfo
import android.os.Build
import com.tangem.data.feedback.converters.BlockchainInfoConverter
import com.tangem.data.feedback.converters.CardInfoConverter
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.models.AppLogModel
import com.tangem.domain.feedback.models.BlockchainInfo
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.PhoneInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.wallets.models.UserWallet
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
/**
* Implementation of [FeedbackRepository]
*
* @property appPreferencesStore application preferences store
* @property userWalletsStore user wallets store
* @property walletManagersStore wallet managers store
* @property context context for getting app version
*
[REDACTED_AUTHOR]
*/
internal class DefaultFeedbackRepository(
private val appPreferencesStore: AppPreferencesStore,
private val userWalletsStore: UserWalletsStore,
private val walletManagersStore: WalletManagersStore,
private val context: Context,
) : FeedbackRepository {
override suspend fun getCardInfo(): CardInfo {
return CardInfoConverter.convert(value = getSelectedUserWallet())
}
override suspend fun getBlockchainInfoList(): List<BlockchainInfo> {
return walletManagersStore
.getAllSync(userWalletId = getSelectedUserWallet().walletId)
.map(BlockchainInfoConverter::convert)
}
override fun getPhoneInfo(): PhoneInfo {
return PhoneInfo(
phoneModel = Build.MODEL,
osVersion = Build.VERSION.SDK_INT.toString(),
appVersion = getAppVersion(),
)
}
override suspend fun getAppLogs(): List<AppLogModel> {
return appPreferencesStore.getObjectMap<String>(key = PreferencesKeys.APP_LOGS_KEY)
.map { AppLogModel(timestamp = it.key.toLong(), message = it.value) }
}
override suspend fun createLogFile(logs: List<String>): File? {
return try {
val file = File(context.filesDir, LOGS_FILE)
file.delete()
file.createNewFile()
val stringWriter = StringWriter()
logs.forEach(stringWriter::append)
val fileWriter = FileWriter(file)
fileWriter.write(stringWriter.toString())
fileWriter.close()
file
} catch (ex: Exception) {
Timber.e(ex, "Logs file isn't created")
null
}
}
private fun getAppVersion(): String {
return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) }
.fold(
onSuccess = PackageInfo::versionName,
onFailure = {
Timber.e(it)
"x.y.z"
},
)
}
private fun getSelectedUserWallet(): UserWallet {
return userWalletsStore.selectedUserWalletOrNull
?: error("UserWallet is not selected")
}
private companion object {
const val LOGS_FILE = "logs.txt"
}
}

View file

@ -0,0 +1,43 @@
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)
},
)
}
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,29 @@
package com.tangem.data.feedback.converters
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.converter.Converter
/**
* Converter from [UserWallet] to [CardInfo]
*
[REDACTED_AUTHOR]
*/
internal object CardInfoConverter : Converter<UserWallet, CardInfo> {
override fun convert(value: UserWallet): CardInfo {
return with(value.scanResponse) {
CardInfo(
userWalletId = value.walletId.stringValue,
cardId = card.cardId,
firmwareVersion = card.firmwareVersion.stringValue,
cardBlockchain = walletData?.blockchain,
signedHashesList = card.wallets.map {
CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
},
isStart2Coin = value.scanResponse.card.isStart2Coin,
)
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.data.feedback.di
import android.content.Context
import com.tangem.data.feedback.DefaultFeedbackRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.repository.FeedbackRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeedbackRepositoryModule {
@Provides
@Singleton
fun provideFeedbackRepository(
appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore,
walletManagersStore: WalletManagersStore,
@ApplicationContext context: Context,
): FeedbackRepository {
return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context)
}
}

View file

@ -3,10 +3,8 @@ package com.tangem.data.settings
import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.settings.models.AppLogsModel
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -36,11 +34,6 @@ internal class DefaultSettingsRepository(
) )
} }
override suspend fun getAppLogs(): List<AppLogsModel> {
return appPreferencesStore.getObjectMap<String>(key = PreferencesKeys.APP_LOGS_KEY)
.map { AppLogsModel(timestamp = it.key.toLong(), message = it.value) }
}
override suspend fun updateAppLogs(message: String) { override suspend fun updateAppLogs(message: String) {
val newLogs = DateTime.now().millis.toString() to message val newLogs = DateTime.now().millis.toString() to message

View file

@ -4,6 +4,7 @@ import com.tangem.domain.feedback.models.BlockchainInfo
import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.PhoneInfo import com.tangem.domain.feedback.models.PhoneInfo
import com.tangem.domain.feedback.utils.breakLine import com.tangem.domain.feedback.utils.breakLine
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
internal class FeedbackDataBuilder { internal class FeedbackDataBuilder {
@ -40,8 +41,16 @@ internal class FeedbackDataBuilder {
} }
builder.appendKeyValue("Host", host) builder.appendKeyValue("Host", host)
builder.appendKeyValue("Wallet address", addresses)
builder.appendKeyValue("Explorer link", explorerLink) builder.appendAddresses(
key = "Wallet address${addresses.isMultiple(suffix = "es")}",
addresses = addresses,
)
builder.appendAddresses(
key = "Explorer link${explorerLinks.isMultiple(suffix = "s")}",
addresses = explorerLinks,
)
if (!isLastIndex) builder.appendDelimiter() if (!isLastIndex) builder.appendDelimiter()
} }
@ -55,7 +64,7 @@ internal class FeedbackDataBuilder {
fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun addDelimiter(): StringBuilder = builder.appendDelimiter()
fun build(): String = builder.toString() fun build(): String = builder.trimEnd().toString()
private fun StringBuilder.appendKeyValue(key: String, value: String?) { private fun StringBuilder.appendKeyValue(key: String, value: String?) {
if (value.isNullOrBlank()) return if (value.isNullOrBlank()) return
@ -70,6 +79,24 @@ internal class FeedbackDataBuilder {
} }
} }
private fun StringBuilder.appendAddresses(key: String, addresses: BlockchainAddresses) {
appendKeyValue(
key = key,
value = when (addresses) {
is BlockchainInfo.Addresses.Multiple -> {
addresses.values.joinToString(separator = "\n", prefix = "\n") {
"${it.type}${it.value}"
}
}
is BlockchainInfo.Addresses.Single -> addresses.value
},
)
}
private fun BlockchainAddresses.isMultiple(suffix: String): String {
return if (this is BlockchainAddresses.Multiple) suffix else ""
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n") private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
private inline fun List<BlockchainInfo>.forEachBlockchain(action: BlockchainInfo.(Boolean) -> Unit) { private inline fun List<BlockchainInfo>.forEachBlockchain(action: BlockchainInfo.(Boolean) -> Unit) {

View file

@ -0,0 +1,3 @@
package com.tangem.domain.feedback.models
data class AppLogModel(val timestamp: Long, val message: String)

View file

@ -5,11 +5,21 @@ data class BlockchainInfo(
val derivationPath: String, val derivationPath: String,
val outputsCount: String?, val outputsCount: String?,
val host: String, val host: String,
val addresses: String, val addresses: Addresses,
val explorerLink: String, val explorerLinks: Addresses,
val tokens: List<TokenInfo>, val tokens: List<TokenInfo>,
) { ) {
sealed class Addresses {
data class Single(val value: String) : Addresses()
data class Multiple(val values: List<AddressInfo>) : Addresses() {
data class AddressInfo(val type: String, val value: String)
}
}
data class TokenInfo( data class TokenInfo(
val id: String?, val id: String?,
val name: String, val name: String,

View file

@ -0,0 +1,20 @@
package com.tangem.domain.feedback.repository
import com.tangem.domain.feedback.models.AppLogModel
import com.tangem.domain.feedback.models.BlockchainInfo
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.PhoneInfo
import java.io.File
interface FeedbackRepository {
suspend fun getCardInfo(): CardInfo
suspend fun getBlockchainInfoList(): List<BlockchainInfo>
fun getPhoneInfo(): PhoneInfo
suspend fun getAppLogs(): List<AppLogModel>
suspend fun createLogFile(logs: List<String>): File?
}

View file

@ -1,3 +0,0 @@
package com.tangem.domain.settings.models
data class AppLogsModel(val timestamp: Long, val message: String)

View file

@ -1,7 +1,5 @@
package com.tangem.domain.settings.repositories package com.tangem.domain.settings.repositories
import com.tangem.domain.settings.models.AppLogsModel
interface SettingsRepository { interface SettingsRepository {
suspend fun shouldShowSaveUserWalletScreen(): Boolean suspend fun shouldShowSaveUserWalletScreen(): Boolean
@ -10,9 +8,6 @@ interface SettingsRepository {
suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean)
@Throws
suspend fun getAppLogs(): List<AppLogsModel>
@Throws @Throws
suspend fun updateAppLogs(message: String) suspend fun updateAppLogs(message: String)

View file

@ -157,4 +157,5 @@ include(":data:transaction")
include(":data:visa") include(":data:visa")
include(":data:promo") include(":data:promo")
include(":data:onboarding") include(":data:onboarding")
include(":data:feedback")
// endregion Data modules // endregion Data modules