diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 9f6c1aa883..4bf2f7eb2a 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -3,6 +3,7 @@ package com.tangem.data.feedback import android.content.Context import android.content.pm.PackageInfo import android.os.Build +import com.tangem.blockchain.common.Blockchain import com.tangem.data.feedback.converters.BlockchainInfoConverter import com.tangem.data.feedback.converters.CardInfoConverter import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -13,6 +14,9 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import timber.log.Timber import java.io.File import java.io.FileWriter @@ -35,6 +39,8 @@ internal class DefaultFeedbackRepository( private val context: Context, ) : FeedbackRepository { + private val blockchainsErrors = MutableStateFlow>(emptyMap()) + override suspend fun getUserWalletsInfo(): UserWalletsInfo { return UserWalletsInfo( selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, @@ -52,6 +58,16 @@ internal class DefaultFeedbackRepository( .map(BlockchainInfoConverter::convert) } + override suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? { + return walletManagersStore + .getSyncOrNull( + userWalletId = getSelectedUserWallet().walletId, + blockchain = Blockchain.fromId(blockchainId), + derivationPath = derivationPath, + ) + ?.let(BlockchainInfoConverter::convert) + } + override fun getPhoneInfo(): PhoneInfo { return PhoneInfo( phoneModel = Build.MODEL, @@ -60,6 +76,22 @@ internal class DefaultFeedbackRepository( ) } + override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { + blockchainsErrors.update { + it.toMutableMap().apply { + put(getSelectedUserWallet().walletId, error) + } + } + } + + override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? { + return blockchainsErrors.value[getSelectedUserWallet().walletId].also { + if (it == null) { + Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}") + } + } + } + override suspend fun getAppLogs(): List { return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 3e7859b754..6a21560035 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(deps.jodatime) implementation(projects.core.res) + implementation(projects.domain.wallets.models) } \ No newline at end of file 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 5350902e33..a671bad033 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 @@ -1,9 +1,6 @@ package com.tangem.domain.feedback -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.models.UserWalletsInfo +import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.utils.breakLine import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses @@ -68,6 +65,24 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("App version", phoneInfo.appVersion) } + fun addBlockchainError(info: BlockchainInfo, error: BlockchainErrorInfo) { + builder.appendKeyValue("Blockchain", info.blockchain) + builder.appendKeyValue("Derivation path", info.derivationPath) + builder.appendKeyValue("Host", info.host) + builder.appendKeyValue("Token", error.tokenSymbol) + builder.appendKeyValue("Error", error.errorMessage) + + builder.appendDelimiter() + + builder.appendAddresses( + key = "Source address${info.addresses.isMultiple(suffix = "es")}", + addresses = info.addresses, + ) + builder.appendKeyValue("Destination address", error.destinationAddress) + builder.appendKeyValue("Amount", error.amount) + builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") + } + fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun build(): String = builder.trimEnd().toString() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt new file mode 100644 index 0000000000..596699a073 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.repository.FeedbackRepository + +/** + * Save last blockchain error + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +class SaveBlockchainErrorUseCase( + private val feedbackRepository: FeedbackRepository, +) { + + fun invoke(error: BlockchainErrorInfo) { + feedbackRepository.saveBlockchainErrorInfo(error = error) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt new file mode 100644 index 0000000000..fc76ba451b --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.feedback.models + +/** + * Information about blockchain's operation error + * + * @property errorMessage message about error + * @property blockchainId blockchain id + * @property derivationPath derivation path + * @property destinationAddress destination address + * @property tokenSymbol token symbol or null, if it isn't operation with token + * @property amount amount + * @property fee fee or null, if unable to get + */ +data class BlockchainErrorInfo( + val errorMessage: String, + val blockchainId: String, + val derivationPath: String?, + val destinationAddress: String, + val tokenSymbol: String?, + val amount: String, + val fee: String?, +) \ 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 new file mode 100644 index 0000000000..6bbd5a9670 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.feedback.models + +/** + * Email feedback type + * +[REDACTED_AUTHOR] + */ +sealed interface FeedbackEmailType { + + /** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */ + data object DirectUserRequest : FeedbackEmailType + + /** User rate the app as "can be better" */ + data object RateCanBeBetter : FeedbackEmailType + + /** User has problem with scanning */ + data object ScanningProblem : FeedbackEmailType + + /** User has problem with sending transaction */ + data object TransactionSendingProblem : FeedbackEmailType +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index 58281067c1..c9d6806eea 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -11,8 +11,14 @@ interface FeedbackRepository { suspend fun getBlockchainInfoList(): List + suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? + fun getPhoneInfo(): PhoneInfo + fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) + + suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? + suspend fun getAppLogs(): List suspend fun createLogFile(logs: String): File? diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt new file mode 100644 index 0000000000..e5596b2a78 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.feedback.utils + +import com.tangem.domain.feedback.models.AppLogModel +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormatter +import org.joda.time.format.DateTimeFormatterBuilder +import java.util.Locale + +/** + * App logs formatter + * +[REDACTED_AUTHOR] + */ +internal class AppLogsFormatter { + + private val dateFormatter = createDateFormatter() + + /** Format [appLogs] to [String] */ + fun format(appLogs: List): String { + val builder = StringBuilder() + + var sum = 0 + for (i in appLogs.lastIndex downTo 0) { + val log = appLogs[i] + val date = dateFormatter.print(DateTime(log.timestamp)) + + val formattedLog = "$date: ${log.message}\n" + + sum += formattedLog.length + if (sum < GMAIL_MAX_FILE_SIZE) { + builder.insert(0, formattedLog) + } else { + break + } + } + + return builder.toString() + } + + // Example, 00.00 00:00:00.000 + private fun createDateFormatter(): DateTimeFormatter { + return DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral('.') + .appendMonthOfYear(2) + .appendLiteral(' ') + .appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2) + .appendLiteral('.') + .appendMillisOfSecond(MIN_MILLIS_DIGITS) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + private companion object { + const val MIN_MILLIS_DIGITS = 3 + } +} \ 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 new file mode 100644 index 0000000000..8574fb1010 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -0,0 +1,70 @@ +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.FeedbackEmailType +import com.tangem.domain.feedback.repository.FeedbackRepository + +/** + * Email message body resolver + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +internal class EmailMessageBodyResolver( + private val feedbackRepository: FeedbackRepository, +) { + + /** Resolve email message body by [type] using [cardInfo] */ + suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) { + when (type) { + FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo) + FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo) + FeedbackEmailType.ScanningProblem -> addScanningProblemBody() + FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo) + } + + return build() + } + + private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) + addDelimiter() + addCardInfo(cardInfo) + addDelimiter() + addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private fun FeedbackDataBuilder.addScanningProblemBody() { + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(cardInfo: CardInfo) { + addCardInfo(cardInfo) + addDelimiter() + + val blockchainError = feedbackRepository.getBlockchainErrorInfo() + val blockchainInfo = blockchainError?.let { + feedbackRepository.getBlockchainInfo( + blockchainId = blockchainError.blockchainId, + derivationPath = blockchainError.derivationPath, + ) + } + + if (blockchainInfo != null) { + addBlockchainError(blockchainInfo, blockchainError) + addDelimiter() + } + + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) { + addCardInfo(cardInfo) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } +} \ 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 new file mode 100644 index 0000000000..38c983c617 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.feedback.utils + +import android.content.res.Resources +import com.tangem.domain.feedback.R +import com.tangem.domain.feedback.models.FeedbackEmailType + +/** + * Email message title resolver + * + * @property resources resources + * +[REDACTED_AUTHOR] + */ +internal class EmailMessageTitleResolver(private val resources: Resources) { + + /** Resolve email message title by [type] */ + fun resolve(type: FeedbackEmailType): String { + return when (type) { + FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support + FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative + FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed + FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + } + .let(resources::getString) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..392fa7d2f4 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.feedback.utils + +import android.content.res.Resources +import com.tangem.domain.feedback.R +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.FeedbackEmailType + +/** + * Email subject resolver + * + * @property resources resources + * +[REDACTED_AUTHOR] + */ +internal class EmailSubjectResolver(private val resources: Resources) { + + /** Resolve email message body by [type] using [cardInfo] */ + fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String { + return when (type) { + FeedbackEmailType.DirectUserRequest -> { + if (cardInfo.isStart2Coin) { + R.string.feedback_subject_support + } else { + R.string.feedback_subject_support_tangem + } + } + FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative + FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed + FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + } + .let(resources::getString) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt new file mode 100644 index 0000000000..aeaa917e5f --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.feedback.utils + +internal const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB + +internal const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" +internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" \ No newline at end of file