Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-28 09:36:24 +03:00
commit 4b78bf0d94
429 changed files with 10253 additions and 1865 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

@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
class BlockchainExceptionEvent(
class BlockchainApiExceptionEvent(
selectedHost: String,
exceptionHost: String,
error: String,

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

@ -0,0 +1,18 @@
package com.tangem.tap.common.analytics.events
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
class TangemSdkErrorEvent(
val exception: TangemSdkError,
) : AnalyticsEvent(
category = "TangemSdk",
event = "Tangem Sdk Error",
params = errorConverter.convert(exception),
) {
companion object {
private val errorConverter = AnalyticsErrorConverter()
}
}

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,16 +1,16 @@
package com.tangem.tap.common.analytics.handlers
import com.tangem.blockchain.common.ExceptionHandlerOutput
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.events.BlockchainExceptionEvent
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent
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(
BlockchainExceptionEvent(
analyticsErrorHandler.sendErrorEvent(
BlockchainApiExceptionEvent(
selectedHost = nextHost,
exceptionHost = currentHost,
error = message,

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,11 @@ 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)
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

@ -5,10 +5,11 @@ import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -209,7 +210,6 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository,
promoRepository: PromoRepository,
swapFeatureToggles: SwapFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
currencyStatusOperations: BaseCurrencyStatusOperations,
): GetCryptoCurrencyActionsUseCase {
@ -219,7 +219,6 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository,
promoRepository = promoRepository,
swapFeatureToggles = swapFeatureToggles,
dispatchers = dispatchers,
currencyStatusOperations = currencyStatusOperations,
)
@ -351,21 +350,13 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): BaseCurrenciesStatusesOperations {
return if (tokensFeatureToggles.isBalancesCachingEnabled) {
CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
} else {
LceCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@Provides
@ -377,20 +368,12 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): BaseCurrencyStatusOperations {
return if (tokensFeatureToggles.isBalancesCachingEnabled) {
CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
} else {
CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
tokensFeatureToggles = tokensFeatureToggles,
)
}
}

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

@ -235,13 +235,25 @@ internal class LegacyScanProcessor @Inject constructor(
} else {
Analytics.setContext(scanResponse)
val isTwinRefactoringEnabled =
store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
if (isTwinRefactoringEnabled) {
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
) { onProgressStateChange(it) }
} else {
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
}
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)

View file

@ -7,9 +7,11 @@ 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
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
@ -34,7 +36,7 @@ internal object UseCaseScanProcessor {
ifLeft = {
val error = scanCardExceptionConverter.convertBack(it)
Analytics.send(Basic.ScanError(error))
Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
CompletionResult.Failure(error)
},
ifRight = { CompletionResult.Success(it) },
@ -97,7 +99,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

@ -59,15 +59,27 @@ class CheckForOnboardingChain(
else -> {
Analytics.setContext(previousChainResult)
val isTwinRefactoringEnabled =
store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
// If twins was twinned previously but twins welcome not shown
if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)
ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left()
if (isTwinRefactoringEnabled) {
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
).left()
} else {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)
ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left()
}
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()

View file

@ -12,7 +12,7 @@ internal class ScanCardExceptionConverter : TwoWayConverter<TangemError, ScanCar
else -> ScanCardException.UnknownException(value)
}
override fun convertBack(value: ScanCardException): TangemError {
override fun convertBack(value: ScanCardException): TangemSdkError {
return when (value) {
is ScanCardException.UnknownException -> TangemSdkError.ExceptionError(value)
is ScanCardException.UserCancelled -> TangemSdkError.UserCancelled()
@ -31,7 +31,7 @@ internal class ScanCardExceptionConverter : TwoWayConverter<TangemError, ScanCar
else -> ScanCardException.UnknownException(value)
}
private fun concertScanChainException(value: ScanCardException.ChainException): TangemError {
private fun concertScanChainException(value: ScanCardException.ChainException): TangemSdkError {
return when (val e = value as? ScanChainException) {
is ScanChainException.DisclaimerWasCanceled -> TangemSdkError.UserCancelled()
is ScanChainException.OnboardingNeeded,

View file

@ -14,7 +14,6 @@ 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.res.getStringSafe
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath
@ -41,6 +40,7 @@ import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
@ -224,7 +224,7 @@ 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.sendErrorEvent(TangemSdkErrorEvent(error))
}
}
}

View file

@ -7,6 +7,6 @@ internal class DefaultTokensFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles {
override val isBalancesCachingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "BALANCES_CACHING_ENABLED")
override val isNetworksLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED")
}

View file

@ -5,17 +5,10 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
internal class TangemWcBlockchainHelper(
featureToggles: WalletConnectFeatureToggles,
) : WcBlockchainHelper {
internal class TangemWcBlockchainHelper : WcBlockchainHelper {
private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) {
setOf(Blockchain.Solana, Blockchain.SolanaTestnet)
} else {
emptySet()
}
private val supportedNonEvmBlockchains = setOf(Blockchain.Solana, Blockchain.SolanaTestnet)
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null

View file

@ -8,11 +8,14 @@ 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
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
@ -24,7 +27,6 @@ internal class DefaultLegacyWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
) : LegacyWalletConnectRepository {
private var sessionProposal: Wallet.Model.SessionProposal? = null
@ -36,7 +38,7 @@ internal class DefaultLegacyWalletConnectRepository(
private val _activeSessions: MutableSharedFlow<List<WalletConnectSession>> = MutableSharedFlow()
override val activeSessions: Flow<List<WalletConnectSession>> = _activeSessions
private val blockchainHelper by lazy { TangemWcBlockchainHelper(walletConnectFeatureToggles) }
private val blockchainHelper by lazy { TangemWcBlockchainHelper() }
override var currentSessions: List<WalletConnectSession> = emptyList()
private set
@ -555,9 +557,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

@ -33,7 +33,6 @@ internal object WalletConnectInteractorModule {
fun provideWalletConnectInteractor(
wcRepository: LegacyWalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
@ -44,7 +43,7 @@ internal object WalletConnectInteractorModule {
walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles),
blockchainHelper = TangemWcBlockchainHelper(),
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
@ -69,13 +68,11 @@ internal object WalletConnectModule {
application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
): LegacyWalletConnectRepository {
return DefaultLegacyWalletConnectRepository(
application = application,
wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler,
walletConnectFeatureToggles = walletConnectFeatureToggles,
)
}

View file

@ -6,6 +6,6 @@ internal class WalletConnectFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) {
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

@ -20,6 +20,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
@ -55,6 +56,7 @@ internal class CardSettingsModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : Model() {
private val params = paramsContainer.require<CardSettingsComponent.Params>()
@ -198,13 +200,24 @@ internal class CardSettingsModel @Inject constructor(
}
if (scanResponse.cardTypesResolver.isTangemTwins()) {
// needs to prepare twin state if it's twin cards (depends on onboarding refactoring)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse)))
if (onboardingV2FeatureToggles.isTwinRefactoringEnabled) {
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.RecreateWalletTwin,
),
)
}
} else {
// needs to prepare twin state if it's twin cards (depends on onboarding refactoring)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse)))
cardSettingsInteractor.clear()
cardSettingsInteractor.clear()
store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) }
store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) }
}
} else {
val card = scanResponse.card

View file

@ -140,9 +140,9 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean)
Icon(
painter = painterResource(
if (checked) {
R.drawable.ic_accepted
R.drawable.ic_accepted_20
} else {
R.drawable.ic_unticked
R.drawable.ic_unticked_20
},
),
contentDescription = null,

View file

@ -4,13 +4,15 @@ 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.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
@ -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 analyticsErrorHandler: AnalyticsErrorHandler,
) : Model() {
private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value
@ -85,14 +89,14 @@ 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))
analyticsErrorHandler.sendErrorEvent(TangemSdkErrorEvent(error))
}
}
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

@ -30,8 +30,6 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
@ -60,8 +58,6 @@ internal class MainViewModel @Inject constructor(
private val getStoryContentUseCase: GetStoryContentUseCase,
private val imagePreloader: ImagePreloader,
private val fetchHotCryptoUseCase: FetchHotCryptoUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
onrampFeatureToggles: OnrampFeatureToggles,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -82,9 +78,7 @@ internal class MainViewModel @Inject constructor(
}
}
if (onrampFeatureToggles.isHotTokensEnabled) {
viewModelScope.launch { fetchHotCryptoUseCase() }
}
viewModelScope.launch { fetchHotCryptoUseCase() }
updateAppCurrencies()
observeFlips()
@ -301,23 +295,21 @@ internal class MainViewModel @Inject constructor(
private fun preloadImages() {
try {
viewModelScope.launch {
if (swapFeatureToggles.isPromoStoriesEnabled) {
val swapStories = getStoryContentUseCase.invokeSync(
id = StoryContentIds.STORY_FIRST_TIME_SWAP.id,
refresh = true,
).getOrNull()
val swapStories = getStoryContentUseCase.invokeSync(
id = StoryContentIds.STORY_FIRST_TIME_SWAP.id,
refresh = true,
).getOrNull()
if (swapStories == null) {
analyticsEventHandler.send(
StoriesEvents.Error(
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
),
)
} else {
val storiesImages = swapStories.getImageUrls()
// try to preload images for stories
storiesImages.forEach(imagePreloader::preload)
}
if (swapStories == null) {
analyticsEventHandler.send(
StoriesEvents.Error(
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
),
)
} else {
val storiesImages = swapStories.getImageUrls()
// try to preload images for stories
storiesImages.forEach(imagePreloader::preload)
}
}
} catch (ex: Exception) {

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 ||
@ -85,7 +85,7 @@ object OnboardingHelper {
scanResponse = scanResponse,
mode = when (backupState.startedSource) {
BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding
BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackup
BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1
},
)
}
@ -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
@ -40,6 +43,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")
@ -73,8 +77,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 {
@ -201,9 +209,12 @@ internal class ChildFactory @Inject constructor(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
multiWalletMode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding
AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup
mode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding
AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin
AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin
},
),
componentFactory = onboardingEntryComponentFactory,
@ -255,18 +266,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(
@ -276,17 +302,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,
@ -399,18 +437,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(
@ -482,10 +535,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,
@ -531,11 +588,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(
@ -645,9 +710,12 @@ internal class ChildFactory @Inject constructor(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
multiWalletMode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding
AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup
mode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding
AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin
AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin
},
),
componentFactory = onboardingEntryComponentFactory,

View file

@ -9,7 +9,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/access_code_info_protect_margin_top"
android:src="@drawable/ic_access_code"
android:src="@drawable/ic_access_code_119"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

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(
@ -255,7 +260,9 @@ sealed class AppRoute(val path: String) : Route {
enum class Mode {
Onboarding, // general Mode
AddBackup, // continue backup process for existing wallet 1
AddBackupWallet1, // continue backup process for existing wallet 1
WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins
RecreateWalletTwin, // reset twins
}
}

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,6 @@
package com.tangem.core.analytics.models
class ExceptionAnalyticsEvent(
val exception: Throwable,
val 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

@ -3,22 +3,10 @@
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "undefined"
},
{
"name": "WC_SOLANA_TX_SIGN_ENABLED",
"version": "5.18.0"
},
{
"name": "IS_ETHEREUM_EIP_1559_ENABLED",
"version": "5.17.0"
},
{
"name": "ONRAMP_ENABLED",
"version": "undefined"
},
{
"name": "MAIN_ACTION_BUTTONS_ENABLED",
"version": "5.19.0"
},
{
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",
"version": "5.22.0"
@ -27,26 +15,14 @@
"name": "NAVIGATION_REFACTORING",
"version": "undefined"
},
{
"name": "SWAP_STORIES_ENABLED",
"version": "5.21.0"
},
{
"name": "VISA_ONBOARDING_ENABLED",
"version": "undefined"
},
{
"name": "ONRAMP_HOT_TOKENS_ENABLED",
"version": "5.21.0"
},
{
"name": "STAKING_TON_ENABLED",
"version": "undefined"
},
{
"name": "BALANCES_CACHING_ENABLED",
"version": "5.21.0"
},
{
"name": "NFT_ENABLED",
"version": "undefined"
@ -65,6 +41,26 @@
},
{
"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"
},
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "undefined"
}
]

View file

@ -19,13 +19,13 @@ internal class TangemTech(
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem-tech.com/v1/",
baseUrl = "https://api.tangem.org/v1/",
headers = createHeaders(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "https://devapi.tangem-tech.com/v1/",
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)

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

@ -152,7 +152,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
id = ApiConfig.ID.TangemTech,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem-tech.com/v1/",
baseUrl = "https://api.tangem.org/v1/",
headers = mapOf(
"card_id" to ProviderSuspend { APP_CARD_ID },
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },

View file

@ -14,6 +14,11 @@ fun SpacerH(height: Dp, modifier: Modifier = Modifier) {
Spacer(modifier = modifier.height(height))
}
@Composable
fun SpacerH2(modifier: Modifier = Modifier) {
SpacerH(2.dp, modifier)
}
@Composable
fun SpacerH4(modifier: Modifier = Modifier) {
SpacerH(4.dp, modifier)

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.tokenlist.impl.ui.components
package com.tangem.core.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
@ -8,16 +8,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
@Composable
internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),

View file

@ -109,7 +109,7 @@ private fun BoxScope.ContentIconContainer(
)
if (icon.topBadgeIconResId != null && shouldShowTopBadge) {
TopBadge(
CurrencyIconTopBadge(
modifier = Modifier
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
.align(Alignment.TopEnd),
@ -120,7 +120,7 @@ private fun BoxScope.ContentIconContainer(
}
if (icon.showCustomBadge) {
BottomBadge(
CurrencyIconBottomBadge(
modifier = Modifier.align(Alignment.BottomEnd),
)
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.components.currency.icon
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun CurrencyIconBottomBadge(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size12)
.background(
color = TangemTheme.colors.background.primary,
shape = CircleShape,
),
) {
Box(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing2)
.matchParentSize()
.background(
color = TangemTheme.colors.icon.informative,
shape = CircleShape,
),
)
}
}

View file

@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun TopBadge(
fun CurrencyIconTopBadge(
@DrawableRes iconResId: Int,
alpha: Float,
colorFilter: ColorFilter?,
@ -38,26 +38,4 @@ internal fun TopBadge(
contentDescription = null,
)
}
}
@Composable
internal fun BottomBadge(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size12)
.background(
color = TangemTheme.colors.background.primary,
shape = CircleShape,
),
) {
Box(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing2)
.matchParentSize()
.background(
color = TangemTheme.colors.icon.informative,
shape = CircleShape,
),
)
}
}

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
}
}

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