Updated on 2026-08-14
This commit is contained in:
parent
eee206bec1
commit
675d13ab6f
15 changed files with 316 additions and 20 deletions
|
|
@ -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
1
data/feedback/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
42
data/feedback/build.gradle.kts
Normal file
42
data/feedback/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.tangem.domain.feedback.models
|
||||||
|
|
||||||
|
data class AppLogModel(val timestamp: Long, val message: String)
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
package com.tangem.domain.settings.models
|
|
||||||
|
|
||||||
data class AppLogsModel(val timestamp: Long, val message: String)
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue