Updated on 2026-08-14
This commit is contained in:
commit
ee5a24f333
34 changed files with 308 additions and 207 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Log.Level>,
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import timber.log.Timber
|
|||
val logMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
Timber.i("Dispatch action: $action")
|
||||
Timber.i("Dispatch action: ${action::class.java.simpleName}")
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<out AppRoute>, 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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppRoute>?
|
||||
|
||||
// TODO: Replace with UI message handler: [REDACTED_JIRA]
|
||||
var snackbarHandler: SnackbarHandler?
|
||||
}
|
||||
|
|
@ -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<AppRoute>? = null
|
||||
override var snackbarHandler: SnackbarHandler? = null
|
||||
}
|
||||
|
|
@ -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<out AppRoute>, onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
fun popTo(
|
||||
routeClass: KClass<out AppRoute>,
|
||||
onComplete: (isSuccess: Boolean) -> Unit = { isSuccess ->
|
||||
defaultCompletionHandler(isSuccess, errorMessage = "Unable to pop to $routeClass")
|
||||
},
|
||||
)
|
||||
|
||||
fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>(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<String>(PreferencesKeys.APP_LOGS_KEY)
|
||||
|
||||
var sum = 0
|
||||
preferences.setObjectMap(
|
||||
key = PreferencesKeys.APP_LOGS_KEY,
|
||||
value = savedLogs.entries
|
||||
.sortedBy(Map.Entry<String, String>::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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<out Route>, 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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
},
|
||||
{
|
||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.14.0"
|
||||
},
|
||||
{
|
||||
"name": "GENERATE_XPUB_ENABLED",
|
||||
|
|
|
|||
|
|
@ -221,7 +221,6 @@
|
|||
<string name="express_choose_providers_subtitle">Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an.</string>
|
||||
<string name="express_choose_providers_title">Anbieter wählen</string>
|
||||
<string name="express_error_code">Es ist ein Fehler aufgetreten. Code: %s</string>
|
||||
<string name="express_error_provider_unavailable">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)</string>
|
||||
<string name="express_error_swap_pair_unavailable">Der gewählte Anbieter ist im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s)</string>
|
||||
<string name="express_error_swap_unavailable">Swaps sind im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s)</string>
|
||||
<string name="express_estimated_amount">Geschätzter Betrag</string>
|
||||
|
|
|
|||
|
|
@ -217,7 +217,6 @@
|
|||
<string name="express_choose_providers_subtitle">Tangem propose des échanges de jetons via des fournisseurs tiers selon les conditions de chaque fournisseur</string>
|
||||
<string name="express_choose_providers_title">Choisissez un fournisseur</string>
|
||||
<string name="express_error_code">Une erreur s\'est produite. Code : %s</string>
|
||||
<string name="express_error_provider_unavailable">Oups! L\'échange de la paire sélectionnée via le fournisseur choisi est temporairement indisponible. Veuillez réessayer plus tard. (Code : %s)</string>
|
||||
<string name="express_error_swap_pair_unavailable">Le fournisseur sélectionné n\'est pas disponible pour le moment. Veuillez réessayer plus tard. (Code : %s)</string>
|
||||
<string name="express_error_swap_unavailable">Les échanges ne sont pas disponibles pour le moment. Veuillez réessayer plus tard. (Code : %s)</string>
|
||||
<string name="express_estimated_amount">Montant estimé</string>
|
||||
|
|
|
|||
|
|
@ -219,7 +219,6 @@
|
|||
<string name="express_choose_providers_subtitle">Tangemは、各プロバイダーの条件に従って、サードパーティプロバイダーを介してトークンスワップを提供します。</string>
|
||||
<string name="express_choose_providers_title">プロバイダーを選択</string>
|
||||
<string name="express_error_code">エラーが発生しました。コード: %s</string>
|
||||
<string name="express_error_provider_unavailable">おっと!このプロバイダーで選択したペアを交換することは一時的にできません。後でもう一度お試しください。(Code:%s)</string>
|
||||
<string name="express_error_swap_pair_unavailable">選択したプロバイダーは現在利用できません。しばらくしてからもう一度お試しください。(コード: %s )</string>
|
||||
<string name="express_error_swap_unavailable">現在、交換はご利用いただけません。しばらくしてからもう一度お試しください。(コード: %s )</string>
|
||||
<string name="express_estimated_amount">推定金額</string>
|
||||
|
|
|
|||
|
|
@ -225,7 +225,6 @@
|
|||
<string name="express_choose_providers_subtitle">Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами</string>
|
||||
<string name="express_choose_providers_title">Выберите провайдера</string>
|
||||
<string name="express_error_code">Произошла ошибка. Код: %s</string>
|
||||
<string name="express_error_provider_unavailable">К сожалению, обмен указанной пары через выбранного провайдера на данный момент невозможен. Попробуйте совершить обмен позже. (Код: %s)</string>
|
||||
<string name="express_error_swap_pair_unavailable">Выбранный провайдер недоступен для обмена. Попробуйте позже. (Код: %s)</string>
|
||||
<string name="express_error_swap_unavailable">В данный момент обмен невозможен. Попробуйте позже. (Код: %s)</string>
|
||||
<string name="express_estimated_amount">Курс обмена</string>
|
||||
|
|
|
|||
|
|
@ -225,7 +225,6 @@
|
|||
<string name="express_choose_providers_subtitle">Tangem пропонує обмін токенів через сторонніх провайдерів відповідно до умов кожного провайдера</string>
|
||||
<string name="express_choose_providers_title">Оберіть провайдера</string>
|
||||
<string name="express_error_code">Виникла помилка. Код: %s</string>
|
||||
<string name="express_error_provider_unavailable">На жаль, обмін вказаної пари через обраного провайдера тимчасово неможливий. Спробуйте здійснити обмін пізніше. (Код: %s)</string>
|
||||
<string name="express_error_swap_pair_unavailable">Наразі обраний провайдер недоступний для обміну. Спробуй пізніше. (Код: %s)</string>
|
||||
<string name="express_error_swap_unavailable">Наразі обмін неможливий. Спробуй пізніше. (Код: %s)</string>
|
||||
<string name="express_estimated_amount">Курс обміну</string>
|
||||
|
|
|
|||
|
|
@ -222,7 +222,6 @@
|
|||
<string name="express_choose_providers_subtitle">Tangem offers token swaps via 3rd-party providers according to each provider\'s terms</string>
|
||||
<string name="express_choose_providers_title">Choose provider</string>
|
||||
<string name="express_error_code">An error occurred. Code: %s</string>
|
||||
<string name="express_error_provider_unavailable">Oops! Swapping the selected pair through the chosen provider is temporarily unavailable. Please try again later. (Code: %s)</string>
|
||||
<string name="express_error_swap_pair_unavailable">Selected provider is unavailable at the moment. Please try again later. (Code: %s)</string>
|
||||
<string name="express_error_swap_unavailable">Swaps are unavailable at the moment. Please try again later. (Code: %s)</string>
|
||||
<string name="express_estimated_amount">Estimated amount</string>
|
||||
|
|
|
|||
|
|
@ -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<Map<UserWalletId, BlockchainErrorInfo>>(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<AppLogModel> {
|
||||
return appPreferencesStore.getObjectMapSync<String>(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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,10 +37,6 @@ internal class DefaultSettingsRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override fun saveLogMessage(message: String) {
|
||||
appLogsStore.saveLogMessage(message)
|
||||
}
|
||||
|
||||
override fun deleteDeprecatedLogs(maxSize: Int) {
|
||||
appLogsStore.deleteDeprecatedLogs(maxSize)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BlockchainInfo>
|
||||
|
||||
|
|
@ -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<AppLogModel>
|
||||
|
||||
suspend fun createLogFile(logs: String): File?
|
||||
fun getLogFile(): File?
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@ interface SettingsRepository {
|
|||
|
||||
suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean)
|
||||
|
||||
fun saveLogMessage(message: String)
|
||||
|
||||
fun deleteDeprecatedLogs(maxSize: Int)
|
||||
|
||||
suspend fun isSendTapHelpPreviewEnabled(): Boolean
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue