Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-26 16:34:17 +03:00
commit 6920ad3b6d
356 changed files with 7941 additions and 1194 deletions

View file

@ -100,6 +100,7 @@ dependencies {
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.staking)
implementation(projects.domain.walletConnect)
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.markets)
implementation(projects.domain.manageTokens)
implementation(projects.domain.nft)
@ -169,6 +170,8 @@ dependencies {
implementation(projects.features.manageTokens.impl)
implementation(projects.features.send.api)
implementation(projects.features.send.impl)
implementation(projects.features.sendV2.api)
implementation(projects.features.sendV2.impl)
implementation(projects.features.qrScanning.api)
implementation(projects.features.qrScanning.impl)
implementation(projects.features.staking.api)
@ -195,6 +198,8 @@ dependencies {
implementation(projects.features.biometry.impl)
implementation(projects.features.nft.api)
implementation(projects.features.nft.impl)
implementation(projects.features.walletconnect.api)
implementation(projects.features.walletconnect.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

View file

@ -3,7 +3,6 @@ package com.tangem.tap
import androidx.hilt.work.HiltWorkerFactory
import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -15,6 +14,7 @@ import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
@ -32,13 +32,13 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
@ -94,8 +94,6 @@ interface ApplicationEntryPoint {
fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase
fun getWalletNameGenerateUseCase(): GenerateWalletNameUseCase
fun getCardRepository(): CardRepository
fun getTangemSdkLogger(): TangemSdkLogger
@ -118,8 +116,6 @@ interface ApplicationEntryPoint {
fun getTransactionSignerFactory(): TransactionSignerFactory
fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase
fun getOnrampFeatureToggles(): OnrampFeatureToggles
fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles
@ -142,4 +138,8 @@ interface ApplicationEntryPoint {
fun getUiMessageSender(): UiMessageSender
fun getWorkerFactory(): HiltWorkerFactory
fun getOnlineCardVerifier(): OnlineCardVerifier
fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory
}

View file

@ -8,6 +8,7 @@ import coil.ImageLoaderFactory
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.ExceptionHandler
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
@ -46,15 +47,16 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.images.createCoilImageLoader
@ -146,9 +148,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase
get() = entryPoint.getSaveTwinsOnboardingShownUseCase()
private val generateWalletNameUseCase: GenerateWalletNameUseCase
get() = entryPoint.getWalletNameGenerateUseCase()
private val cardRepository: CardRepository
get() = entryPoint.getCardRepository()
@ -182,9 +181,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val transactionSignerFactory: TransactionSignerFactory
get() = entryPoint.getTransactionSignerFactory()
private val getUserCountryUseCase: GetUserCountryUseCase
get() = entryPoint.getGetUserCountryCodeUseCase()
private val onrampFeatureToggles: OnrampFeatureToggles
get() = entryPoint.getOnrampFeatureToggles()
@ -212,6 +208,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val uiMessageSender: UiMessageSender
get() = entryPoint.getUiMessageSender()
private val blockchainExceptionHandler: BlockchainExceptionHandler
get() = entryPoint.getBlockchainExceptionHandler()
private val workerFactory: HiltWorkerFactory
get() = entryPoint.getWorkerFactory()
@ -220,6 +219,12 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
.setWorkerFactory(workerFactory)
.build()
private val onlineCardVerifier: OnlineCardVerifier
get() = entryPoint.getOnlineCardVerifier()
private val userWalletBuilderFactory: UserWalletBuilder.Factory
get() = entryPoint.getUserWalletBuilderFactory()
// endregion
override fun onCreate() {
@ -265,7 +270,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
}
loadNativeLibraries()
// ExceptionHandler.append(blockchainExceptionHandler) // TODO [REDACTED_TASK_KEY] Send only to Firebase
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(),
@ -301,7 +306,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
generalUserWalletsListManager = generalUserWalletsListManager,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
generateWalletNameUseCase = generateWalletNameUseCase,
cardRepository = cardRepository,
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
@ -312,7 +316,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
shareManager = shareManager,
appRouter = appRouter,
transactionSignerFactory = transactionSignerFactory,
getUserCountryUseCase = getUserCountryUseCase,
onrampFeatureToggles = onrampFeatureToggles,
environmentConfigStorage = environmentConfigStorage,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
@ -322,6 +325,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
clipboardManager = clipboardManager,
settingsManager = settingsManager,
uiMessageSender = uiMessageSender,
onlineCardVerifier = onlineCardVerifier,
userWalletBuilderFactory = userWalletBuilderFactory,
),
),
)

View file

@ -1,20 +1,20 @@
package com.tangem.tap.common.analytics
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.core.analytics.api.ErrorEventLogger
import com.tangem.core.analytics.api.ExceptionLogger
import com.tangem.core.analytics.api.EventLogger
import timber.log.Timber
class AnalyticsEventsLogger(
private val name: String,
private val jsonConverter: MoshiJsonConverter,
) : EventLogger, ErrorEventLogger {
) : EventLogger, ExceptionLogger {
override fun logEvent(event: String, params: Map<String, String>) {
Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
}
override fun logErrorEvent(error: Throwable, params: Map<String, String>) {
override fun logException(error: Throwable, params: Map<String, String>) {
Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params)))
}
}

View file

@ -6,7 +6,6 @@ internal sealed class Push(event: String) : AnalyticsEvent(
category = "Push",
event = event,
params = emptyMap(),
error = null,
) {
data object PushNotificationOpened : Push(event = "Push Notification Opened")

View file

@ -9,8 +9,7 @@ sealed class Settings(
category: String = "Settings",
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : AnalyticsEvent(category, event, params, error) {
) : AnalyticsEvent(category, event, params) {
class ScreenOpened : Settings(event = "Settings Screen Opened")
class ButtonStartWalletConnectSession : Settings(event = "Button - Start Wallet Connect Session")
@ -19,16 +18,14 @@ sealed class Settings(
sealed class CardSettings(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : Settings("Settings / Card Settings", event, params, error) {
) : Settings("Settings / Card Settings", event, params) {
class ButtonFactoryReset : CardSettings("Button - Factory Reset")
class FactoryResetFinished(cardsCount: Int? = null, error: Throwable? = null) : CardSettings(
class FactoryResetFinished(cardsCount: Int? = null) : CardSettings(
event = "Factory Reset Finished",
params = buildMap {
cardsCount?.let { put("Cards Count", "$it") }
},
error = error,
)
class FactoryResetCanceled(cardsCount: Int) : CardSettings(
@ -44,10 +41,9 @@ sealed class Settings(
params = mapOf("Type" to type.value),
)
class SecurityModeChanged(mode: AnalyticsParam.SecurityMode, error: Throwable? = null) : CardSettings(
class SecurityModeChanged(mode: AnalyticsParam.SecurityMode) : CardSettings(
event = "Security Mode Changed",
params = mapOf("Mode" to mode.value),
error = error,
)
class AccessCodeRecoveryChanged(status: AnalyticsParam.AccessCodeRecoveryStatus) : CardSettings(

View file

@ -8,8 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
sealed class SignIn(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : AnalyticsEvent("Sign In", event, params, error) {
) : AnalyticsEvent("Sign In", event, params) {
class ScreenOpened : SignIn(event = "Sign In Screen Opened")

View file

@ -10,8 +10,7 @@ sealed class Token(
category: String,
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : AnalyticsEvent(category, event, params, error) {
) : AnalyticsEvent(category, event, params) {
sealed class Receive(
event: String,

View file

@ -9,8 +9,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.O
internal sealed class WalletConnect(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : AnalyticsEvent("Wallet Connect", event, params, error) {
) : AnalyticsEvent("Wallet Connect", event, params) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionInitiated(source: SourceType) : WalletConnect(

View file

@ -1,15 +1,15 @@
package com.tangem.tap.common.analytics.handlers
import com.tangem.blockchain.common.ExceptionHandlerOutput
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.tap.common.analytics.events.BlockchainExceptionEvent
import javax.inject.Inject
class BlockchainExceptionHandler @Inject constructor(
private val analyticsHandler: AnalyticsEventHandler,
private val analyticsErrorHandler: AnalyticsErrorHandler,
) : ExceptionHandlerOutput {
override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) {
analyticsHandler.send(
analyticsErrorHandler.sendErrorEvent(
BlockchainExceptionEvent(
selectedHost = nextHost,
exceptionHost = currentHost,

View file

@ -1,11 +1,13 @@
package com.tangem.tap.common.analytics.handlers.amplitude
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
class AmplitudeAnalyticsHandler(
private val client: AmplitudeAnalyticsClient,
) : AnalyticsHandler {
) : AnalyticsHandler, AnalyticsErrorHandler {
override fun id(): String = ID
@ -13,6 +15,10 @@ class AmplitudeAnalyticsHandler(
client.logEvent(eventId, params)
}
override fun sendErrorEvent(event: AnalyticsEvent) {
send(event.id, event.params)
}
companion object {
const val ID = "Amplitude"
}

View file

@ -1,16 +1,18 @@
package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.ErrorEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
import com.tangem.tap.common.analytics.events.Shop
class FirebaseAnalyticsHandler(
private val client: FirebaseAnalyticsClient,
) : AnalyticsHandler, ErrorEventHandler {
) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler {
private val errorConverter = AnalyticsErrorConverter()
override fun id(): String = ID
@ -18,30 +20,17 @@ class FirebaseAnalyticsHandler(
client.logEvent(eventId, params)
}
override fun send(event: AnalyticsEvent) {
val error = event.error
when {
error != null -> {
val errorConverter = AnalyticsErrorConverter()
if (!errorConverter.canBeHandled(error)) return
override fun sendException(event: ExceptionAnalyticsEvent) {
if (!errorConverter.canBeHandled(event.exception)) return
val errorParams = errorConverter.convert(error).toMutableMap()
errorParams["Category"] = event.category
errorParams["Event"] = event.event
errorParams.putAll(event.params)
send(error, errorParams)
}
event is Shop.Purchased -> {
send(FirebaseAnalytics.Event.PURCHASE, event.params)
}
else -> {
super.send(event)
}
}
val errorParams = errorConverter.convert(event.exception).toMutableMap()
errorParams.putAll(event.params)
client.logException(event.exception, event.params)
}
override fun send(error: Throwable, params: Map<String, String>) {
client.logErrorEvent(error, params)
override fun sendErrorEvent(event: AnalyticsEvent) {
send(event)
}
companion object {

View file

@ -4,14 +4,15 @@ import android.os.Bundle
import androidx.core.os.bundleOf
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.crashlytics.recordException
import com.google.firebase.ktx.Firebase
import com.tangem.core.analytics.api.ErrorEventLogger
import com.tangem.core.analytics.api.ExceptionLogger
import com.tangem.core.analytics.api.EventLogger
/**
[REDACTED_AUTHOR]
*/
interface FirebaseAnalyticsClient : EventLogger, ErrorEventLogger
interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger
internal class FirebaseClient : FirebaseAnalyticsClient {
@ -27,11 +28,12 @@ internal class FirebaseClient : FirebaseAnalyticsClient {
)
}
override fun logErrorEvent(error: Throwable, params: Map<String, String>) {
eventConverter.convertEventParams(params)
.forEach { fbCrashlytics.setCustomKey(it.key, it.value) }
fbCrashlytics.recordException(error)
// TODO [REDACTED_TASK_KEY] fix recordException usages in the app
override fun logException(error: Throwable, params: Map<String, String>) {
fbCrashlytics.recordException(error) {
eventConverter.convertEventParams(params)
.forEach { key(it.key, it.value) }
}
}
private fun Map<String, String>.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray())

View file

@ -16,7 +16,7 @@ internal class FirebaseLogClient(
logger.logEvent(event, params)
}
override fun logErrorEvent(error: Throwable, params: Map<String, String>) {
logger.logErrorEvent(error, params)
override fun logException(error: Throwable, params: Map<String, String>) {
logger.logException(error, params)
}
}

View file

@ -10,11 +10,13 @@ import com.tangem.common.authentication.AuthenticationManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.Config
import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.crypto.bip39.Wordlist
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.sdk.DefaultSessionViewDelegate
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
import com.tangem.sdk.extensions.*
import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider
import com.tangem.sdk.nfc.NfcManager
@ -32,8 +34,9 @@ import javax.inject.Singleton
*/
@Singleton
internal class DefaultCardSdkProvider @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val dispatchers: CoroutineDispatcherProvider,
private val cardSdkFeatureToggles: CardSdkFeatureToggles,
) : CardSdkProvider, CardSdkOwner {
private val observer = Observer()
@ -46,7 +49,12 @@ internal class DefaultCardSdkProvider @Inject constructor(
override fun register(activity: FragmentActivity) = runBlocking(dispatchers.mainImmediate) {
if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) {
val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing"
analyticsEventHandler.send(TangemSdkWarningEvent(message))
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(
exception = IllegalStateException(message),
params = errorParams,
),
)
Log.info { message }
return@runBlocking
}
@ -64,14 +72,24 @@ internal class DefaultCardSdkProvider @Inject constructor(
private fun tryToRegisterWithForegroundActivity(): TangemSdk = runBlocking(dispatchers.mainImmediate) {
val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity"
analyticsEventHandler.send(TangemSdkWarningEvent(warning))
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(
exception = IllegalStateException(warning),
params = errorParams,
),
)
Log.warning { warning }
val activity = foregroundActivityObserver.foregroundActivity
if (activity == null) {
val error = "Tangem SDK holder is null and foreground activity is null"
analyticsEventHandler.send(TangemSdkWarningEvent(error))
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(
exception = IllegalStateException(error),
params = errorParams,
),
)
Log.error { error }
error(error)
}
@ -82,7 +100,12 @@ internal class DefaultCardSdkProvider @Inject constructor(
if (sdk == null) {
val error = "Tangem SDK is null after re-registering with foreground activity"
analyticsEventHandler.send(TangemSdkWarningEvent(error))
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(
exception = IllegalStateException(error),
params = errorParams,
),
)
Log.error { error }
error(error)
}
@ -108,7 +131,9 @@ internal class DefaultCardSdkProvider @Inject constructor(
authenticationManager = authenticationManager,
keystoreManager = keystoreManager,
wordlist = Wordlist.getWordlist(activity),
config = config,
config = config.apply {
isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled
},
)
holder = Holder(
@ -170,5 +195,10 @@ internal class DefaultCardSdkProvider @Inject constructor(
),
),
)
val errorParams = mapOf(
"Category" to "Tangem SDK",
"Event" to "Warning",
)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap.data
import com.tangem.core.analytics.models.AnalyticsEvent
internal class TangemSdkWarningEvent(message: String) : AnalyticsEvent(
category = "Tangem SDK",
event = "Warning",
error = IllegalStateException(message),
)

View file

@ -0,0 +1,40 @@
package com.tangem.tap.di.domain
import com.tangem.domain.nft.FetchNFTCollectionsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NFTDomainModule {
@Provides
@Singleton
fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
): GetNFTCollectionsUseCase {
return GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
)
}
@Provides
@Singleton
fun providesFetchNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
): FetchNFTCollectionsUseCase {
return FetchNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
@ -242,4 +243,10 @@ internal object OnrampDomainModule {
fun provideFetchHotCryptoUseCase(hotCryptoRepository: HotCryptoRepository): FetchHotCryptoUseCase {
return FetchHotCryptoUseCase(hotCryptoRepository)
}
@Provides
@Singleton
fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase {
return GetLegacyTopUpUrlUseCase(legacyTopUpRepository)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -209,4 +210,12 @@ internal object StakingDomainModule {
fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase {
return GetStakingIntegrationIdUseCase(stakingRepository)
}
@Provides
@Singleton
fun provideCheckAccountInitializedUseCase(
walletManagersFacade: WalletManagersFacade,
): CheckAccountInitializedUseCase {
return CheckAccountInitializedUseCase(walletManagersFacade)
}
}

View file

@ -10,7 +10,10 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -18,6 +21,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@ -165,4 +169,22 @@ internal object WalletsDomainModule {
fun providesSeedPhraseNotificationUseCase(walletsRepository: WalletsRepository): SeedPhraseNotificationUseCase {
return SeedPhraseNotificationUseCase(walletsRepository = walletsRepository)
}
@Provides
@Singleton
fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase {
return GetCardImageUseCase(verifier = onlineCardVerifier)
}
@Provides
@Singleton
fun providesIsWalletNFTEnabledSyncUseCase(
walletsRepository: WalletsRepository,
nftFeatureToggles: NFTFeatureToggles,
): IsWalletNFTEnabledSyncUseCase {
return IsWalletNFTEnabledSyncUseCase(
walletsRepository = walletsRepository,
nftFeatureToggles = nftFeatureToggles,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.routing
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.tap.routing.ProxyAppRouter
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.configurator.MutableAppRouterConfig
@ -17,8 +18,15 @@ internal object AppRouterModule {
@Provides
@Singleton
fun provideAppRouter(config: AppRouterConfig, dispatchers: CoroutineDispatcherProvider): AppRouter =
ProxyAppRouter(config, dispatchers)
fun provideAppRouter(
config: AppRouterConfig,
dispatchers: CoroutineDispatcherProvider,
analyticsExceptionHandler: AnalyticsExceptionHandler,
): AppRouter = ProxyAppRouter(
config = config,
dispatchers = dispatchers,
analyticsExceptionHandler = analyticsExceptionHandler,
)
@Provides
@Singleton

View file

@ -6,14 +6,17 @@ import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
fun CardDTO.signedHashesCount(): Int {
return wallets.sumOf { it.totalSignedHashes ?: 0 }
}
suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null): String {
suspend fun CardDTO.getOrLoadCardArtworkUrl(
cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null,
onlineCardVerifier: OnlineCardVerifier,
): String {
fun ifAnyError(): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
@ -28,7 +31,7 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInf
}
}
return when (val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
return when (val cardInfoResult = cardInfo ?: onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = cardInfoResult.data.artwork?.id
if (artworkId.isNullOrEmpty()) {

View file

@ -7,6 +7,7 @@ import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
@ -34,7 +35,12 @@ internal object UseCaseScanProcessor {
ifLeft = {
val error = scanCardExceptionConverter.convertBack(it)
Analytics.send(Basic.ScanError(error))
Analytics.sendException(
ExceptionAnalyticsEvent(
exception = error,
params = mapOf("Event" to "Scan"),
),
)
CompletionResult.Failure(error)
},
ifRight = { CompletionResult.Success(it) },
@ -97,7 +103,12 @@ internal object UseCaseScanProcessor {
-> {
val error = scanCardExceptionConverter.convertBack(exception)
Analytics.send(Basic.ScanError(error))
Analytics.sendException(
ExceptionAnalyticsEvent(
exception = error,
params = mapOf("Event" to "Scan"),
),
)
onFailure(error)
}
}

View file

@ -14,7 +14,7 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.services.secure.SecureStorage
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.res.getStringSafe
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath
@ -224,7 +224,12 @@ internal class DefaultTangemSdkManager(
private fun sendScanResultsToAnalytics(result: CompletionResult<ScanResponse>) {
if (result is CompletionResult.Failure) {
(result.error as? TangemSdkError)?.let { error ->
Analytics.send(Basic.ScanError(error))
Analytics.sendException(
ExceptionAnalyticsEvent(
exception = error,
params = mapOf("Event" to "Scan"),
),
)
}
}
}

View file

@ -8,9 +8,13 @@ import com.reown.android.relay.ConnectionType
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.unsupportedDApps
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
@ -555,9 +559,4 @@ internal class DefaultLegacyWalletConnectRepository(
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
return wcProvidedChains.intersect(userChains.toSet())
}
private companion object {
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
}
}

View file

@ -8,4 +8,7 @@ internal class WalletConnectFeatureToggles(
val isSolanaTxSignEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED")
val isRedesignedWalletConnectEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED")
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.routing.AppRoute
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
@ -61,7 +60,11 @@ class WalletConnectMiddleware {
if (uri != null && isWalletConnectUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) }
store.dispatchNavigationAction {
push(
AppRoute.QrScanning(AppRoute.QrScanning.Source.WALLET_CONNECT),
)
}
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {

View file

@ -4,7 +4,9 @@ import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.common.util.cardTypesResolver
@ -30,6 +32,8 @@ internal class SecurityModeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val cardSettingsInteractor: CardSettingsInteractor,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : Model() {
private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value
@ -85,14 +89,19 @@ internal class SecurityModeModel @Inject constructor(
val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
when (result) {
is CompletionResult.Success -> {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
analyticsEventHandler.send(Settings.CardSettings.SecurityModeChanged(paramValue))
store.dispatchNavigationAction(AppRouter::pop)
}
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(
exception = error,
params = mapOf("Event" to "Security Mode Changed"),
),
)
}
}
else -> Unit

View file

@ -21,7 +21,7 @@ object ClipboardOrScanQrDialog {
}
setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ ->
store.dispatchNavigationAction {
push(AppRoute.QrScanning(source = SourceType.WALLET_CONNECT))
push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.WALLET_CONNECT))
}
}
setOnDismissListener {

View file

@ -19,7 +19,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
@ -44,12 +43,12 @@ import javax.inject.Inject
internal class HomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val scanCardProcessor: ScanCardProcessor,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val saveWalletUseCase: SaveWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
) : Model() {
private val tangemErrorHandler = TangemTangemErrorsHandler(store)
@ -100,10 +99,7 @@ internal class HomeModel @Inject constructor(
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = UserWalletBuilder(
scanResponse = scanResponse,
generateWalletNameUseCase = generateWalletNameUseCase,
).build()
val userWallet = userWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")

View file

@ -10,7 +10,6 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -91,8 +90,9 @@ private suspend fun readCard() {
}
private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build().guard {
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse)
val userWallet = userWalletBuilder.build().guard {
Timber.e("User wallet not created")
return@launch
}

View file

@ -14,7 +14,6 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
@ -70,6 +69,7 @@ object OnboardingHelper {
}
}
@Suppress("CyclomaticComplexMethod")
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
scanResponse.productType == ProductType.Ring ||
@ -104,7 +104,11 @@ object OnboardingHelper {
} else {
AppRoute.OnboardingOther
}
ProductType.Twins -> AppRoute.OnboardingTwins
ProductType.Twins -> if (featureToggles.isTwinRefactoringEnabled) {
AppRoute.Onboarding(scanResponse = scanResponse)
} else {
AppRoute.OnboardingTwins
}
ProductType.Start2Coin,
ProductType.Visa,
-> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported")
@ -250,8 +254,9 @@ object OnboardingHelper {
hasBackupError: Boolean,
alreadyCreatedWallet: UserWallet? = null,
) {
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWallet = alreadyCreatedWallet ?: UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
val userWalletBuilderFactory = store.inject(DaggerGraphState::userWalletBuilderFactory)
val userWallet = alreadyCreatedWallet ?: userWalletBuilderFactory.create(scanResponse)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()

View file

@ -7,8 +7,8 @@ import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
@ -26,16 +26,22 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Remove when navigation refactoring will be implemented")
class OnboardingManager(var scanResponse: ScanResponse) {
private val cardRepository: CardRepository = store.inject(DaggerGraphState::cardRepository)
private val cardRepository: CardRepository by lazy { store.inject(DaggerGraphState::cardRepository) }
private val onlineCardVerifier: OnlineCardVerifier by lazy { store.inject(DaggerGraphState::onlineCardVerifier) }
private var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
suspend fun loadArtworkUrl(): String {
val cardInfo = cardInfo
?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
?: onlineCardVerifier.getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
this.cardInfo = cardInfo
return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo)
return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo, onlineCardVerifier)
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {

View file

@ -26,9 +26,7 @@ import com.tangem.tap.store
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Use OnboardingNoteComponent")
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
@ -12,6 +11,7 @@ import com.tangem.common.routing.AppRouter
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
@ -24,7 +24,6 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.Artwork
@ -243,7 +242,9 @@ private suspend fun readCard(): CompletionResult<ScanResponse> {
}
private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri {
return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
val onlineCardVerifier = store.inject(DaggerGraphState::onlineCardVerifier)
return when (val cardInfo = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = cardInfo.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
@ -453,8 +454,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.dispatchOnMain(BackupAction.AddBackupCard.Success(result.data))
}
is CompletionResult.Failure -> {
val crashlytics = FirebaseCrashlytics.getInstance()
when (val error = result.error) {
is TangemSdkError.CardVerificationFailed -> {
Analytics.send(
@ -491,7 +490,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
GlobalAction.ShowDialog(BackupDialog.AttestationFailed),
)
}
else -> crashlytics.recordException(error)
else -> Analytics.sendException(ExceptionAnalyticsEvent(error))
}
}
}
@ -782,9 +781,10 @@ private suspend fun updateWallet(
}
private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet {
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse)
return requireNotNull(
value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
value = userWalletBuilder
.backupCardsIds(backupState.backupCardIds.toSet())
.hasBackupError(backupState.hasBackupError)
.build(),

View file

@ -5,7 +5,7 @@ import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog

View file

@ -6,7 +6,6 @@ import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
@ -70,8 +69,10 @@ internal class SaveWalletMiddleware {
scope.launch {
val backupInfo = state.backupInfo ?: error("Backup info is null")
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWallet = UserWalletBuilder(backupInfo.scanResponse, walletNameGenerateUseCase)
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory)
.create(scanResponse = backupInfo.scanResponse)
val userWallet = userWalletBuilder
.backupCardsIds(state.backupInfo.backupCardsIds)
.hasBackupError(hasBackupError)
.build()

View file

@ -11,10 +11,8 @@ import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.backupService
@ -118,9 +116,9 @@ internal class WelcomeMiddleware {
)
scanCardInternal { scanResponse ->
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build()
?: return@scanCardInternal
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse)
val userWallet = userWalletBuilder.build() ?: return@scanCardInternal
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet, canOverride = true)

View file

@ -6,6 +6,7 @@ import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.data.onramp.legacy.mercuryoNetwork
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading

View file

@ -26,13 +26,13 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@ -56,7 +56,6 @@ data class DaggerGraphState(
val generalUserWalletsListManager: UserWalletsListManager? = null,
val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null,
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
val generateWalletNameUseCase: GenerateWalletNameUseCase? = null,
val cardRepository: CardRepository? = null,
val settingsRepository: SettingsRepository? = null,
val blockchainSDKFactory: BlockchainSDKFactory? = null,
@ -67,7 +66,6 @@ data class DaggerGraphState(
val shareManager: ShareManager? = null,
val appRouter: AppRouter? = null,
val transactionSignerFactory: TransactionSignerFactory? = null,
val getUserCountryUseCase: GetUserCountryUseCase? = null,
val onrampFeatureToggles: OnrampFeatureToggles? = null,
val environmentConfigStorage: EnvironmentConfigStorage? = null,
val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null,
@ -77,4 +75,6 @@ data class DaggerGraphState(
val clipboardManager: ClipboardManager? = null,
val settingsManager: SettingsManager? = null,
val uiMessageSender: UiMessageSender? = null,
val onlineCardVerifier: OnlineCardVerifier? = null,
val userWalletBuilderFactory: UserWalletBuilder.Factory? = null,
) : StateType

View file

@ -1,8 +1,9 @@
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.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.navigation.Router
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -15,6 +16,7 @@ import kotlin.reflect.KClass
internal class ProxyAppRouter(
private val config: AppRouterConfig,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : AppRouter {
private val routerScope: CoroutineScope
@ -76,7 +78,7 @@ internal class ProxyAppRouter(
override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) {
if (!isSuccess) {
FirebaseCrashlytics.getInstance().recordException(RuntimeException(errorMessage))
analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage)))
Timber.w(errorMessage)
with(receiver = config.snackbarHandler ?: return) {

View file

@ -2,6 +2,7 @@ package com.tangem.tap.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.stories.api.StoriesComponent
@ -15,11 +16,13 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -41,6 +44,7 @@ import com.tangem.utils.Provider
import dagger.hilt.android.scopes.ActivityScoped
import java.util.WeakHashMap
import javax.inject.Inject
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedisegnedWalletConnectComponent
@ActivityScoped
@Suppress("LongParameterList", "LargeClass")
@ -74,8 +78,12 @@ internal class ChildFactory @Inject constructor(
private val referralComponentFactory: ReferralComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
private val walletComponentFactory: WalletEntryComponent.Factory,
private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory,
private val sendFeatureToggles: SendFeatureToggles,
private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory,
private val testerRouter: TesterRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
) {
fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child {
@ -256,18 +264,33 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.Send -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
if (sendFeatureToggles.isSendV2Enabled) {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = com.tangem.features.send.v2.api.SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactoryV2,
)
} else {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
}
}
is AppRoute.Home -> {
createComponentChild(
@ -277,17 +300,29 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.WalletConnectSessions -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
}
}
is AppRoute.QrScanning -> {
val source = when (route.source) {
AppRoute.QrScanning.Source.SEND -> SourceType.SEND
AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT
}
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = QrScanningComponent.Params(
source = route.source,
source = source,
networkName = route.networkName,
),
componentFactory = qrScanningComponentFactory,
@ -400,18 +435,33 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() })
}
is AppRoute.Send -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
if (sendFeatureToggles.isSendV2Enabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = com.tangem.features.send.v2.api.SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactoryV2,
)
} else {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
}
}
is AppRoute.AppSettings -> {
route.asComponentChild(
@ -483,10 +533,14 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { OnboardingWalletFragment() })
}
is AppRoute.QrScanning -> {
val source = when (route.source) {
AppRoute.QrScanning.Source.SEND -> SourceType.SEND
AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT
}
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = QrScanningComponent.Params(
source = route.source,
source = source,
networkName = route.networkName,
),
componentFactory = qrScanningComponentFactory,
@ -532,11 +586,19 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.WalletConnectSessions -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
}
}
is AppRoute.CurrencyDetails -> {
route.asComponentChild(

View file

@ -9,7 +9,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@ -147,9 +146,15 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class QrScanning(
val source: SourceType,
val source: Source,
val networkName: String? = null,
) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}")
) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}") {
enum class Source {
WALLET_CONNECT,
SEND,
}
}
@Serializable
data class ReferralProgram(

View file

@ -40,7 +40,12 @@ fun AmountScreenContent(
bottom = TangemTheme.dimens.spacing16,
),
) {
amountField(amountState = amountState, isBalanceHidden = isBalanceHidden)
amountField(
amountState = amountState,
isBalanceHidden = isBalanceHidden,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,

View file

@ -6,7 +6,7 @@ import com.tangem.utils.transformer.Transformer
/**
* Dismisses indication on pasted value
*/
class AmountPastedTriggerDismissTransformer : Transformer<AmountState> {
object AmountPastedTriggerDismissTransformer : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
if (prevState !is AmountState.Data) return prevState

View file

@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverterV2
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState
@ -30,6 +31,7 @@ import kotlinx.collections.immutable.persistentListOf
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
* @property iconStateConverter currency icon converter
*/
@Deprecated("Use AmountStateConverterV2")
class AmountStateConverter(
private val clickIntents: AmountScreenClickIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
@ -80,4 +82,63 @@ class AmountStateConverter(
selectedButton = 0,
)
}
}
/**
* Converts initial [String] to [AmountState]
*
* @property clickIntents amount screen clicks
* @property appCurrency selected app currency
* @property maxEnterAmount max enter amount data
* @property cryptoCurrencyStatus current cryptocurrency status
* @property iconStateConverter currency icon converter
*/
class AmountStateConverterV2(
private val clickIntents: AmountScreenClickIntents,
private val appCurrency: AppCurrency,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
AmountFieldConverterV2(
clickIntents = clickIntents,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency,
)
}
override fun convert(value: AmountParameters): AmountState {
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
return AmountState.Data(
title = value.title,
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus),
amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false,
appCurrencyCode = appCurrency.code,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(
title = stringReference(cryptoCurrencyStatus.currency.symbol),
iconState = iconStateConverter.convertCustom(
value = cryptoCurrencyStatus,
forceGrayscale = noFeeRate,
showCustomTokenBadge = false,
),
isFiat = false,
),
AmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
),
isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0,
)
}
}

View file

@ -31,6 +31,7 @@ class AmountFieldChangeTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxEnterAmount: EnterAmountBoundary,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
private val value: String,
) : Transformer<AmountState> {
@ -58,7 +59,7 @@ class AmountFieldChangeTransformer(
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField)
val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false
val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } == true
val isZero = if (amountTextField.isFiatValue) {
decimalFiatValue.isNullOrZero()
} else {
@ -67,6 +68,7 @@ class AmountFieldChangeTransformer(
val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided
return prevState.copy(
isPrimaryButtonEnabled = !isZero && !isCheckFailed,
reduceAmountBy = reduceAmountBy,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
@ -74,10 +76,11 @@ class AmountFieldChangeTransformer(
error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount
?.amount
?.format { crypto(cryptoCurrencyStatus.currency) }
.orEmpty()
val minimumAmount =
minimumTransactionAmount.amount.format {
crypto(cryptoCurrencyStatus.currency)
}
resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount),
@ -98,6 +101,7 @@ class AmountFieldChangeTransformer(
private fun AmountState.Data.emptyState(): AmountState.Data {
return copy(
isPrimaryButtonEnabled = false,
reduceAmountBy = BigDecimal.ZERO,
amountTextField = amountTextField.copy(
value = "",
fiatValue = "",

View file

@ -19,12 +19,13 @@ import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
/**
* Converts initial [String] to [AmountField]
* Converts initial [String] to [AmountFieldModel]
*
* @property clickIntents amount screen clicks
* @property appCurrencyProvider selected app currency provider
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
*/
@Deprecated("Use AmountFieldConverterV2")
class AmountFieldConverter(
private val clickIntents: AmountScreenClickIntents,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
@ -76,6 +77,68 @@ class AmountFieldConverter(
type = AmountType.FiatType(appCurrency.code),
)
private companion object {
private const val FIAT_DECIMALS = 2
}
}
/**
* Converts initial [String] to [AmountFieldModel]
*
* @property clickIntents amount screen clicks
* @property appCurrency selected app currency
* @property cryptoCurrencyStatus current cryptocurrency status
*/
class AmountFieldConverterV2(
private val clickIntents: AmountScreenClickIntents,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
) : Converter<String, AmountFieldModel> {
override fun convert(value: String): AmountFieldModel {
val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val (fiatValue, fiatDecimal) = when {
fiatRate.isNullOrZero() -> "" to null
value.isEmpty() -> "" to BigDecimal.ZERO
else -> {
val fiatDecimal = fiatRate?.multiply(cryptoDecimal)
val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty()
fiatValue to fiatDecimal
}
}
val isDoneActionEnabled = !cryptoDecimal.isNullOrZero()
return AmountFieldModel(
value = value,
fiatValue = fiatValue,
onValueChange = clickIntents::onAmountValueChange,
keyboardOptions = KeyboardOptions(
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onAmountNext() },
),
isFiatValue = false,
cryptoAmount = cryptoAmount,
fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrency),
isError = false,
isWarning = false,
error = TextReference.EMPTY,
isFiatUnavailable = fiatRate == null,
isValuePasted = false,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
}
private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount(
currencySymbol = appCurrency.symbol,
value = fiatValue,
decimals = FIAT_DECIMALS,
type = AmountType.FiatType(appCurrency.code),
)
private companion object {
private const val FIAT_DECIMALS = 2
}

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.extensions.isZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
import java.math.RoundingMode
/**
@ -26,6 +27,7 @@ class AmountFieldSetMaxAmountTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxAmount: EnterAmountBoundary,
private val minAmount: EnterAmountBoundary?,
private val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
@ -42,9 +44,10 @@ class AmountFieldSetMaxAmountTransformer(
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty()
val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } ?: false
val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } == true
return prevState.copy(
isPrimaryButtonEnabled = !isLessThanMinimumIfProvided,
reduceAmountBy = reduceAmountBy,
amountTextField = amountTextField.copy(
isValuePasted = true,
value = cryptoValue,
@ -52,10 +55,7 @@ class AmountFieldSetMaxAmountTransformer(
isError = isLessThanMinimumIfProvided,
error = when {
isLessThanMinimumIfProvided -> {
val minimumAmount = minAmount
?.amount
?.format { crypto(cryptoCurrencyStatus.currency) }
.orEmpty()
val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount),

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal
/** Model for amount state */
@Stable
@ -21,6 +22,8 @@ sealed class AmountState {
* @param isSegmentedButtonsEnabled indicates if currency switches is enabled
* @param amountTextField amount field state
* @param appCurrencyCode app currency code
* @param isEditingDisabled indicated whether amount is editable
* @param reduceAmountBy reduces amount to be sent by specified value
*/
data class Data(
override val isPrimaryButtonEnabled: Boolean,
@ -32,6 +35,8 @@ sealed class AmountState {
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: AmountFieldModel,
val appCurrencyCode: String,
val isEditingDisabled: Boolean = false,
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
) : AmountState()
data class Empty(

View file

@ -25,12 +25,13 @@ import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTr
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
@Deprecated("Use AmountField with clicks")
@Composable
internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) {
val decimalFormat = rememberDecimalFormat()
@ -80,6 +81,60 @@ internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String)
AmountSecondary(amountField, appCurrencyCode)
}
@Composable
internal fun AmountField(
amountField: AmountFieldModel,
appCurrencyCode: String,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
) {
val decimalFormat = rememberDecimalFormat()
val isFiatValue = amountField.isFiatValue
val currencyCode = if (isFiatValue) appCurrencyCode else null
val (primaryAmount, primaryValue) = if (isFiatValue) {
amountField.fiatAmount to amountField.fiatValue
} else {
amountField.cryptoAmount to amountField.value
}
val requester = remember { FocusRequester() }
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
visualTransformation = AmountVisualTransformation(
decimals = primaryAmount.decimals,
symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode,
decimalFormat = decimalFormat,
),
onValueChange = onValueChange,
keyboardOptions = amountField.keyboardOptions,
keyboardActions = amountField.keyboardActions,
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
isAutoResize = true,
isValuePasted = amountField.isValuePasted,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
modifier = Modifier
.focusRequester(requester)
.padding(
top = TangemTheme.dimens.spacing24,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
)
.requiredHeightIn(min = TangemTheme.dimens.size32),
)
LaunchedEffect(key1 = Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
AmountSecondary(amountField, appCurrencyCode)
}
@Composable
private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) {
val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount
@ -96,11 +151,12 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
val text = if (amountField.isFiatValue) {
secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) }
} else {
BigDecimalFormatter.formatFiatAmount(
fiatAmount = secondaryAmount.value,
fiatCurrencySymbol = secondaryAmount.currencySymbol,
fiatCurrencyCode = appCurrencyCode,
)
secondaryAmount.value.format {
fiat(
fiatCurrencySymbol = secondaryAmount.currencySymbol,
fiatCurrencyCode = appCurrencyCode,
)
}
}
Text(
text = text,

View file

@ -24,6 +24,8 @@ internal fun LazyListScope.amountField(
amountState: AmountState.Data,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
) {
item(key = AMOUNT_FIELD_KEY) {
Column(
@ -63,6 +65,8 @@ internal fun LazyListScope.amountField(
AmountField(
amountField = amountState.amountTextField,
appCurrencyCode = amountState.appCurrencyCode,
onValueChange = onValueChange,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
)
}
}

View file

@ -159,6 +159,11 @@ sealed class NotificationUM(val config: NotificationConfig) {
),
subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text),
)
data object DestinationMemoRequired : Error(
title = resourceReference(id = R.string.send_validation_destination_tag_required_title),
subtitle = resourceReference(id = R.string.send_validation_destination_tag_required_description),
)
}
open class Warning(

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.common.ui.notifications.NotificationsFactory.addRequireDestinationFlagErrorNotification
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
@ -112,6 +113,10 @@ object NotificationsFactory {
}
}
fun MutableList<NotificationUM>.addRequireDestinationFlagErrorNotification() {
add(NotificationUM.Error.DestinationMemoRequired)
}
fun MutableList<NotificationUM>.addMinimumAmountErrorNotification(
minimumSendAmount: BigDecimal?,
sendingAmount: BigDecimal,
@ -326,6 +331,9 @@ object NotificationsFactory {
error = validationError,
onReduceClick = onReduceClick,
)
is BlockchainSdkError.XRP -> addXRPTransactionValidationError(
error = validationError,
)
null -> minAdaValue?.let {
add(
NotificationUM.Cardano.MinAdaValueCharged(
@ -406,6 +414,12 @@ object NotificationsFactory {
}
}
private fun MutableList<NotificationUM>.addXRPTransactionValidationError(error: BlockchainSdkError.XRP) {
when (error) {
is BlockchainSdkError.XRP.DestinationMemoRequired -> addRequireDestinationFlagErrorNotification()
}
}
fun MutableList<NotificationUM>.addRentExemptionNotification(rentWarning: CryptoCurrencyWarning.Rent?) {
if (rentWarning == null) return
add(NotificationUM.Solana.RentInfo(rentWarning))

View file

@ -7,7 +7,6 @@ open class AnalyticsEvent(
val category: String,
val event: String,
var params: Map<String, String> = mapOf(),
val error: Throwable? = null,
) {
val id: String = "[$category] $event"

View file

@ -3,8 +3,7 @@ package com.tangem.core.analytics.models
sealed class Basic(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : AnalyticsEvent("Basic", event, params, error) {
) : AnalyticsEvent("Basic", event, params) {
class CardWasScanned(
source: AnalyticsParam.ScreensSources,
@ -73,11 +72,6 @@ sealed class Basic(
}
}
class ScanError(error: Throwable) : Basic(
event = "Scan",
error = error,
)
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
event = "Request Support",
params = mapOf(

View file

@ -0,0 +1,8 @@
package com.tangem.core.analytics.models
class ExceptionAnalyticsEvent(
val exception: Throwable,
// val category: String,
// val event: String,
var params: Map<String, String> = mapOf(),
)

View file

@ -2,6 +2,7 @@ package com.tangem.core.analytics
import com.tangem.core.analytics.api.*
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
@ -16,7 +17,9 @@ interface GlobalAnalyticsEventHandler :
AnalyticsEventHandler,
AnalyticsHandlerHolder,
AnalyticsFilterHolder,
ParamsInterceptorHolder
ParamsInterceptorHolder,
AnalyticsErrorHandler,
AnalyticsExceptionHandler
object Analytics : GlobalAnalyticsEventHandler {
@ -72,6 +75,25 @@ object Analytics : GlobalAnalyticsEventHandler {
}
}
override fun sendErrorEvent(event: AnalyticsEvent) {
analyticsScope.launch {
event.params = applyParamsInterceptors(event)
analyticsMutex.withLock {
analyticsHandlers.filterIsInstance<AnalyticsErrorHandler>()
.forEach { handler -> handler.sendErrorEvent(event) }
}
}
}
override fun sendException(event: ExceptionAnalyticsEvent) {
analyticsScope.launch {
analyticsMutex.withLock {
analyticsHandlers.filterIsInstance<AnalyticsExceptionHandler>()
.forEach { it.sendException(event) }
}
}
}
private suspend fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap<String, String> {
val interceptedParams = event.params.toMutableMap()
analyticsMutex.withLock {

View file

@ -8,5 +8,9 @@ interface EventLogger {
}
interface ErrorEventLogger {
fun logErrorEvent(error: Throwable, params: Map<String, String> = emptyMap())
fun logErrorEvent(event: String, params: Map<String, String> = emptyMap())
}
interface ExceptionLogger {
fun logException(error: Throwable, params: Map<String, String> = emptyMap())
}

View file

@ -1,15 +1,25 @@
package com.tangem.core.analytics.api
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
/**
[REDACTED_AUTHOR]
* Sends usual analytics events
*/
interface AnalyticsEventHandler {
fun send(event: AnalyticsEvent)
}
interface AnalyticsErrorHandler {
fun sendErrorEvent(event: AnalyticsEvent)
}
interface AnalyticsExceptionHandler {
fun sendException(event: ExceptionAnalyticsEvent)
}
interface AnalyticsHandler : AnalyticsEventHandler {
fun id(): String
fun send(eventId: String, params: Map<String, String> = emptyMap())
@ -19,10 +29,6 @@ interface AnalyticsHandler : AnalyticsEventHandler {
}
}
interface ErrorEventHandler {
fun send(error: Throwable, params: Map<String, String> = emptyMap())
}
interface AnalyticsHandlerHolder {
fun addHandler(name: String, handler: AnalyticsHandler)
fun removeHandler(name: String): AnalyticsHandler?

View file

@ -1,7 +1,9 @@
package com.tangem.core.analytics.di
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.api.ParamsInterceptorHolder
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.domain.analytics.repository.AnalyticsRepository
@ -21,6 +23,18 @@ internal object AnalyticsModule {
return Analytics // todo replace after refactoring calling Analytics in whole project
}
@Singleton
@Provides
fun provideAnalyticsErrorHandler(): AnalyticsErrorHandler {
return Analytics
}
@Singleton
@Provides
fun provideAnalyticsExceptionHandler(): AnalyticsExceptionHandler {
return Analytics
}
@Singleton
@Provides
fun provideParamsInterceptorHolder(): ParamsInterceptorHolder {

View file

@ -65,6 +65,22 @@
},
{
"name": "NOTE_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "NEW_ATTESTATION_ENABLED",
"version": "undefined"
},
{
"name": "TWIN_REFACTORING_ENABLED",
"version": "undefined"
},
{
"name": "SEND_V2_ENABLED",
"version": "undefined"
},
{
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
}
]

View file

@ -1,16 +1,18 @@
package com.tangem.datasource.api.common.response
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type
internal class ApiResponseCallAdapter(
private val resultType: Type,
private val analyticsErrorHandler: AnalyticsErrorHandler,
) : CallAdapter<Type, Call<ApiResponse<Type>>> {
override fun responseType(): Type = resultType
override fun adapt(call: Call<Type>): Call<ApiResponse<Type>> {
return ApiResponseCallDelegate(call)
return ApiResponseCallDelegate(call, analyticsErrorHandler)
}
}

View file

@ -1,12 +1,15 @@
package com.tangem.datasource.api.common.response
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
class ApiResponseCallAdapterFactory private constructor(
private val analyticsErrorHandler: AnalyticsErrorHandler,
) : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) {
@ -19,11 +22,11 @@ class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory(
}
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
return ApiResponseCallAdapter(resultType)
return ApiResponseCallAdapter(resultType, analyticsErrorHandler)
}
companion object {
fun create() = ApiResponseCallAdapterFactory()
fun create(analyticsErrorHandler: AnalyticsErrorHandler) = ApiResponseCallAdapterFactory(analyticsErrorHandler)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.common.response
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
@ -9,6 +10,7 @@ import timber.log.Timber
internal class ApiResponseCallDelegate<T : Any>(
private val wrappedCall: Call<T>,
private val analyticsErrorHandler: AnalyticsErrorHandler,
) : Call<ApiResponse<T>> {
override fun enqueue(callback: Callback<ApiResponse<T>>) {
@ -16,7 +18,7 @@ internal class ApiResponseCallDelegate<T : Any>(
}
override fun execute(): Response<ApiResponse<T>> = throw NotImplementedError()
override fun clone(): Call<ApiResponse<T>> = ApiResponseCallDelegate(wrappedCall.clone())
override fun clone(): Call<ApiResponse<T>> = ApiResponseCallDelegate(wrappedCall.clone(), analyticsErrorHandler)
override fun request(): Request = wrappedCall.request()
override fun timeout(): Timeout = wrappedCall.timeout()
override fun isExecuted(): Boolean = wrappedCall.isExecuted
@ -30,7 +32,7 @@ internal class ApiResponseCallDelegate<T : Any>(
) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val safeResponse = response.toSafeApiResponse()
val safeResponse = response.toSafeApiResponse(analyticsErrorHandler)
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
}

View file

@ -19,51 +19,51 @@ sealed class ApiResponseError : Exception() {
) : ApiResponseError() {
// region Error Codes
enum class Code(val code: Int) {
enum class Code(val numericCode: Int) {
// 4xx Server Errors
BAD_REQUEST(code = 400),
UNAUTHORIZED(code = 401),
PAYMENT_REQUIRED(code = 402),
FORBIDDEN(code = 403),
NOT_FOUND(code = 404),
METHOD_NOT_ALLOWED(code = 405),
NOT_ACCEPTABLE(code = 406),
PROXY_AUTHENTICATION_REQUIRED(code = 407),
REQUEST_TIMEOUT(code = 408),
CONFLICT(code = 409),
GONE(code = 410),
LENGTH_REQUIRED(code = 411),
PRECONDITION_FAILED(code = 412),
PAYLOAD_TOO_LARGE(code = 413),
URI_TOO_LONG(code = 414),
UNSUPPORTED_MEDIA_TYPE(code = 415),
RANGE_NOT_SATISFIABLE(code = 416),
EXPECTATION_FAILED(code = 417),
IM_A_TEAPOT(code = 418), // Not an error, but an April Fools' joke from RFC 2324
UNPROCESSABLE_ENTITY(code = 422),
LOCKED(code = 423),
FAILED_DEPENDENCY(code = 424),
TOO_EARLY(code = 425),
UPGRADE_REQUIRED(code = 426),
PRECONDITION_REQUIRED(code = 428),
TOO_MANY_REQUESTS(code = 429),
REQUEST_HEADER_FIELDS_TOO_LARGE(code = 431),
UNAVAILABLE_FOR_LEGAL_REASONS(code = 451),
BAD_REQUEST(numericCode = 400),
UNAUTHORIZED(numericCode = 401),
PAYMENT_REQUIRED(numericCode = 402),
FORBIDDEN(numericCode = 403),
NOT_FOUND(numericCode = 404),
METHOD_NOT_ALLOWED(numericCode = 405),
NOT_ACCEPTABLE(numericCode = 406),
PROXY_AUTHENTICATION_REQUIRED(numericCode = 407),
REQUEST_TIMEOUT(numericCode = 408),
CONFLICT(numericCode = 409),
GONE(numericCode = 410),
LENGTH_REQUIRED(numericCode = 411),
PRECONDITION_FAILED(numericCode = 412),
PAYLOAD_TOO_LARGE(numericCode = 413),
URI_TOO_LONG(numericCode = 414),
UNSUPPORTED_MEDIA_TYPE(numericCode = 415),
RANGE_NOT_SATISFIABLE(numericCode = 416),
EXPECTATION_FAILED(numericCode = 417),
IM_A_TEAPOT(numericCode = 418), // Not an error, but an April Fools' joke from RFC 2324
UNPROCESSABLE_ENTITY(numericCode = 422),
LOCKED(numericCode = 423),
FAILED_DEPENDENCY(numericCode = 424),
TOO_EARLY(numericCode = 425),
UPGRADE_REQUIRED(numericCode = 426),
PRECONDITION_REQUIRED(numericCode = 428),
TOO_MANY_REQUESTS(numericCode = 429),
REQUEST_HEADER_FIELDS_TOO_LARGE(numericCode = 431),
UNAVAILABLE_FOR_LEGAL_REASONS(numericCode = 451),
// 5xx Server Errors
INTERNAL_SERVER_ERROR(code = 500),
NOT_IMPLEMENTED(code = 501),
BAD_GATEWAY(code = 502),
SERVICE_UNAVAILABLE(code = 503),
GATEWAY_TIMEOUT(code = 504),
HTTP_VERSION_NOT_SUPPORTED(code = 505),
VARIANT_ALSO_NEGOTIATES(code = 506),
INSUFFICIENT_STORAGE(code = 507),
LOOP_DETECTED(code = 508),
NOT_EXTENDED(code = 510),
NETWORK_AUTHENTICATION_REQUIRED(code = 511),
INTERNAL_SERVER_ERROR(numericCode = 500),
NOT_IMPLEMENTED(numericCode = 501),
BAD_GATEWAY(numericCode = 502),
SERVICE_UNAVAILABLE(numericCode = 503),
GATEWAY_TIMEOUT(numericCode = 504),
HTTP_VERSION_NOT_SUPPORTED(numericCode = 505),
VARIANT_ALSO_NEGOTIATES(numericCode = 506),
INSUFFICIENT_STORAGE(numericCode = 507),
LOOP_DETECTED(numericCode = 508),
NOT_EXTENDED(numericCode = 510),
NETWORK_AUTHENTICATION_REQUIRED(numericCode = 511),
;
override fun toString(): String = "$code - $name"
override fun toString(): String = "$numericCode - $name"
companion object {
val values = values()

View file

@ -1,5 +1,7 @@
package com.tangem.datasource.api.common.response
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.api.common.response.analytics.ApiErrorEvent
import kotlinx.coroutines.TimeoutCancellationException
import retrofit2.Response
import timber.log.Timber
@ -9,18 +11,20 @@ import java.net.UnknownHostException
import java.util.concurrent.TimeoutException
import javax.net.ssl.SSLHandshakeException
internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: AnalyticsErrorHandler): ApiResponse<T> {
val body = body()
return if (isSuccessful && body != null) {
apiSuccess(body)
} else {
val code = ApiResponseError.HttpException.Code.values
.firstOrNull { it.code == code() }
.firstOrNull { it.numericCode == code() }
val e = try {
if (code == null) {
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
} else {
sendHttpError(code, analyticsErrorHandler)
ApiResponseError.HttpException(code, message(), errorBody()?.string())
}
} catch (e: Exception) {
@ -32,6 +36,21 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
}
}
private fun <T : Any> Response<T>.sendHttpError(
code: ApiResponseError.HttpException.Code,
analyticsErrorHandler: AnalyticsErrorHandler,
) {
val fullRequestUrl = raw().request.url.toUrl()
val shortUrl = fullRequestUrl.authority + fullRequestUrl.path
analyticsErrorHandler.sendErrorEvent(
ApiErrorEvent(
endpoint = shortUrl,
code = code.numericCode,
message = errorBody()?.string().orEmpty(),
),
)
}
internal fun Throwable.toApiError(): ApiResponseError = when (this) {
is ConnectException,
is UnknownHostException,

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.common.response.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
internal data class ApiErrorEvent(
val endpoint: String,
val code: Int,
val message: String,
) : AnalyticsEvent(
category = "Tangem API Service",
event = "Exception",
params = mapOf(
"Endpoint" to endpoint,
"Code" to code.toString(),
"Message" to message,
),
)

View file

@ -20,7 +20,7 @@ data class TokenMarketListResponse(
@Json(name = "name") val name: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "current_price") val currentPrice: BigDecimal,
@Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage,
@Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage?,
@Json(name = "market_rating") val marketRating: Int?,
@Json(name = "market_cap") val marketCap: BigDecimal?,
@Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?,
@ -28,9 +28,9 @@ data class TokenMarketListResponse(
@JsonClass(generateAdapter = true)
data class PriceChangePercentage(
@Json(name = "24h") val h24: BigDecimal,
@Json(name = "1w") val week1: BigDecimal,
@Json(name = "30d") val day30: BigDecimal,
@Json(name = "24h") val h24: BigDecimal?,
@Json(name = "1w") val week1: BigDecimal?,
@Json(name = "30d") val day30: BigDecimal?,
)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.request.Address
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.PendingAction.PendingActionArgs.Amount
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import org.joda.time.DateTime
import java.math.BigDecimal
@ -31,6 +32,8 @@ data class BalanceDTO(
val pricePerShare: BigDecimal,
@Json(name = "pendingActions")
val pendingActions: List<PendingAction>,
@Json(name = "pendingActionConstraints")
val pendingActionConstraints: List<PendingActionConstraints>?,
@Json(name = "token")
val tokenDTO: TokenDTO,
@Json(name = "validatorAddress")
@ -99,7 +102,7 @@ data class BalanceDTO(
@JsonClass(generateAdapter = true)
data class Amount(
@Json(name = "required")
val required: Boolean,
val required: Boolean = true,
@Json(name = "minimum")
val minimum: BigDecimal?,
@Json(name = "maximum")
@ -136,6 +139,14 @@ data class BalanceDTO(
}
}
@JsonClass(generateAdapter = true)
data class PendingActionConstraints(
@Json(name = "type")
val type: StakingActionTypeDTO,
@Json(name = "amount")
val amountArg: Amount,
)
@JsonClass(generateAdapter = true)
data class Required(
@Json(name = "required")

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
@ -61,6 +62,7 @@ internal object NetworkModule {
fun provideExpressApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): TangemExpressApi {
@ -69,6 +71,7 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
@ -83,6 +86,7 @@ internal object NetworkModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
apiConfigsManager: ApiConfigsManager,
analyticsErrorHandler: AnalyticsErrorHandler,
appLogsStore: AppLogsStore,
): StakeKitApi {
return createApi(
@ -90,6 +94,7 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
timeouts = Timeouts(
callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
@ -109,6 +114,7 @@ internal object NetworkModule {
fun provideOnrampApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): OnrampApi {
@ -117,6 +123,7 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
@ -130,6 +137,7 @@ internal object NetworkModule {
fun provideTangemTechApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
): TangemTechApi {
return createApi(
@ -137,6 +145,7 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = { applyTimeoutAnnotations() },
)
}
@ -147,6 +156,7 @@ internal object NetworkModule {
fun provideTangemTechApiV2(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
appVersionProvider: AppVersionProvider,
): TangemTechApiV2 {
return provideTangemTechApiInternal(
@ -154,6 +164,7 @@ internal object NetworkModule {
context = context,
appVersionProvider = appVersionProvider,
baseUrl = PROD_V2_TANGEM_TECH_BASE_URL,
analyticsErrorHandler = analyticsErrorHandler,
)
}
@ -162,6 +173,7 @@ internal object NetworkModule {
fun provideTangemTechMarketsApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
): TangemTechMarketsApi {
return createApi(
@ -169,6 +181,7 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -183,6 +196,7 @@ internal object NetworkModule {
fun provideTangemVisaAuthApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): TangemVisaAuthApi {
@ -191,6 +205,7 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
@ -204,6 +219,7 @@ internal object NetworkModule {
fun provideTangemVisaApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): TangemVisaApi {
@ -212,6 +228,7 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
@ -226,6 +243,7 @@ internal object NetworkModule {
context: Context,
appVersionProvider: AppVersionProvider,
baseUrl: String,
analyticsErrorHandler: AnalyticsErrorHandler,
timeouts: Timeouts = Timeouts(),
requestHeaders: List<RequestHeader> = listOf(AppVersionPlatformHeaders(appVersionProvider)),
): T {
@ -257,7 +275,7 @@ internal object NetworkModule {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler))
.baseUrl(baseUrl)
.client(client)
.build()
@ -269,6 +287,7 @@ internal object NetworkModule {
moshi: Moshi,
context: Context,
apiConfigsManager: ApiConfigsManager,
analyticsErrorHandler: AnalyticsErrorHandler,
timeouts: Timeouts = Timeouts(),
clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this },
): T {
@ -276,7 +295,7 @@ internal object NetworkModule {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler))
.baseUrl(environmentConfig.baseUrl)
.client(
OkHttpClient.Builder()

View file

@ -4,18 +4,15 @@ import android.content.Context
import com.tangem.datasource.BuildConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
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 java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.inject.Inject
import javax.inject.Singleton
@ -30,7 +27,7 @@ import javax.inject.Singleton
@Singleton
class AppLogsStore @Inject constructor(
@ApplicationContext private val applicationContext: Context,
dispatchers: CoroutineDispatcherProvider,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val scope = CoroutineScope(
@ -38,8 +35,10 @@ class AppLogsStore @Inject constructor(
CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") },
)
private val mutex = Mutex()
private val zipMutex = Mutex()
private val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME)
private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME)
private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
private val formatter = DateTimeFormatterBuilder()
.appendDayOfMonth(2)
@ -58,6 +57,16 @@ class AppLogsStore @Inject constructor(
/** Get log file */
fun getFile(): File? = if (file.exists()) file else null
suspend fun getZipFile(): File? {
return zipMutex.withLock {
if (file.exists()) {
zip(listOf(file), fileZip)
} else {
null
}
}
}
/** Save log [message] */
fun saveLogMessage(tag: String, message: String) {
// Temporally logs are not saved in prod environment
@ -133,8 +142,41 @@ class AppLogsStore @Inject constructor(
}
}
@Suppress("NestedBlockDepth")
private suspend fun zip(filesToCompress: List<File>, outputZipFile: File): File? {
return withContext(dispatchers.io) {
if (outputZipFile.exists() && !outputZipFile.delete()) {
return@withContext null
}
val buffer = ByteArray(BUFFER_SIZE)
FileOutputStream(outputZipFile).use { fos ->
ZipOutputStream(fos).use { zos ->
filesToCompress.forEach { file ->
FileInputStream(file).use { inStream ->
val ze = ZipEntry(file.name)
zos.putNextEntry(ze)
var len: Int
while (inStream.read(buffer).also { len = it } > 0) {
zos.write(buffer, 0, len)
}
}
}
zos.finish() // Ensures the zip output is finalized
}
}
outputZipFile
}
}
private companion object {
const val BUFFER_SIZE = 1024
const val LOG_FILE_NAME = "logs.txt"
const val NEW_LOG_FILE_NAME = "app_logs.txt"
// the only name that we allow to send as email to company addresses
const val PERMITTED_FILE_NAME = "log.txt"
const val PERMITTED_FILE_NAME_ZIP = "log.zip"
}
}

View file

@ -12,7 +12,7 @@ internal class DefaultNFTPersistenceStore(
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
) : NFTPersistenceStore {
override fun getCollections(): Flow<List<NFTCollection>> = collectionsPersistenceStore.data
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
override suspend fun getCollectionsSync(): List<NFTCollection>? = collectionsPersistenceStore
.data

View file

@ -5,7 +5,7 @@ import com.tangem.blockchain.nft.models.NFTCollection
import kotlinx.coroutines.flow.Flow
interface NFTPersistenceStore {
fun getCollections(): Flow<List<NFTCollection>>
fun getCollections(): Flow<List<NFTCollection>?>
suspend fun getCollectionsSync(): List<NFTCollection>?

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.local.nft
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
@ -11,10 +12,12 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithCustomKeyTypes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import java.lang.reflect.ParameterizedType
import javax.inject.Inject
import javax.inject.Singleton
@ -25,41 +28,51 @@ class NFTPersistenceStoreFactory @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) {
fun provide(network: Network): NFTPersistenceStore {
val networkStringIdentifier = listOfNotNull(
network.id.value,
network.derivationPath.value,
).joinToString("_") {
// remove all non-alphanumeric characters
it
.toCharArray()
.filter(Char::isLetterOrDigit)
.joinToString("")
.lowercase()
}
fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore {
// simplify network identifier so that is correct for a file name
// e.g. eth_m4460000 or theopennetwork_m446070
val networkStringId =
network.id.formatted() + network.derivationPath.formatted()?.let { "_$it" }.orEmpty()
// simplify user wallet id so that is correct for a file name
// e.g. 9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a
val userWalletStringId = userWalletId.formatted()
return DefaultNFTPersistenceStore(
collectionsPersistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = listTypes<NFTCollection>(),
defaultValue = emptyList(),
),
produceFile = {
context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_collections")
},
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
collectionsPersistenceStore = createPersistenceStore(
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_collections
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_collections
fileName = "nft_${userWalletStringId}_${networkStringId}_collections",
types = listTypes<NFTCollection>(),
defaultValue = emptyList(),
),
pricesPersistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
defaultValue = emptyMap(),
),
produceFile = {
context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_prices")
},
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
pricesPersistenceStore = createPersistenceStore(
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
defaultValue = emptyMap(),
),
)
}
private fun <T> createPersistenceStore(fileName: String, types: ParameterizedType, defaultValue: T): DataStore<T> =
DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = types,
defaultValue = defaultValue,
),
produceFile = { context.dataStoreFile(fileName = fileName) },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
private fun Network.ID.formatted(): String = value
.filter(Char::isLetterOrDigit)
.lowercase()
private fun Network.DerivationPath.formatted(): String? = value
?.filter(Char::isLetterOrDigit)
?.lowercase()
private fun UserWalletId.formatted(): String = stringValue
.lowercase()
}

View file

@ -7,14 +7,11 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
class NFTSdkAssetConverter(
private val nftSdkAssetIdentifierConverter: NFTSdkAssetIdentifierConverter,
private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter,
) : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
override fun convert(value: Pair<Network, SdkNFTAsset>): NFTAsset {
val (network, asset) = value
val assetId = nftSdkAssetIdentifierConverter.convert(asset.identifier)
val collectionId = nftSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier)
val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier)
val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier)
return NFTAsset.Value(
id = assetId,
collectionId = collectionId,

View file

@ -6,13 +6,10 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
class NFTSdkCollectionConverter(
private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter,
private val nftSdkAssetConverter: NFTSdkAssetConverter,
) : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
override fun convert(value: Pair<Network, SdkNFTCollection>): NFTCollection {
val (network, collection) = value
val collectionId = nftSdkCollectionIdentifierConverter.convert(collection.identifier)
val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier)
return NFTCollection(
id = collectionId,
network = network,
@ -22,7 +19,7 @@ class NFTSdkCollectionConverter(
count = collection.count,
assets = collection.assets
.map { asset ->
nftSdkAssetConverter.convert(network to asset)
NFTSdkAssetConverter.convert(network to asset)
}
.filter {
it.id !is NFTAsset.Identifier.Unknown

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.PendingActionConstraints
import com.tangem.utils.converter.Converter
internal object PendingActionConstraintsConverter :
Converter<BalanceDTO.PendingActionConstraints, PendingActionConstraints> {
override fun convert(value: BalanceDTO.PendingActionConstraints): PendingActionConstraints {
return PendingActionConstraints(
type = StakingActionTypeConverter.convert(value.type),
amountArg = PendingAction.PendingActionArgs.Amount(
required = value.amountArg.required,
minimum = value.amountArg.minimum,
maximum = value.amountArg.maximum,
),
)
}
}

View file

@ -38,6 +38,8 @@ internal class YieldBalanceConverter(
pendingActions = PendingActionConverter
.convertList(item.pendingActions)
.sortedBy { it.passthrough },
pendingActionsConstraints = PendingActionConstraintsConverter
.convertList(item.pendingActionConstraints.orEmpty()),
isPending = false,
)
}

View file

@ -139,6 +139,7 @@
<string name="common_fee_selector_option_market">Markt</string>
<string name="common_fee_selector_option_slow">Langsam</string>
<string name="common_fee_selector_title">Gebühren</string>
<string name="common_finish">Beenden</string>
<string name="common_generate_addresses">Adressen synchronisieren</string>
<string name="common_go_to_provider">Zum Anbieter gehen</string>
<string name="common_go_to_token">Zum Token</string>
@ -158,6 +159,7 @@
<string name="common_no_address">Keine Adresse</string>
<string name="common_now">Jetzt</string>
<string name="common_ok">OK</string>
<string name="common_open_in_browser">Im Browser öffnen</string>
<string name="common_origin_card">Hauptkarte oder Ring</string>
<string name="common_origin_ring">Primärring</string>
<string name="common_passphrase">Passphrase</string>
@ -181,6 +183,7 @@
<string name="common_send">Senden</string>
<string name="common_server_unavailable">Der Server ist nicht verfügbar. Bitte versuche es später erneut.</string>
<string name="common_share">Teilen</string>
<string name="common_share_link">Link teilen</string>
<string name="common_sign">Signieren</string>
<string name="common_sign_and_send">Signieren und senden</string>
<string name="common_stake">Staken</string>
@ -495,6 +498,10 @@
<string name="markets_tooltip_message">Rufe dies auf oder tippe auf die Suchleiste, um Token direkt vom Markt hinzuzufügen</string>
<string name="markets_tooltip_title">Token hinzufügen</string>
<string name="nfc_error_unavailable">NFC ist auf deinem Gerät nicht verfügbar</string>
<string name="nft_wallet_count">%1$d NFTs in der %2$d Sammlung</string>
<string name="nft_wallet_receive_nft">Tippe hier, um das erste NFT zu erhalten</string>
<string name="nft_wallet_title">NFT-Sammlungen</string>
<string name="nft_wallet_unable_to_load">Die Daten konnten nicht geladen werden</string>
<string name="onboarding_access_code_feature_1_description">Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen</string>
<string name="onboarding_access_code_feature_1_title">Schützen</string>
<string name="onboarding_access_code_feature_2_description">Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten</string>
@ -580,6 +587,7 @@
<string name="onboarding_top_up_body_no_account_error">Um zu beginnen, lade die Wallet einfach mit mehr als %1$s %2$s auf.</string>
<string name="onboarding_top_up_button_but_crypto">Krypto kaufen</string>
<string name="onboarding_top_up_button_show_wallet_address">Adresse der Wallet anzeigen</string>
<string name="onboarding_top_up_min_create_account_amount">Um zu beginnen, lade die Wallet einfach mit mehr als %1$s auf.</string>
<string name="onboarding_topup_title">Aktivieren einer Wallet</string>
<string name="onboarding_twin_exit_warning">Der Twinning-Prozess ist teilweise abgeschlossen. Du kannst ihn jetzt nicht mehr beenden.</string>
<string name="onboarding_twins_interrupt_warning">Wenn der Prozess der Erstellung der Brieftasche in irgendeiner Weise unterbrochen wird, musst du diesen von vorne beginnen</string>
@ -760,6 +768,8 @@
<string name="send_summary_transaction_description_suffix_including">inklusive einer Netzgebühr von %1$s</string>
<string name="send_transaction_success">Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert</string>
<string name="send_tron_account_activation_error">%1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen.</string>
<string name="send_validation_destination_tag_required_description">Um diese Transaktion für die angegebene Adresse abzuschließen, ist ein Ziel-Tag (Memo) erforderlich.</string>
<string name="send_validation_destination_tag_required_title">Ziel-Tag erforderlich</string>
<string name="sent_transaction_sent_title">Transaktion gesendet</string>
<string name="settings_card_settings_footer">Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest.</string>
<string name="settings_forget_wallet">Entferne diese Wallet</string>
@ -857,6 +867,7 @@
<string name="staking_rewards">Belohnungen</string>
<string name="staking_stake_locked">Stake gesperrt</string>
<string name="staking_stake_more">Mehr staken</string>
<string name="staking_stake_more_button_unavailability_reason">Beim Staking %1$s wird Dein gesamtes %2$s -Guthaben eingesetzt. Alle weiteren %2$s due Du in Deine Tangem-Wallet einzahlst, werden ebenfalls automatisch eingesetzt.</string>
<string name="staking_staked_amount">Gestakeder Betrag</string>
<string name="staking_summary_description_text">Du setzt %1$s ein und ethältst Deine Belohnung %2$s</string>
<string name="staking_tap_to_unlock">Zum Entsperren antippen</string>
@ -931,7 +942,7 @@
<string name="token_button_unavailability_reason_custom_token">Die Aktion ist für benutzerdefinierte Token nicht verfügbar.</string>
<string name="token_button_unavailability_reason_empty_balance_sell">Du hast kein Guthaben um dies zu Verkaufen. Lade dein Konto auf, um Geld von dort zu verkaufen.</string>
<string name="token_button_unavailability_reason_empty_balance_send">Du hast kein Guthaben zum Versenden. Lade dein Konto auf, um Geld von dort aus senden zu können.</string>
<string name="token_button_unavailability_reason_loading">Die Daten wurden noch nicht geladen. Dies kann einige Sekunden dauern. Bitte versuche es später erneut.</string>
<string name="token_button_unavailability_reason_loading">Die Daten werden gerade geladen. Dies kann einige Sekunden dauern. Bitte versuche es später noch einmal.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Das Tauschen von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen.</string>
<string name="token_button_unavailability_reason_out_of_date_balance">Der angezeigte Kontostand kann aufgrund der Zwischenspeicherung veraltet sein.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Der Verkauf von Geldmitteln ist möglich, sobald die ausstehende(n) Transaktion(en) im %s -Netzwerk abgeschlossen ist/sind.</string>
@ -995,6 +1006,24 @@
<string name="user_wallet_list_rename_popup_title">Wallet umbenennen</string>
<string name="user_wallet_list_unlock_all">Alle freischalten</string>
<string name="user_wallet_list_unlock_all_with">Alle mit %s freischalten</string>
<plurals name="visa_limits_available_for_days_title">
<item quantity="one">Verfügbar für %d Tag</item>
<item quantity="other">Verfügbar für %d Tage</item>
</plurals>
<string name="visa_main_balances_and_limits">Guthaben und Limits</string>
<string name="visa_onboarding_approve_wallet_selector_notification_message">Bitte wähle die Wallet aus, mit der Du den Registrierungsprozess gestartet hast, um die Transaktion zur Erstellung Deines Kontos auf der Blockchain zu signieren</string>
<string name="visa_onboarding_close_alert_message">Möchtest Du den Vorgang wirklich beenden? Du kannst später dort fortfahren, wo Du aufgehört hast.</string>
<string name="visa_onboarding_in_progress_description">Das wird nicht lange dauern. Wir richten gerade Dein Konto ein.</string>
<string name="visa_onboarding_in_progress_issuer_description">Das wird nicht lange dauern. Wir schließen die Aktivierung ab.</string>
<string name="visa_onboarding_in_progress_title">Alles wird vorbereitet!</string>
<string name="visa_onboarding_pin_validation_error_message">Ungültige PIN: Sequenzen oder Wiederholungen vermeiden</string>
<string name="visa_onboarding_success_screen_description">Schon kann es losgehen!</string>
<string name="visa_onboarding_wallet_connect_description">Du kannst Deine Verbindung \n auf der Website des Drittanbieters \n herstellen und zurück zur Tangem-App gelangen</string>
<string name="visa_onboarding_wallet_connect_title">Zur Website gehen</string>
<string name="visa_onboarding_welcome_back_description">Fahren wir mit der Einrichtung Deines Kontos fort.</string>
<string name="visa_onboarding_welcome_back_title">Willkommen zurück!</string>
<string name="visa_onboarding_welcome_description">Folge den Schritten, um Dein Konto einzurichten.</string>
<string name="visa_onboarding_welcome_title">Willkommen!</string>
<string name="visa_unlock_notification_button">Freischalten</string>
<string name="visa_unlock_notification_subtitle">Scanne Deine Karte, um den Zugriff freizuschalten</string>
<string name="visa_unlock_notification_title">Benötigte Freischaltung</string>

View file

@ -578,6 +578,7 @@
<string name="onboarding_top_up_body_no_account_error">Para empezar, simplemente recargue la billetera con más de %1$s %2$s</string>
<string name="onboarding_top_up_button_but_crypto">Comprar cripto</string>
<string name="onboarding_top_up_button_show_wallet_address">Mostrar la dirección de la billetera</string>
<string name="onboarding_top_up_min_create_account_amount">Para empezar, simplemente recargue la billetera con más de %1$s</string>
<string name="onboarding_topup_title">Activar una billetera</string>
<string name="onboarding_twin_exit_warning">El proceso de emparejamiento está parcialmente completo. No puede salir ahora.</string>
<string name="onboarding_twins_interrupt_warning">Si el proceso de creación del wallet se interrumpe de alguna manera, tendrá que empezar de nuevo</string>

View file

@ -580,6 +580,7 @@
<string name="onboarding_top_up_body_no_account_error">Pour commencer, rechargez simplement le portefeuille avec plus de %1$s %2$s</string>
<string name="onboarding_top_up_button_but_crypto">Acheter des crypto-monnaies</string>
<string name="onboarding_top_up_button_show_wallet_address">Afficher l\'adresse du portefeuille</string>
<string name="onboarding_top_up_min_create_account_amount">Pour commencer, rechargez simplement le portefeuille avec plus de %1$s</string>
<string name="onboarding_topup_title">Activer un portefeuille</string>
<string name="onboarding_twin_exit_warning">Le processus de jumelage est partiellement terminé. Vous ne pouvez pas le quitter maintenant.</string>
<string name="onboarding_twins_interrupt_warning">Si le processus de création du portefeuille est interrompu de quelque manière que ce soit, vous devrez recommencer</string>
@ -1038,7 +1039,7 @@
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain va s\'arrêter de fonctionner</string>
<string name="warning_blocked_funds_for_fee_message">Veuillez déposer environ %1$s pour couvrir les frais de réseau</string>
<string name="warning_blocked_funds_for_fee_title">Fonds insuffisants pour couvrir les frais de réseau</string>
<string name="warning_button_could_be_better">Pas terrible</string>
<string name="warning_button_could_be_better">Peut mieux faire</string>
<string name="warning_button_like_it">J\'aime</string>
<string name="warning_button_ok">Ok, compris!</string>
<string name="warning_button_really_cool">Vraiment cool !</string>

View file

@ -136,6 +136,7 @@
<string name="common_fee_selector_option_market">マーケット</string>
<string name="common_fee_selector_option_slow">遅い</string>
<string name="common_fee_selector_title">速度と料金</string>
<string name="common_finish">終了</string>
<string name="common_generate_addresses">アドレスを同期する</string>
<string name="common_go_to_provider">プロバイダーへ移動</string>
<string name="common_go_to_token">トークンへ移動</string>
@ -155,6 +156,7 @@
<string name="common_no_address">アドレスがありません</string>
<string name="common_now"></string>
<string name="common_ok">わかりました</string>
<string name="common_open_in_browser">ブラウザで開く</string>
<string name="common_origin_card">プライマリーカード</string>
<string name="common_origin_ring">プライマリーリング</string>
<string name="common_passphrase">パスフレーズ</string>
@ -178,6 +180,7 @@
<string name="common_send">送る</string>
<string name="common_server_unavailable">サーバーが利用できません。しばらくしてからもう一度お試しください。</string>
<string name="common_share">共有</string>
<string name="common_share_link">リンクを共有</string>
<string name="common_sign">署名</string>
<string name="common_sign_and_send">署名して送信</string>
<string name="common_stake">ステーキング</string>
@ -489,6 +492,19 @@
<string name="markets_tooltip_message">これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します</string>
<string name="markets_tooltip_title">トークンを追加</string>
<string name="nfc_error_unavailable">お使いのデバイスではNFCが使用できません</string>
<plurals name="nft_collections_count">
<item quantity="other">アイテム</item>
</plurals>
<string name="nft_collections_empty_description">ウォレットに送信されたNFTがここに表示されます。</string>
<string name="nft_collections_empty_title">コレクションはまだありません</string>
<string name="nft_collections_receive">NFTを受け取る</string>
<string name="nft_collections_title">NFTコレクション</string>
<string name="nft_collections_warning_subtitle">一部のデータが読み込まれない場合があります</string>
<string name="nft_collections_warning_title">一時的な読み込みの問題</string>
<string name="nft_wallet_count">%1$dコレクションの%2$dNFT</string>
<string name="nft_wallet_receive_nft">ここをタップして最初のNFTを受け取ります</string>
<string name="nft_wallet_title">NFTコレクション</string>
<string name="nft_wallet_unable_to_load">データを読み込めません</string>
<string name="onboarding_access_code_feature_1_description">すべてのデバイスを保護するには、単一のアクセスコードを設定してください。</string>
<string name="onboarding_access_code_feature_1_title">保護する</string>
<string name="onboarding_access_code_feature_2_description">後で各カードおよびリングに個別のアクセスコードを設定できます。</string>
@ -572,6 +588,7 @@
<string name="onboarding_top_up_body_no_account_error">始めるには、ウォレットに%1$s %2$s以上入金するだけです</string>
<string name="onboarding_top_up_button_but_crypto">暗号資産を購入する</string>
<string name="onboarding_top_up_button_show_wallet_address">ウォレットのアドレスを表示する</string>
<string name="onboarding_top_up_min_create_account_amount">始めるには、ウォレットに%1$s 以上入金するだけです</string>
<string name="onboarding_topup_title">ウォレットを有効化する</string>
<string name="onboarding_twin_exit_warning">ツイン化プロセスは部分的に完了しています。現在終了することはできません。</string>
<string name="onboarding_twins_interrupt_warning">ウォレットの作成プロセスが何らかの理由で中断された場合は、最初からやり直す必要があります。</string>
@ -750,6 +767,8 @@
<string name="send_summary_transaction_description_suffix_including">ネットワーク手数料%1$sを含む</string>
<string name="send_transaction_success">取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。</string>
<string name="send_tron_account_activation_error">%1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTronTRXを入金する必要があります。</string>
<string name="send_validation_destination_tag_required_description">指定されたアドレスへの取引を完了するには、宛先タグ(メモ)が必要です。</string>
<string name="send_validation_destination_tag_required_title">宛先タグが必要です</string>
<string name="sent_transaction_sent_title">取引が送信されました</string>
<string name="settings_card_settings_footer">設定したいカードまたはリングをスキャンするために準備してください。</string>
<string name="settings_forget_wallet">ウォレット削除</string>
@ -847,6 +866,7 @@
<string name="staking_rewards">報酬</string>
<string name="staking_stake_locked">ステーキングはロックされています</string>
<string name="staking_stake_more">もっとステーキングする</string>
<string name="staking_stake_more_button_unavailability_reason">%1$sをステーキングすると、 %2$s残高全体がステーキングされます。Tangemウォレットに入金した追加の%2$sも、自動的にステーキングされます。</string>
<string name="staking_staked_amount">ステーキング中の金額</string>
<string name="staking_summary_description_text">%1$sをステーキングし、報酬%2$sを受け取ります</string>
<string name="staking_tap_to_unlock">タップしてロック解除</string>
@ -985,6 +1005,20 @@
<string name="user_wallet_list_rename_popup_title">ウォレット名の変更</string>
<string name="user_wallet_list_unlock_all">すべてのロックを解除</string>
<string name="user_wallet_list_unlock_all_with">%sですべてをロック解除</string>
<plurals name="visa_limits_available_for_days_title">
<item quantity="other">%d日間利用可能</item>
</plurals>
<string name="visa_main_balances_and_limits">残高と限度額</string>
<string name="visa_onboarding_close_alert_message">本当に終了してもよろしいですか?中断したところから後で続行できます。</string>
<string name="visa_onboarding_in_progress_description">長くはかかりません。アカウントを設定しています。</string>
<string name="visa_onboarding_in_progress_issuer_description">長くはかかりません。アクティベーションを完了しています。</string>
<string name="visa_onboarding_in_progress_title">準備完了です!</string>
<string name="visa_onboarding_pin_validation_error_message">無効な暗証番号:連続や繰り返しを避けてください</string>
<string name="visa_onboarding_wallet_connect_title">ウェブサイトに移動</string>
<string name="visa_onboarding_welcome_back_description">アカウントの設定を続けましょう。</string>
<string name="visa_onboarding_welcome_back_title">お帰りなさい!</string>
<string name="visa_onboarding_welcome_description">手順に従ってアカウントを設定してください。</string>
<string name="visa_onboarding_welcome_title">ようこそ!</string>
<string name="visa_unlock_notification_button">ロック解除</string>
<string name="visa_unlock_notification_subtitle">カードをスキャンしてアクセスロックを解除する</string>
<string name="visa_unlock_notification_title">ロック解除が必要</string>

View file

@ -594,6 +594,7 @@
<string name="onboarding_top_up_body_no_account_error">Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой</string>
<string name="onboarding_top_up_button_but_crypto">Купить криптовалюту</string>
<string name="onboarding_top_up_button_show_wallet_address">Показать адрес кошелька</string>
<string name="onboarding_top_up_min_create_account_amount">Пополните кошелек более чем на %1$s, чтобы начать пользоваться картой</string>
<string name="onboarding_topup_title">Активация кошелька</string>
<string name="onboarding_twin_exit_warning">Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас.</string>
<string name="onboarding_twins_interrupt_warning">Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала</string>

View file

@ -594,6 +594,7 @@
<string name="onboarding_top_up_body_no_account_error">Щоб почати, просто поповніть гаманець більше, ніж на %1$s %2$s</string>
<string name="onboarding_top_up_button_but_crypto">Купити криптовалюту</string>
<string name="onboarding_top_up_button_show_wallet_address">Показати адресу гаманця</string>
<string name="onboarding_top_up_min_create_account_amount">Щоб почати, просто поповніть гаманець більше, ніж на %1$s</string>
<string name="onboarding_topup_title">Активація гаманця</string>
<string name="onboarding_twin_exit_warning">Процес поєднання карток частково завершено. Ви не можете вийти з нього зараз.</string>
<string name="onboarding_twins_interrupt_warning">Якщо процес створення гаманця буде перервано будь-яким чином, вам доведеться почати все спочатку</string>

View file

@ -205,6 +205,7 @@
<string name="onboarding_top_up_body_no_account_error">要開始,只需為錢包充值超過 %1$s %2$s</string>
<string name="onboarding_top_up_button_but_crypto">購買加密貨幣</string>
<string name="onboarding_top_up_button_show_wallet_address">顯示錢包地址</string>
<string name="onboarding_top_up_min_create_account_amount">要開始,只需為錢包充值超過 %1$s</string>
<string name="onboarding_topup_title">啟動錢包</string>
<string name="onboarding_twin_exit_warning">結對過程已部分完成。你現在不能退出</string>
<string name="onboarding_twins_interrupt_warning">如果創建錢包的過程以任何方式中斷,您將得重新開始</string>

View file

@ -126,6 +126,7 @@
</plurals>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_disconnect">Disconnect</string>
<string name="common_done">Done</string>
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
@ -139,6 +140,7 @@
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_slow">Slow</string>
<string name="common_fee_selector_title">Speed and fee</string>
<string name="common_finish">Finish</string>
<string name="common_generate_addresses">Synchronize addresses</string>
<string name="common_go_to_provider">Go to provider</string>
<string name="common_go_to_token">Go to token</string>
@ -158,6 +160,7 @@
<string name="common_no_address">No address</string>
<string name="common_now">Now</string>
<string name="common_ok">OK</string>
<string name="common_open_in_browser">Open in Browser</string>
<string name="common_origin_card">Primary card</string>
<string name="common_origin_ring">Primary ring</string>
<string name="common_passphrase">Passphrase</string>
@ -181,6 +184,7 @@
<string name="common_send">Send</string>
<string name="common_server_unavailable">The server is not available, please try again later</string>
<string name="common_share">Share</string>
<string name="common_share_link">Share Link</string>
<string name="common_sign">Sign</string>
<string name="common_sign_and_send">Sign and send</string>
<string name="common_stake">Stake</string>
@ -190,6 +194,7 @@
<string name="common_success">Success</string>
<string name="common_support">Support</string>
<string name="common_swap">Swap</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_terms_and_conditions">terms and conditions</string>
<string name="common_terms_of_use">Terms of Use</string>
<string name="common_today">Today</string>
@ -495,9 +500,19 @@
<string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string>
<string name="markets_tooltip_title">Add tokens</string>
<string name="nfc_error_unavailable">NFC is not available on your device</string>
<string name="nft_wallet_title">NFT collections</string>
<string name="nft_wallet_receive_nft">Tap here to receive first NFT</string>
<plurals name="nft_collections_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
<string name="nft_collections_empty_description">NFTs sent to your wallet address will show up here.</string>
<string name="nft_collections_empty_title">No collections, yet</string>
<string name="nft_collections_receive">Receive NFT</string>
<string name="nft_collections_title">NFT collections</string>
<string name="nft_collections_warning_subtitle">Some data may not load</string>
<string name="nft_collections_warning_title">Temporary loading problems</string>
<string name="nft_wallet_count">%1$d NFTs in %2$d collection</string>
<string name="nft_wallet_receive_nft">Tap here to receive first NFT</string>
<string name="nft_wallet_title">NFT collections</string>
<string name="nft_wallet_unable_to_load">Unable to load the data</string>
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
@ -584,6 +599,7 @@
<string name="onboarding_top_up_body_no_account_error">To get started, simply top up the wallet with more than %1$s %2$s</string>
<string name="onboarding_top_up_button_but_crypto">Buy crypto</string>
<string name="onboarding_top_up_button_show_wallet_address">Show the wallet\'s address</string>
<string name="onboarding_top_up_min_create_account_amount">To get started, simply top up the wallet with more than %1$s</string>
<string name="onboarding_topup_title">Activate a wallet</string>
<string name="onboarding_twin_exit_warning">The twinning process is partly complete. You can\'t exit it now.</string>
<string name="onboarding_twins_interrupt_warning">If the process of creating the wallet gets interrupted in any way, you\'ll have to start over</string>
@ -764,6 +780,8 @@
<string name="send_summary_transaction_description_suffix_including">including a network fee of %1$s</string>
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
<string name="send_tron_account_activation_error">%1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account.</string>
<string name="send_validation_destination_tag_required_description">A destination tag (memo) is required to complete this transaction for the specified address.</string>
<string name="send_validation_destination_tag_required_title">Destination Tag Required</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card or ring you want to set up.</string>
<string name="settings_forget_wallet">Forget wallet</string>
@ -1000,6 +1018,69 @@
<string name="user_wallet_list_rename_popup_title">Rename wallet</string>
<string name="user_wallet_list_unlock_all">Unlock all</string>
<string name="user_wallet_list_unlock_all_with">Unlock all with %s</string>
<string name="visa_balance_limits_details_aml_verified">AML verifired</string>
<string name="visa_balance_limits_details_available">Available</string>
<string name="visa_balance_limits_details_blocked">Blocked</string>
<string name="visa_balance_limits_details_debt">Debt</string>
<string name="visa_balance_limits_details_limits">Limits</string>
<string name="visa_balance_limits_details_no_otp_limit">Other (no-otp)</string>
<string name="visa_balance_limits_details_single_transaction">Single transaction</string>
<string name="visa_balance_limits_details_total">Total</string>
<plurals name="visa_limits_available_for_days_title">
<item quantity="one">available for %d day</item>
<item quantity="other">available for %d days</item>
</plurals>
<string name="visa_main_available_balance_alert_message">Available balance is actual funds available, considering pending transactions, blocked amounts, and debit balance to prevent overdrafts.</string>
<string name="visa_main_available_till_date">Available till %1$s</string>
<string name="visa_main_balances_and_limits">Balances &amp; Limits</string>
<string name="visa_main_button_alert_missing_payment_account_info_message">Missing Payment account information. Please contact support</string>
<string name="visa_main_button_alert_missing_token_info">Unable to load information about token</string>
<string name="visa_main_button_alert_still_loading">Information is still loading, please wait</string>
<string name="visa_main_limits_alert_description_message">Limits are needed to control costs, improve security, manage risk. You can spend %1$s during the period for card payments in shops and %2$s for other transactions, e.g. subscriptions or debts.</string>
<string name="visa_onboarding_access_code_description">The access code will be used manage your payment account and protect it from unauthorized access</string>
<string name="visa_onboarding_access_code_navigation_title">Access Code</string>
<string name="visa_onboarding_account_activation_navigation_title">Account activation</string>
<string name="visa_onboarding_approve_wallet_selector_notification_message">Please choose the wallet you started the registration process with to sign the transaction for creating your account on the Blockchain</string>
<string name="visa_onboarding_cancel_activation">Cancel Activation</string>
<string name="visa_onboarding_close_alert_message">Are you sure you want to exit? You can continue later from where you left off.</string>
<string name="visa_onboarding_in_progress_description">This won\'t take long. We\'re setting up your account.</string>
<string name="visa_onboarding_in_progress_issuer_description">This wont take long. Were completing the activation.</string>
<string name="visa_onboarding_in_progress_title">Getting everything ready!</string>
<string name="visa_onboarding_other_wallet">Other wallet</string>
<string name="visa_onboarding_pin_code_description">Set up a 4-digit code. It will be used for payments.</string>
<string name="visa_onboarding_pin_code_navigation_title">PIN code</string>
<string name="visa_onboarding_pin_code_title">Create PIN Code</string>
<string name="visa_onboarding_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</string>
<string name="visa_onboarding_success_screen_description">You\'re good to go!</string>
<string name="visa_onboarding_tangem_approve_description">Prepare the Tangem card and tap to approve</string>
<string name="visa_onboarding_tangem_approve_title">Prepare Tangem Wallet</string>
<string name="visa_onboarding_wallet_connect_description">You will be able to complete your connection \n on the third-party web-site \n and back to the Tangem app</string>
<string name="visa_onboarding_wallet_connect_title">Go to Website</string>
<string name="visa_onboarding_wallet_connection_navigation_title">Wallet connection</string>
<string name="visa_onboarding_wallet_list_header">Choose wallet</string>
<string name="visa_onboarding_welcome_back_button_title">Continue Activation</string>
<string name="visa_onboarding_welcome_back_description">Let\'s continue setting up your account.</string>
<string name="visa_onboarding_welcome_back_title">Welcome back!</string>
<string name="visa_onboarding_welcome_button_title">Start Activation</string>
<string name="visa_onboarding_welcome_description">Follow the steps to set up your account.</string>
<string name="visa_onboarding_welcome_title">Welcome!</string>
<string name="visa_transaction_details_blockchain_amount">Blockchain amount</string>
<string name="visa_transaction_details_currency_code">Currency code</string>
<string name="visa_transaction_details_date">Date</string>
<string name="visa_transaction_details_error_code">Error code</string>
<string name="visa_transaction_details_header">Transaction details</string>
<string name="visa_transaction_details_merchant_category_code">Merchant category code</string>
<string name="visa_transaction_details_merchant_city">Merchant city</string>
<string name="visa_transaction_details_merchant_country_code">Merchant country code</string>
<string name="visa_transaction_details_merchant_name">Merchant name</string>
<string name="visa_transaction_details_request_id">Request ID</string>
<string name="visa_transaction_details_status">Status</string>
<string name="visa_transaction_details_title">Transaction</string>
<string name="visa_transaction_details_transaction_amount">Transaction amount</string>
<string name="visa_transaction_details_transaction_hash">Transaction hash</string>
<string name="visa_transaction_details_transaction_request">Transaction request</string>
<string name="visa_transaction_details_transaction_status">Transaction status</string>
<string name="visa_transaction_details_type">Type</string>
<string name="visa_unlock_notification_button">Unlock</string>
<string name="visa_unlock_notification_subtitle">Scan your card to unlock access</string>
<string name="visa_unlock_notification_title">Needed unlock</string>
@ -1133,6 +1214,13 @@
<string name="warning_testnet_card_message">This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.</string>
<string name="warning_testnet_card_title">For testing purposes only</string>
<string name="warning_token_balance_not_updated">Balance may be outdated. Refresh the page.</string>
<string name="wc_connections">Connections</string>
<string name="wc_disconnect_all">Disconnect all</string>
<string name="wc_disconnect_all_alert_desc">Text about discnected all dApps</string>
<string name="wc_disconnect_all_alert_title">Disconect All dApps</string>
<string name="wc_new_connection">New connection</string>
<string name="wc_no_sessions_desc">Connect your wallet to a different dApps</string>
<string name="wc_no_sessions_title">No sessions</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>
<string name="welcome_interrupted_backup_alert_resume">Yes, resume</string>

View file

@ -0,0 +1,47 @@
package com.tangem.core.ui.components.dropdownmenu
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Suppress("ComposableEventParameterNaming")
@Composable
fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit, modifier: Modifier = Modifier) {
Text(
modifier = modifier
.clickable {
dismissParent()
item.onClick()
}
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16),
text = item.title.resolveReference(),
style = TangemTheme.typography.button.copy(color = item.textColorProvider()),
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenDetailsAppBarDropdownItem() {
TangemThemePreview {
TangemDropdownItem(
modifier = Modifier.background(TangemTheme.colors.background.primary),
dismissParent = {},
item = TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
)
}
}

View file

@ -1,11 +1,17 @@
@file:Suppress("TopLevelPropertyNaming")
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
package com.tangem.core.ui.components.dropdownmenu
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
@ -15,16 +21,19 @@ import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.tangem.core.ui.res.TangemTheme
import kotlin.math.max
import kotlin.math.min
/**
* Just copy paste [DropdownMenu] from material3 with deleting vertical paddings.
*/
@Composable
internal fun TangemDropdownMenu(
fun TangemDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
offset: DpOffset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
@ -142,10 +151,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect)
menuBounds.width == 0 -> 0f
else -> {
val intersectionCenter =
(
kotlin.math.max(parentBounds.left, menuBounds.left) +
kotlin.math.min(parentBounds.right, menuBounds.right)
) / 2
(max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2
(intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
}
}
@ -155,10 +161,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect)
menuBounds.height == 0 -> 0f
else -> {
val intersectionCenter =
(
kotlin.math.max(parentBounds.top, menuBounds.top) +
kotlin.math.min(parentBounds.bottom, menuBounds.bottom)
) / 2
(max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2
(intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.core.ui.components.dropdownmenu
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
data class TangemDropdownMenuItem(
val title: TextReference,
val textColorProvider: @Composable () -> Color,
val onClick: () -> Unit,
)

View file

@ -32,7 +32,7 @@ fun Modifier.roundedShapeItemDecoration(
val applyShape: Modifier.(shape: RoundedCornerShape?) -> Modifier = { shape ->
if (backgroundColor != null) {
if (shape != null) {
background(color = backgroundColor, shape = shape)
clip(shape).background(color = backgroundColor, shape = shape)
} else {
background(color = backgroundColor)
}

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="76dp"
android:height="76dp"
android:viewportWidth="76"
android:viewportHeight="76">
<path
android:fillColor="#3B99FC"
android:pathData="M16,0L60,0A16,16 0,0 1,76 16L76,60A16,16 0,0 1,60 76L16,76A16,16 0,0 1,0 60L0,16A16,16 0,0 1,16 0z" />
<group>
<clip-path android:pathData="M11.084,22.167h53.833v31.667h-53.833z" />
<path
android:fillColor="#ffffff"
android:pathData="M22.112,28.351C30.889,20.101 45.126,20.101 53.903,28.351L54.959,29.337C55.403,29.751 55.403,30.417 54.959,30.83L51.348,34.229C51.126,34.429 50.778,34.429 50.556,34.229L49.098,32.856C42.973,27.098 33.042,27.098 26.917,32.856L25.362,34.322C25.139,34.522 24.792,34.522 24.57,34.322L20.945,30.937C20.501,30.524 20.501,29.857 20.945,29.444L22.112,28.351ZM61.376,35.375L64.598,38.4C65.042,38.813 65.042,39.48 64.598,39.893L50.084,53.527C49.639,53.94 48.931,53.94 48.501,53.527L38.209,43.851C38.098,43.745 37.917,43.745 37.806,43.851L27.514,53.527C27.07,53.94 26.362,53.94 25.931,53.527L11.417,39.893C10.973,39.48 10.973,38.813 11.417,38.4L14.639,35.375C15.084,34.962 15.792,34.962 16.223,35.375L26.514,45.051C26.626,45.158 26.806,45.158 26.917,45.051L37.209,35.375C37.653,34.962 38.362,34.962 38.792,35.375L49.084,45.051C49.195,45.158 49.376,45.158 49.487,45.051L59.778,35.375C60.223,34.962 60.931,34.962 61.376,35.375Z" />
</group>
</vector>

View file

@ -1,8 +1,15 @@
package com.tangem.utils.transformer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
/**
* Transforms state to updated state.
*/
interface Transformer<S> {
fun transform(prevState: S): S
}
fun <T> MutableStateFlow<T>.update(transformer: Transformer<T>) {
update { transformer.transform(this.value) }
}

View file

@ -94,6 +94,8 @@ internal class DefaultFeedbackRepository(
override fun getLogFile(): File? = appLogsStore.getFile()
override suspend fun getZipLogFile(): File? = appLogsStore.getZipFile()
override fun sendEmail(feedbackEmail: FeedbackEmail) {
emailSender.send(
EmailSender.Email(

View file

@ -335,7 +335,7 @@ internal class DefaultMarketsTokenRepository(
): T {
return when (error) {
is ApiResponseError.HttpException -> {
createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.code, error.message.orEmpty())
createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.numericCode, error.message.orEmpty())
}
is ApiResponseError.TimeoutException -> {
createEvent(MarketsDataAnalyticsEvent.Type.Timeout, null, error.message.orEmpty())

View file

@ -28,9 +28,9 @@ internal object TokenMarketListConverter : Converter<TokenMarketListResponse, Li
imageHost = imageHost,
tokenQuotesShort = TokenQuotesShort(
currentPrice = token.currentPrice,
h24ChangePercent = token.priceChangePercentage.h24.movePointLeft(2),
weekChangePercent = token.priceChangePercentage.week1.movePointLeft(2),
monthChangePercent = token.priceChangePercentage.day30.movePointLeft(2),
h24ChangePercent = token.priceChangePercentage?.h24?.movePointLeft(2),
weekChangePercent = token.priceChangePercentage?.week1?.movePointLeft(2),
monthChangePercent = token.priceChangePercentage?.day30?.movePointLeft(2),
),
tokenCharts = TokenMarket.Charts(h24 = null, week = null, month = null),
)

View file

@ -1,14 +1,14 @@
package com.tangem.data.nft
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.datasource.local.nft.NFTPersistenceStore
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
import com.tangem.datasource.local.nft.NFTRuntimeStore
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
@ -20,8 +20,10 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
@ -34,82 +36,82 @@ internal class DefaultNFTRepository @Inject constructor(
private val jobs = mutableMapOf<Network, JobHolder>()
private val nftRuntimeStores = mutableMapOf<Network, NFTRuntimeStore>()
private val nftPersistenceStores = mutableMapOf<Network, NFTPersistenceStore>()
private val nftSdkAssetConverter = NFTSdkAssetConverter(
nftSdkAssetIdentifierConverter = NFTSdkAssetIdentifierConverter,
nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter,
)
private val collectionConverter = NFTSdkCollectionConverter(
nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter,
nftSdkAssetConverter = nftSdkAssetConverter,
)
private val nftRuntimeStores = mutableMapOf<String, NFTRuntimeStore>()
private val nftPersistenceStores = mutableMapOf<String, NFTPersistenceStore>()
override fun observeCollections(userWalletId: UserWalletId, networks: List<Network>): Flow<List<NFTCollections>> =
flow {
emitAll(
combine(
networks.map { getNFTRuntimeStore(it).getCollections() },
) { it.asList() },
)
}.onStart {
refreshCollections(userWalletId, networks)
}
flow { emitAll(observeCollectionsInternal(userWalletId, networks)) }
private suspend fun observeCollectionsInternal(
userWalletId: UserWalletId,
networks: List<Network>,
): Flow<List<NFTCollections>> = combine(
networks.map { observeCollectionsInternal(userWalletId, it) },
) { it.asList() }
private suspend fun observeCollectionsInternal(
userWalletId: UserWalletId,
network: Network,
): Flow<NFTCollections> = if (network.canHandleNFTs()) {
getNFTRuntimeStore(userWalletId, network).getCollections()
} else {
flowOf(NFTCollections.empty(network))
}
override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List<Network>) = coroutineScope {
networks.map { network ->
launch(dispatchers.io) {
Either.catch {
expireCollections(network)
walletManagersFacade.getNFTCollections(userWalletId, network)
}.onLeft {
saveFailedStateInRuntime(
network = network,
error = it,
)
}.onRight {
val mergedCollections = it.mergeWithStoredAssets(network)
networks.mapNotNull { network ->
if (network.canHandleNFTs()) {
launch(dispatchers.io) {
Either.catch {
expireCollections(userWalletId, network)
walletManagersFacade.getNFTCollections(userWalletId, network)
}.onLeft {
saveFailedStateInRuntime(
userWalletId = userWalletId,
network = network,
error = it,
)
}.onRight {
val mergedCollections = it.mergeWithStoredAssets(userWalletId, network)
saveCollectionsInRuntime(
network = network,
collections = mergedCollections,
)
saveCollectionsInPersistence(
network = network,
collections = mergedCollections,
)
}
}.saveIn(getJobHolder(network))
saveCollectionsInRuntime(
userWalletId = userWalletId,
network = network,
collections = mergedCollections,
)
saveCollectionsInPersistence(
userWalletId = userWalletId,
network = network,
collections = mergedCollections,
)
}
}.saveIn(getJobHolder(network))
} else {
null
}
}.joinAll()
}
private suspend fun expireCollections(network: Network) {
val runtimeStore = getNFTRuntimeStore(network)
private suspend fun expireCollections(userWalletId: UserWalletId, network: Network) {
val runtimeStore = getNFTRuntimeStore(userWalletId, network)
val expiredCollections = runtimeStore
.getCollectionsSync()
.let { collections ->
collections.copy(
content = when (val content = collections.content) {
is NFTCollections.Content.Collections -> content.copy(
source = StatusSource.CACHE,
)
is NFTCollections.Content.Error -> content
},
)
}
.changeStatusSource(StatusSource.CACHE)
runtimeStore.saveCollections(expiredCollections)
}
private suspend fun saveCollectionsInRuntime(network: Network, collections: List<SdkNFTCollection>) {
getNFTRuntimeStore(network).saveCollections(
private suspend fun saveCollectionsInRuntime(
userWalletId: UserWalletId,
network: Network,
collections: List<SdkNFTCollection>,
) {
getNFTRuntimeStore(userWalletId, network).saveCollections(
NFTCollections(
network = network,
content = NFTCollections.Content.Collections(
collections = collections
.map { collection ->
collectionConverter.convert(network to collection)
NFTSdkCollectionConverter.convert(network to collection)
}
.filter {
it.id !is NFTCollection.Identifier.Unknown
@ -120,19 +122,30 @@ internal class DefaultNFTRepository @Inject constructor(
)
}
private suspend fun saveFailedStateInRuntime(network: Network, error: Throwable) {
getNFTRuntimeStore(network).saveCollections(
NFTCollections(
network = network,
content = NFTCollections.Content.Error(
error = error,
),
),
)
private suspend fun saveFailedStateInRuntime(userWalletId: UserWalletId, network: Network, error: Throwable) {
getNFTRuntimeStore(userWalletId, network).let { store ->
val storedCollections = store.getCollectionsSync()
val content = storedCollections.content
val updatedCollections = if (content is NFTCollections.Content.Collections && content.collections != null) {
// if there is any cached collections in store, then mark them as not actual and emit anyway
storedCollections.changeStatusSource(StatusSource.ONLY_CACHE)
} else {
// otherwise, just emit an error
NFTCollections(
network = network,
content = NFTCollections.Content.Error(error),
)
}
store.saveCollections(updatedCollections)
}
}
private suspend fun saveCollectionsInPersistence(network: Network, collections: List<SdkNFTCollection>) {
getNFTPersistenceStore(network).saveCollections(collections)
private suspend fun saveCollectionsInPersistence(
userWalletId: UserWalletId,
network: Network,
collections: List<SdkNFTCollection>,
) {
getNFTPersistenceStore(userWalletId, network).saveCollections(collections)
}
private fun getJobHolder(network: Network): JobHolder = jobs[network] ?: run {
@ -141,65 +154,85 @@ internal class DefaultNFTRepository @Inject constructor(
}
}
private fun getNFTPersistenceStore(network: Network): NFTPersistenceStore = nftPersistenceStores[network] ?: run {
nftPersistenceStoreFactory.provide(network).also {
nftPersistenceStores[network] = it
private fun getNFTPersistenceStore(userWalletId: UserWalletId, network: Network): NFTPersistenceStore {
val storeId = (userWalletId to network).formatted()
return nftPersistenceStores[storeId] ?: run {
nftPersistenceStoreFactory.provide(userWalletId, network).also {
nftPersistenceStores[storeId] = it
}
}
}
private suspend fun getNFTRuntimeStore(network: Network): NFTRuntimeStore = nftRuntimeStores[network] ?: run {
nftRuntimeStoreFactory.provide(network).also {
nftRuntimeStores[network] = it
val storedCollections = getStoredCollections(network)
val storedPrices = getStoredPrices(network)
it.initialize(
collections = storedCollections,
prices = storedPrices,
)
private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore {
val storeId = (userWalletId to network).formatted()
return nftRuntimeStores[storeId] ?: run {
nftRuntimeStoreFactory.provide(network).also {
nftRuntimeStores[storeId] = it
val storedCollections = getStoredCollections(userWalletId, network)
val storedPrices = getStoredPrices(userWalletId, network)
it.initialize(
collections = storedCollections,
prices = storedPrices,
)
}
}
}
private suspend fun getStoredCollections(network: Network) = getNFTPersistenceStore(network)
.getCollectionsSync()
.let {
NFTCollections(
network = network,
content = NFTCollections.Content.Collections(
collections = it
?.map { collection ->
collectionConverter.convert(network to collection)
}
?.filter {
it.id !is NFTCollection.Identifier.Unknown
},
source = StatusSource.CACHE,
),
)
}
private suspend fun getStoredPrices(network: Network) = getNFTPersistenceStore(network)
.getSalePricesSync()
.orEmpty()
.let { prices ->
prices
.mapKeys {
val (assetId, _) = it
NFTSdkAssetIdentifierConverter.convert(assetId)
}
.mapValues {
val (assetId, price) = it
NFTSalePrice.Value(
assetId = assetId,
value = price.value,
symbol = price.symbol,
private suspend fun getStoredCollections(userWalletId: UserWalletId, network: Network) =
getNFTPersistenceStore(userWalletId, network)
.getCollectionsSync()
.let {
NFTCollections(
network = network,
content = NFTCollections.Content.Collections(
collections = it
?.map { collection ->
NFTSdkCollectionConverter.convert(network to collection)
}
?.filter {
it.id !is NFTCollection.Identifier.Unknown
},
source = StatusSource.CACHE,
)
}
}
),
)
}
private suspend fun List<SdkNFTCollection>.mergeWithStoredAssets(network: Network): List<SdkNFTCollection> {
private suspend fun getStoredPrices(userWalletId: UserWalletId, network: Network) =
getNFTPersistenceStore(userWalletId, network)
.getSalePricesSync()
.orEmpty()
.let { prices ->
prices
.mapKeys {
val (assetId, _) = it
NFTSdkAssetIdentifierConverter.convert(assetId)
}
.mapValues {
val (assetId, price) = it
NFTSalePrice.Value(
assetId = assetId,
value = price.value,
symbol = price.symbol,
source = StatusSource.CACHE,
)
}
}
private fun NFTCollections.changeStatusSource(source: StatusSource) = copy(
content = when (val content = content) {
is NFTCollections.Content.Collections -> content.copy(
source = source,
)
is NFTCollections.Content.Error -> content
},
)
private suspend fun List<SdkNFTCollection>.mergeWithStoredAssets(
userWalletId: UserWalletId,
network: Network,
): List<SdkNFTCollection> {
val storedCollections =
getNFTPersistenceStore(network)
getNFTPersistenceStore(userWalletId, network)
.getCollectionsSync()
.orEmpty()
.associateBy(SdkNFTCollection::identifier)
@ -224,4 +257,11 @@ internal class DefaultNFTRepository @Inject constructor(
}
}
}
private fun Pair<UserWalletId, Network>.formatted(): String {
val (walletId, network) = this
return walletId.stringValue + "_" + network.id.value + "_" + network.derivationPath.value
}
private fun Network.canHandleNFTs(): Boolean = Blockchain.fromNetworkId(backendId)?.canHandleNFTs() == true
}

View file

@ -131,7 +131,7 @@ internal class DefaultHotCryptoRepository(
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.HotTokenError(
errorCode = (it as? ApiResponseError.HttpException)?.code?.code?.toString().orEmpty(),
errorCode = (it as? ApiResponseError.HttpException)?.code?.numericCode?.toString().orEmpty(),
),
)
}

View file

@ -8,6 +8,8 @@ import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
import com.tangem.data.onramp.legacy.MercuryoTopUpRepository
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
@ -15,6 +17,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
@ -112,4 +115,16 @@ internal object OnrampDataModule {
analyticsEventHandler = analyticsEventHandler,
)
}
@Provides
@Singleton
fun provideMercuryoRepository(
environmentConfigStorage: EnvironmentConfigStorage,
dispatchersProvider: CoroutineDispatcherProvider,
): LegacyTopUpRepository {
return MercuryoTopUpRepository(
environmentConfigStorage = environmentConfigStorage,
dispatchersProvider = dispatchersProvider,
)
}
}

View file

@ -1,12 +1,13 @@
package com.tangem.tap.network.exchangeServices.mercuryo
package com.tangem.data.onramp.legacy
import com.tangem.blockchain.common.Blockchain
/**
* Legacy onboarding note/twins to get top up URL from mercuryo
* Map [Blockchain] to ids from [link](https://api.mercuryo.io/v1.6/lib/currencies) [crypto_currencies]
*/
@Suppress("CyclomaticComplexMethod")
internal val Blockchain.mercuryoNetwork: String?
public val Blockchain.mercuryoNetwork: String?
get() {
return when (this) {
Blockchain.Algorand -> "ALGORAND"

Some files were not shown because too many files have changed in this diff Show more