diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 8e0a528fca..b8830c622b 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -242,6 +242,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac appRouterConfig.routerScope = lifecycleScope appRouterConfig.componentRouter = routingComponent.router + appRouterConfig.snackbarHandler = this routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack -> appRouterConfig.stack = childStack.backStack diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt index 7a7e2c72b0..1b3f599f3f 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt @@ -3,19 +3,19 @@ package com.tangem.tap.common.log import android.util.Log import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.Logger -import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.wallet.BuildConfig import timber.log.Timber /** * Tangem app logger * - * @property settingsRepository repository for saving logs + * @property appLogsStore app logs store * [REDACTED_AUTHOR] */ class TangemAppLoggerInitializer( - private val settingsRepository: SettingsRepository, + private val appLogsStore: AppLogsStore, ) { /** Initialize */ @@ -35,7 +35,7 @@ class TangemAppLoggerInitializer( } if (PERMITTED_PRIORITY.contains(priority)) { - settingsRepository.saveLogMessage(message) + appLogsStore.saveLogMessage(message) } } } diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt b/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt index fad6b562f7..ef740906db 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt @@ -3,26 +3,26 @@ package com.tangem.tap.common.log import com.tangem.Log import com.tangem.LogFormat import com.tangem.TangemSdkLogger -import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.datasource.local.logs.AppLogsStore /** * CardSDK logger implementation * - * @property levels logging levels - * @property messageFormatter message formatter - * @property settingsRepository settings repository + * @property levels logging levels + * @property messageFormatter message formatter + * @property appLogsStore app logs store * [REDACTED_AUTHOR] */ internal class TangemCardSDKLogger( private val levels: List, private val messageFormatter: LogFormat, - private val settingsRepository: SettingsRepository, + private val appLogsStore: AppLogsStore, ) : TangemSdkLogger { override fun log(message: () -> String, level: Log.Level) { if (!levels.contains(level)) return - settingsRepository.saveLogMessage(message = messageFormatter.format(message, level)) + appLogsStore.saveLogMessage(message = messageFormatter.format(message, level)) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index 21cca072f7..a544190398 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -9,7 +9,7 @@ import timber.log.Timber val logMiddleware: Middleware = { _, _ -> { nextDispatch -> { action -> - Timber.i("Dispatch action: $action") + Timber.i("Dispatch action: ${action::class.java.simpleName}") nextDispatch(action) } } diff --git a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt index c275de1626..a85c3e4667 100644 --- a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt @@ -1,20 +1,20 @@ package com.tangem.tap.data import com.tangem.blockchain.common.logging.BlockchainSDKLogger -import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.datasource.local.logs.AppLogsStore /** * BlockchainSDK logger implementation * - * @property settingsRepository settings repository + * @property appLogsStore app logs store * [REDACTED_AUTHOR] */ internal class TangemBlockchainSDKLogger( - private val settingsRepository: SettingsRepository, + private val appLogsStore: AppLogsStore, ) : BlockchainSDKLogger { override fun log(level: BlockchainSDKLogger.Level, message: String) { - settingsRepository.saveLogMessage(message) + appLogsStore.saveLogMessage(message) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/TangemLoggingModule.kt b/app/src/main/java/com/tangem/tap/di/data/TangemLoggingModule.kt index e361ca70a6..400f58d8b2 100644 --- a/app/src/main/java/com/tangem/tap/di/data/TangemLoggingModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/TangemLoggingModule.kt @@ -4,7 +4,7 @@ import com.tangem.Log import com.tangem.LogFormat import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.logging.BlockchainSDKLogger -import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.log.TangemCardSDKLogger import com.tangem.tap.data.TangemBlockchainSDKLogger @@ -20,13 +20,13 @@ internal object TangemLoggingModule { @Provides @Singleton - fun provideAppLoggerInitializer(settingsRepository: SettingsRepository): TangemAppLoggerInitializer { - return TangemAppLoggerInitializer(settingsRepository) + fun provideAppLoggerInitializer(appLogsStore: AppLogsStore): TangemAppLoggerInitializer { + return TangemAppLoggerInitializer(appLogsStore) } @Provides @Singleton - fun provideCardSDKLogger(settingsRepository: SettingsRepository): TangemSdkLogger { + fun provideCardSDKLogger(appLogsStore: AppLogsStore): TangemSdkLogger { val logLevels = listOf( Log.Level.ApduCommand, Log.Level.Apdu, @@ -44,13 +44,13 @@ internal object TangemLoggingModule { return TangemCardSDKLogger( levels = logLevels, messageFormatter = LogFormat.StairsFormatter(), - settingsRepository = settingsRepository, + appLogsStore = appLogsStore, ) } @Provides @Singleton - fun provideBlockchainSDKLogger(settingsRepository: SettingsRepository): BlockchainSDKLogger { - return TangemBlockchainSDKLogger(settingsRepository) + fun provideBlockchainSDKLogger(appLogsStore: AppLogsStore): BlockchainSDKLogger { + return TangemBlockchainSDKLogger(appLogsStore) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 3e8529f150..3914707cc3 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -50,9 +50,7 @@ class WalletConnectSdkHelper { @Suppress("MagicNumber") suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData { val transaction = data.transaction - val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) { - "Blockchain not found" - } + val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) { "Blockchain not found" } val walletManager = requireNotNull(getWalletManager(blockchain, data.rawDerivationPath)) { "WalletManager not found" } @@ -73,27 +71,31 @@ class WalletConnectSdkHelper { "Transaction amount is null" } + // TODO move fee calculation to SDK getFee() [REDACTED_JIRA] val gasLimit = getGasLimitFromTx(value, walletManager, transaction) + val gasPrice = getGasPrice(walletManager, transaction) - val gasPrice = transaction.gasPrice?.hexToBigDecimal() - ?: when (val result = (walletManager as? EthereumGasLoader)?.getGasPrice()) { - is Result.Success -> result.data.toBigDecimal() - is Result.Failure -> { - (result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") } - - error("Unable to get gas price: ${result.error}") - } - null -> error("Gas price is null") - } - - val fee = (gasLimit * gasPrice).movePointLeft(decimals) - val total = value + fee + val feeDecimal = (gasLimit * gasPrice).movePointLeft(decimals) + val total = value + feeDecimal + val feeAmount = Amount(feeDecimal, wallet.blockchain) val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" } + val fee = if (blockchain.isEvm()) { + // TODO [REDACTED_JIRA] + // workaround for Mantle, remove after [REDACTED_JIRA] + val patchedAmount = if (blockchain == Blockchain.Mantle) { + feeAmount.copy(value = feeAmount.value?.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)) + } else { + feeAmount + } + Fee.Ethereum(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) + } else { + Fee.Common(feeAmount) + } val transactionData = TransactionData.Uncompiled( amount = Amount(value, wallet.blockchain), - fee = Fee.Common(Amount(fee, wallet.blockchain)), + fee = fee, sourceAddress = transaction.from, destinationAddress = destinationAddress, extras = EthereumTransactionExtras( @@ -107,7 +109,7 @@ class WalletConnectSdkHelper { dAppName = data.metaName, dAppUrl = data.metaUrl, amount = value.toFormattedString(decimals), - gasAmount = fee.toFormattedString(decimals), + feeAmount = feeDecimal.toFormattedString(decimals), totalAmount = total.toFormattedString(decimals), balance = balance.toFormattedString(decimals), isEnoughFundsToSend = balance - total >= BigDecimal.ZERO, @@ -148,6 +150,22 @@ class WalletConnectSdkHelper { } } + private suspend fun getGasPrice(walletManager: WalletManager, transaction: WcEthereumTransaction): BigDecimal { + val txGasPrice = transaction.gasPrice?.hexToBigDecimal() + if (txGasPrice != null) { + return txGasPrice + } + return when (val result = (walletManager as? EthereumGasLoader)?.getGasPrice()) { + is Result.Success -> result.data.toBigDecimal() + is Result.Failure -> { + (result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") } + + error("Unable to get gas price: ${result.error}") + } + null -> error("Gas price is null") + } + } + private suspend fun getGasLimitFromTx( value: BigDecimal, walletManager: WalletManager, @@ -155,14 +173,14 @@ class WalletConnectSdkHelper { ): BigDecimal { return transaction.gas?.hexToBigDecimal() ?: transaction.gasLimit?.hexToBigDecimal() - ?: getGaLimitFromBlockchain( + ?: getGasLimitFromBlockchain( value = value, walletManager = walletManager, transaction = transaction, ) } - private suspend fun getGaLimitFromBlockchain( + private suspend fun getGasLimitFromBlockchain( value: BigDecimal, walletManager: WalletManager, transaction: WcEthereumTransaction, @@ -418,5 +436,7 @@ class WalletConnectSdkHelper { const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" const val HEX_PREFIX = "0x" const val DEFAULT_MAX_GASLIMIT = 350000 + // TODO remove after [REDACTED_JIRA] + private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.6") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt index 3ec2d8bfda..4fd49641fb 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt @@ -18,7 +18,7 @@ object TransactionDialog { data.dAppName, data.dAppUrl, data.amount, - data.gasAmount, + data.feeAmount, data.totalAmount, data.balance, ) @@ -51,7 +51,7 @@ data class TransactionRequestDialogData( val dAppName: String, val dAppUrl: String, val amount: String, - val gasAmount: String, + val feeAmount: String, val totalAmount: String, val balance: String, val isEnoughFundsToSend: Boolean, diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index 592656008e..d744032000 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -1,10 +1,12 @@ package com.tangem.tap.routing +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.navigation.Router import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.wallet.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -31,37 +33,59 @@ internal class ProxyAppRouter( } override fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { - routerScope.launch(dispatchers.mainImmediate) { - Timber.i("Push route: $route") + safeNavigate(onComplete, message = "Push $route") { innerRouter.push(route, onComplete) } } override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { - routerScope.launch(dispatchers.mainImmediate) { - Timber.i("Replace all routes with $routes") + safeNavigate(onComplete, message = "Replace all routes with $routes") { innerRouter.replaceAll(*routes, onComplete = onComplete) } } override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { - routerScope.launch(dispatchers.mainImmediate) { - Timber.i("Pop route") + safeNavigate(onComplete, message = "Pop route") { innerRouter.pop(onComplete) } } override fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { - routerScope.launch(dispatchers.mainImmediate) { - Timber.i("Pop to route: $route") + safeNavigate(onComplete, message = "Pop to $route") { innerRouter.popTo(route, onComplete) } } override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { - routerScope.launch(dispatchers.mainImmediate) { - Timber.i("Pop to route class: $routeClass") + safeNavigate(onComplete, message = "Pop to $routeClass") { innerRouter.popTo(routeClass, onComplete) } } + + private fun safeNavigate(onComplete: (isSuccess: Boolean) -> Unit, message: String, block: () -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + Timber.i(message) + + try { + block() + } catch (e: Throwable) { + onComplete(false) + } + } + } + + override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) { + if (!isSuccess) { + FirebaseCrashlytics.getInstance().recordException(RuntimeException(errorMessage)) + Timber.w(errorMessage) + + with(receiver = config.snackbarHandler ?: return) { + showSnackbar( + text = R.string.common_unknown_error, + buttonTitle = R.string.common_ok, + action = { dismissSnackbar() }, + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt b/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt index 470c85f674..5c2c9b3843 100644 --- a/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt +++ b/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt @@ -2,6 +2,7 @@ package com.tangem.tap.routing.configurator import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.navigation.Router +import com.tangem.tap.common.SnackbarHandler import kotlinx.coroutines.CoroutineScope internal interface AppRouterConfig { @@ -9,4 +10,7 @@ internal interface AppRouterConfig { var routerScope: CoroutineScope? var componentRouter: Router? var stack: List? + + // TODO: Replace with UI message handler: [REDACTED_JIRA] + var snackbarHandler: SnackbarHandler? } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt b/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt index ae724ac04e..d1b1e93f7c 100644 --- a/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt +++ b/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt @@ -2,6 +2,7 @@ package com.tangem.tap.routing.configurator import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.navigation.Router +import com.tangem.tap.common.SnackbarHandler import kotlinx.coroutines.CoroutineScope internal class MutableAppRouterConfig : AppRouterConfig { @@ -9,4 +10,5 @@ internal class MutableAppRouterConfig : AppRouterConfig { override var routerScope: CoroutineScope? = null override var componentRouter: Router? = null override var stack: List? = null + override var snackbarHandler: SnackbarHandler? = null } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt index cb0f21a793..9f8d5f4111 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt @@ -23,7 +23,12 @@ interface AppRouter { * @param route The route to push. * @param onComplete The callback to be invoked when the operation is complete. */ - fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + fun push( + route: AppRoute, + onComplete: (isSuccess: Boolean) -> Unit = { isSuccess -> + defaultCompletionHandler(isSuccess, errorMessage = "Unable to push $route") + }, + ) /** * Replaces ***all*** routes in the navigation stack with the specified [routes]. @@ -31,14 +36,23 @@ interface AppRouter { * @param routes The routes to replace the current stack with. * @param onComplete The callback to be invoked when the operation is complete. */ - fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + fun replaceAll( + vararg routes: AppRoute, + onComplete: (isSuccess: Boolean) -> Unit = { isSuccess -> + defaultCompletionHandler(isSuccess, errorMessage = "Unable to replace routes with $routes") + }, + ) /** * Pops the top route from the navigation stack. * * @param onComplete The callback to be invoked when the operation is complete. */ - fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) + fun pop( + onComplete: (isSuccess: Boolean) -> Unit = { isSuccess -> + defaultCompletionHandler(isSuccess, errorMessage = "Unable to pop route") + }, + ) /** * Pops routes from the navigation stack until the specified [route] is found. @@ -46,7 +60,12 @@ interface AppRouter { * @param route The route to pop to. * @param onComplete The callback to be invoked when the operation is complete. */ - fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + fun popTo( + route: AppRoute, + onComplete: (isSuccess: Boolean) -> Unit = { isSuccess -> + defaultCompletionHandler(isSuccess, errorMessage = "Unable to pop to $route") + }, + ) /** * Pops routes from the navigation stack until the ***first*** specified [routeClass] is found. @@ -54,5 +73,12 @@ interface AppRouter { * @param routeClass The route class to pop to. * @param onComplete The callback to be invoked when the operation is complete. */ - fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit = {}) + fun popTo( + routeClass: KClass, + onComplete: (isSuccess: Boolean) -> Unit = { isSuccess -> + defaultCompletionHandler(isSuccess, errorMessage = "Unable to pop to $routeClass") + }, + ) + + fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt index dc2c197c50..5353f15353 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt @@ -2,16 +2,30 @@ package com.tangem.datasource.crypto import com.tangem.common.extensions.hexToBytes import com.tangem.crypto.CryptoUtils +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.config.ConfigManager -internal class Sha256SignatureVerifier(private val configManager: ConfigManager) : DataSignatureVerifier { +internal class Sha256SignatureVerifier( + private val configManager: ConfigManager, + private val apiConfigsManager: ApiConfigsManager, +) : DataSignatureVerifier { override fun verifySignature(signature: String, data: String): Boolean { - val pubKey = configManager.config.express?.signVerifierPublicKey ?: return false + val pubKey = getPubKey() ?: return false return CryptoUtils.verify( publicKey = pubKey.hexToBytes().takeLast(n = 65).toByteArray(), message = data.toByteArray(), signature = signature.hexToBytes(), ) } + + private fun getPubKey(): String? { + val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express) + return when (expressConfig.environment) { + ApiEnvironment.PROD -> configManager.config.express?.signVerifierPublicKey + else -> configManager.config.devExpress?.signVerifierPublicKey + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt index b5c2f07c49..b4550b25b2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.di +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.crypto.Sha256SignatureVerifier @@ -15,7 +16,10 @@ internal object SecurityModule { @Provides @Singleton - fun provideDataSignatureVerifier(configManager: ConfigManager): DataSignatureVerifier { - return Sha256SignatureVerifier(configManager) + fun provideDataSignatureVerifier( + configManager: ConfigManager, + apiConfigsManager: ApiConfigsManager, + ): DataSignatureVerifier { + return Sha256SignatureVerifier(configManager, apiConfigsManager) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index 38a05e8d79..d3fa302f26 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -1,69 +1,109 @@ package com.tangem.datasource.local.logs -import androidx.datastore.preferences.core.MutablePreferences -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys +import android.content.Context import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormatterBuilder +import timber.log.Timber +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter import javax.inject.Inject +import javax.inject.Singleton /** * Store for saving app logs * - * @property appPreferencesStore app preferences store - * @param dispatchers coroutine dispatcher provider + * @property applicationContext application context + * @param dispatchers coroutine dispatcher provider * [REDACTED_AUTHOR] */ +@Singleton class AppLogsStore @Inject constructor( - private val appPreferencesStore: AppPreferencesStore, + @ApplicationContext private val applicationContext: Context, dispatchers: CoroutineDispatcherProvider, ) { private val scope = CoroutineScope(dispatchers.io) private val mutex = Mutex() + private val file = File(applicationContext.filesDir, LOG_FILE_NAME) + + private val formatter = DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral('.') + .appendMonthOfYear(2) + .appendLiteral(' ') + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2) + .appendLiteral('.') + .appendMillisOfSecond(3) + .toFormatter() + + /** Get log file */ + fun getFile(): File? = if (file.exists()) file else null + /** Save log [message] */ fun saveLogMessage(message: String) { - val newLogs = DateTime.now().millis.toString() to message + launchWithLock { + createFileIfNotExist() - appPreferencesStore.editDataWithLock { preferences -> - val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) + writeMessage(message) + } + } - preferences.setObjectMap(key = PreferencesKeys.APP_LOGS_KEY, value = savedLogs + newLogs) + /** Save log that consists from [messages] */ + fun saveLogMessage(vararg messages: String) { + launchWithLock { + createFileIfNotExist() + + writeMessage(*messages) } } /** Delete deprecated logs if file size exceeds [maxSize] */ fun deleteDeprecatedLogs(maxSize: Int) { - appPreferencesStore.editDataWithLock { preferences -> - val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) - - var sum = 0 - preferences.setObjectMap( - key = PreferencesKeys.APP_LOGS_KEY, - value = savedLogs.entries - .sortedBy(Map.Entry::key) - .takeLastWhile { - sum += it.value.length - sum < maxSize - } - .associate { it.key to it.value }, - ) - } - } - - private fun AppPreferencesStore.editDataWithLock( - transform: suspend AppPreferencesStore.(MutablePreferences) -> Unit, - ) { - scope.launch { - mutex.withLock { - editData(transform) + launchWithLock { + if (file.exists() && file.length() > maxSize) { + file.delete() } } } + + private fun writeMessage(vararg messages: String) { + BufferedWriter(FileWriter(file, true)).use { writer -> + writer.append(formatter.print(DateTime.now())) + writer.append(": ") + messages.forEach(writer::append) + writer.newLine() + } + } + + private fun createFileIfNotExist() { + if (!file.exists()) { + runCatching { file.createNewFile() } + .onFailure(Timber::e) + } + } + + private fun launchWithLock(callback: () -> Unit) { + scope.launch { + mutex.withLock { + callback() + } + } + } + + private companion object { + const val LOG_FILE_NAME = "logs.txt" + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt index ee74b94ab5..b593688bd9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt @@ -54,8 +54,8 @@ internal class NetworkLogsSaveInterceptor( val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" appLogsStore.saveLogMessage( - "--> ${request.method} ${request.url}$connectionProtocol\n" + - createRequestEndMessage(request), + "--> ${request.method} ${request.url}$connectionProtocol\n", + createRequestEndMessage(request), ) } @@ -88,12 +88,6 @@ internal class NetworkLogsSaveInterceptor( } private fun logResponseMessage(response: Response, startNs: Long) { - val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) - - val responseMessage = if (response.message.isEmpty()) "" else ' ' + response.message - val startMessage = "<-- ${response.code}$responseMessage ${response.request.url} " + - "(${tookMs}ms)" - val responseHeaders = response.headers val responseBody = response.body!! val contentLength = responseBody.contentLength() @@ -138,7 +132,17 @@ internal class NetworkLogsSaveInterceptor( } } - appLogsStore.saveLogMessage(startMessage + "\n" + message) + val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) + + val spaceBeforeResponseMessage = if (response.message.isEmpty()) "" else ' ' + response.message + + appLogsStore.saveLogMessage( + "<-- ${response.code}", + spaceBeforeResponseMessage, + response.message, + " ${response.request.url} (${tookMs}ms)\n", + message, + ) } private fun bodyHasUnknownEncoding(headers: Headers): Boolean { diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt index 86c43706a6..2834f7ab66 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt @@ -3,7 +3,6 @@ package com.tangem.core.decompose.navigation import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.popWhile import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.essenty.instancekeeper.InstanceKeeper import kotlin.reflect.KClass @@ -34,16 +33,27 @@ internal class DefaultRouter( } override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - navigation.popWhile( - predicate = { it != route }, + popTo( + predicate = { it == route }, onComplete = onComplete, ) } override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { - navigation.popWhile( - predicate = { it::class != routeClass }, + popTo( + predicate = { routeClass.isInstance(it) }, onComplete = onComplete, ) } + + private fun popTo(predicate: (Route) -> Boolean, onComplete: (isSuccess: Boolean) -> Unit) { + navigation.navigate( + transformer = { stack -> + stack + .dropLastWhile(predicate) + .ifEmpty { stack } + }, + onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) }, + ) + } } \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index f13343fc36..f0c510874b 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -5,7 +5,7 @@ }, { "name": "LOCAL_USER_LOGS_ENABLED", - "version": "undefined" + "version": "5.14.0" }, { "name": "GENERATE_XPUB_ENABLED", diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 1e9ed93914..b1797d3a2a 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -221,7 +221,6 @@ Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an. Anbieter wählen Es ist ein Fehler aufgetreten. Code: %s - Huch! Der Tausch des ausgewählten Paares über den gewählten Anbieter ist vorübergehend nicht möglich. Bitte versuche es später erneut. (Code: %s) Der gewählte Anbieter ist im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s) Swaps sind im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s) Geschätzter Betrag diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index d39ab86d92..581c5d1af7 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -217,7 +217,6 @@ Tangem propose des échanges de jetons via des fournisseurs tiers selon les conditions de chaque fournisseur Choisissez un fournisseur Une erreur s\'est produite. Code : %s - Oups! L\'échange de la paire sélectionnée via le fournisseur choisi est temporairement indisponible. Veuillez réessayer plus tard. (Code : %s) Le fournisseur sélectionné n\'est pas disponible pour le moment. Veuillez réessayer plus tard. (Code : %s) Les échanges ne sont pas disponibles pour le moment. Veuillez réessayer plus tard. (Code : %s) Montant estimé diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e5c43474a1..cbfddbaa7f 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -219,7 +219,6 @@ Tangemは、各プロバイダーの条件に従って、サードパーティプロバイダーを介してトークンスワップを提供します。 プロバイダーを選択 エラーが発生しました。コード: %s - おっと!このプロバイダーで選択したペアを交換することは一時的にできません。後でもう一度お試しください。(Code:%s) 選択したプロバイダーは現在利用できません。しばらくしてからもう一度お試しください。(コード: %s ) 現在、交換はご利用いただけません。しばらくしてからもう一度お試しください。(コード: %s ) 推定金額 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e89bb61d95..ff9fce14ea 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -225,7 +225,6 @@ Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами Выберите провайдера Произошла ошибка. Код: %s - К сожалению, обмен указанной пары через выбранного провайдера на данный момент невозможен. Попробуйте совершить обмен позже. (Код: %s) Выбранный провайдер недоступен для обмена. Попробуйте позже. (Код: %s) В данный момент обмен невозможен. Попробуйте позже. (Код: %s) Курс обмена diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index d352a7be3c..4a4839b4e7 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -225,7 +225,6 @@ Tangem пропонує обмін токенів через сторонніх провайдерів відповідно до умов кожного провайдера Оберіть провайдера Виникла помилка. Код: %s - На жаль, обмін вказаної пари через обраного провайдера тимчасово неможливий. Спробуйте здійснити обмін пізніше. (Код: %s) Наразі обраний провайдер недоступний для обміну. Спробуй пізніше. (Код: %s) Наразі обмін неможливий. Спробуй пізніше. (Код: %s) Курс обміну diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 79135b1d0d..dadb67073c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -222,7 +222,6 @@ Tangem offers token swaps via 3rd-party providers according to each provider\'s terms Choose provider An error occurred. Code: %s - Oops! Swapping the selected pair through the chosen provider is temporarily unavailable. Please try again later. (Code: %s) Selected provider is unavailable at the moment. Please try again later. (Code: %s) Swaps are unavailable at the moment. Please try again later. (Code: %s) Estimated amount 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 26278b5877..4b018346c2 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 @@ -6,48 +6,43 @@ 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 -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectMapSync +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.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.BlockchainInfo +import com.tangem.domain.feedback.models.PhoneInfo +import com.tangem.domain.feedback.models.UserWalletsInfo 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.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update 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 appLogsStore app logs store * @property userWalletsListManager user wallets list manager * @property walletManagersStore wallet managers store * @property context context for getting app version - * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultFeedbackRepository( - private val appPreferencesStore: AppPreferencesStore, + private val appLogsStore: AppLogsStore, private val userWalletsListManager: UserWalletsListManager, private val walletManagersStore: WalletManagersStore, private val context: Context, - private val dispatchers: CoroutineDispatcherProvider, ) : FeedbackRepository { private val blockchainsErrors = MutableStateFlow>(emptyMap()) - override suspend fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse) + override fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse) - override suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo { + override fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo { return UserWalletsInfo( selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated", totalUserWallets = userWalletsListManager.walletsCount, @@ -92,38 +87,13 @@ internal class DefaultFeedbackRepository( } } - override suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? { + override fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? { return blockchainsErrors.value[userWalletId].also { if (it == null) Timber.e("Blockchain error info is null for $userWalletId") } } - override suspend fun getAppLogs(): List { - return appPreferencesStore.getObjectMapSync(key = PreferencesKeys.APP_LOGS_KEY) - .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } - .sortedBy(AppLogModel::timestamp) - } - - override suspend fun createLogFile(logs: String): File? { - return runCatching(dispatchers.io) { - val file = File(context.filesDir, LOGS_FILE) - file.delete() - file.createNewFile() - - val stringWriter = StringWriter() - - stringWriter.append(logs) - - val fileWriter = FileWriter(file) - fileWriter.write(stringWriter.toString()) - fileWriter.close() - - file - }.getOrElse { - Timber.e(it, "Logs file isn't created") - null - } - } + override fun getLogFile(): File? = appLogsStore.getFile() private fun getAppVersion(): String { return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) } @@ -135,8 +105,4 @@ internal class DefaultFeedbackRepository( }, ) } - - private companion object { - const val LOGS_FILE = "logs.txt" - } } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt index 393a30cf25..2b377e8455 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt @@ -2,11 +2,10 @@ 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.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.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,18 +20,16 @@ internal object FeedbackRepositoryModule { @Provides @Singleton fun provideFeedbackRepository( - appPreferencesStore: AppPreferencesStore, + appLogsStore: AppLogsStore, userWalletsListManager: UserWalletsListManager, walletManagersStore: WalletManagersStore, @ApplicationContext context: Context, - dispatchers: CoroutineDispatcherProvider, ): FeedbackRepository { return DefaultFeedbackRepository( - appPreferencesStore = appPreferencesStore, + appLogsStore = appLogsStore, userWalletsListManager = userWalletsListManager, walletManagersStore = walletManagersStore, context = context, - dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 6ff9910d3e..0fd87454fa 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -37,10 +37,6 @@ internal class DefaultSettingsRepository( ) } - override fun saveLogMessage(message: String) { - appLogsStore.saveLogMessage(message) - } - override fun deleteDeprecatedLogs(maxSize: Int) { appLogsStore.deleteDeprecatedLogs(maxSize) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt index acdcc492b7..ddf8cfb767 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt @@ -25,13 +25,11 @@ class GetFeedbackEmailUseCase( private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository) suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail { - val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs()) - return FeedbackEmail( address = getAddress(type.cardInfo), subject = emailSubjectResolver.resolve(type), message = createMessage(type), - file = feedbackRepository.createLogFile(logs = formattedLogs), + file = feedbackRepository.getLogFile(), ) } 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 1b5de9c70c..25ebed4d9d 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 @@ -7,9 +7,9 @@ import java.io.File interface FeedbackRepository { - suspend fun getCardInfo(scanResponse: ScanResponse): CardInfo + fun getCardInfo(scanResponse: ScanResponse): CardInfo - suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo + fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List @@ -23,9 +23,7 @@ interface FeedbackRepository { fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) - suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? + fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? - suspend fun getAppLogs(): List - - suspend fun createLogFile(logs: String): File? + fun getLogFile(): File? } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 06fd81fde6..50b3e3624d 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -10,8 +10,6 @@ interface SettingsRepository { suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) - fun saveLogMessage(message: String) - fun deleteDeprecatedLogs(maxSize: Int) suspend fun isSendTapHelpPreviewEnabled(): Boolean diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 55450cbdec..68d9795026 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -104,15 +104,17 @@ sealed class SwapEvents( ) data class NoticeProviderError( - val token: String, + val sendToken: String, + val receiveToken: String, val provider: SwapProvider, val errorCode: Int, ) : SwapEvents( event = "Notice - Express Error", params = mapOf( - "Token" to token, + "Send Token" to sendToken, + "Receive Token" to receiveToken, "Provider" to provider.name, - "Error code" to errorCode.toString(), + "Error Code" to errorCode.toString(), ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 517c795db6..c013b188e0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -736,6 +736,7 @@ internal class StateBuilder( private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning { val providerErrorMessage = getProviderErrorMessage(dataError) + val providerErrorTitle = getProviderErrorTitle(dataError) return when (dataError) { is DataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError( notificationConfig = NotificationConfig( @@ -759,22 +760,8 @@ internal class StateBuilder( ) else -> SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( - title = if (dataError is DataError.UnknownError) { - resourceReference(R.string.common_error) - } else { - resourceReference(R.string.warning_express_refresh_required_title) - }, - subtitle = when { - dataError is DataError.UnknownError -> { - resourceReference(R.string.common_unknown_error) - } - providerErrorMessage != null -> { - providerErrorMessage - } - else -> { - resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString())) - } - }, + title = providerErrorTitle, + subtitle = providerErrorMessage, iconResId = R.drawable.img_attention_20, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(R.string.warning_button_refresh), @@ -1096,16 +1083,17 @@ internal class StateBuilder( ) } - private fun getProviderErrorMessage(dataError: DataError): TextReference? { + private fun getProviderErrorMessage(dataError: DataError): TextReference { return when (dataError) { is DataError.SwapsAreUnavailableNowError -> resourceReference( id = R.string.express_error_swap_unavailable, formatArgs = wrappedList(dataError.code), ) is DataError.ExchangeNotPossibleError -> resourceReference( - id = R.string.express_error_provider_unavailable, + id = R.string.warning_express_pair_unavailable_message, formatArgs = wrappedList(dataError.code), ) + is DataError.UnknownError -> resourceReference(R.string.common_unknown_error) is DataError.ExchangeProviderNotActiveError, is DataError.ExchangeProviderNotFoundError, is DataError.ExchangeProviderNotAvailableError, @@ -1114,7 +1102,18 @@ internal class StateBuilder( id = R.string.express_error_swap_pair_unavailable, formatArgs = wrappedList(dataError.code), ) - else -> null + else -> resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString())) + } + } + + private fun getProviderErrorTitle(dataError: DataError): TextReference { + return when (dataError) { + is DataError.ExchangeNotPossibleError -> resourceReference( + id = R.string.warning_express_pair_unavailable_title, + formatArgs = wrappedList(dataError.code), + ) + is DataError.UnknownError -> resourceReference(R.string.common_error) + else -> resourceReference(R.string.warning_express_refresh_required_title) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index eb128c84db..e0061c0c1b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -405,9 +405,13 @@ internal class SwapViewModel @Inject constructor( } private fun sendErrorAnalyticsEvent(error: DataError, provider: SwapProvider) { + val receiveToken = dataState.toCryptoCurrency?.currency?.let { + "${it.network.backendId}:${it.symbol}" + } analyticsEventHandler.send( SwapEvents.NoticeProviderError( - token = initialCryptoCurrency.symbol, + sendToken = "${initialCryptoCurrency.network.backendId}:${initialCryptoCurrency.symbol}", + receiveToken = receiveToken ?: "", provider = provider, errorCode = error.code, ), diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6a52565547..b259900751 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.14-742" +tangemBlockchainSdk = "release-app_5.14-747" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.14-379" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^