Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-23 11:16:28 +03:00
commit ca49abf2b1
892 changed files with 17979 additions and 6555 deletions

3
.gitignore vendored
View file

@ -9,6 +9,9 @@ local.properties
# Google services
app/src/debug/google-services.json
app/src/internal/google-services.json
app/src/external/google-services.json
# Gradle generated files
.gradle

View file

@ -111,6 +111,8 @@ dependencies {
implementation(projects.domain.networks)
implementation(projects.domain.quotes)
implementation(projects.domain.notifications)
implementation(projects.domain.notifications.models)
implementation(projects.domain.notifications.toggles)
implementation(projects.common)
implementation(projects.common.routing)

View file

@ -128,7 +128,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*"
android:host="onramp"
android:scheme="tangem" />
</intent-filter>
@ -150,7 +150,29 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*"
android:host="referral"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="main"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="token"
android:scheme="tangem" />
</intent-filter>

@ -1 +1 @@
Subproject commit a20134ee4be4d7e1e34c6f44089b3cb0713eacc3
Subproject commit 4ec5de66321afa82c04104746015ea9e6bc9fe64

View file

@ -15,6 +15,7 @@ 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.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
@ -142,4 +143,6 @@ interface ApplicationEntryPoint {
fun getOnlineCardVerifier(): OnlineCardVerifier
fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory
fun getApiConfigsManager(): ApiConfigsManager
}

View file

@ -19,16 +19,19 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.RoutingFeatureToggle
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.UiDependencies
@ -48,6 +51,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
@ -67,6 +71,7 @@ import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.utils.DeepLinkFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import dagger.hilt.android.AndroidEntryPoint
@ -171,6 +176,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector
@Inject
internal lateinit var routingFeatureToggle: RoutingFeatureToggle
@Inject
internal lateinit var deeplinkFactory: DeepLinkFactory
@Inject
internal lateinit var walletConnectFeatureToggles: WalletConnectFeatureToggles
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -223,9 +237,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
sendStakingUnsubmittedHashes()
checkGoogleServicesAvailability()
if (intent != null && savedInstanceState == null) {
if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) {
// handle intent only on start, not on recreate
deepLinksRegistry.launch(intent)
handleDeepLink(intent)
}
lifecycle.addObserver(WindowObscurationObserver)
@ -341,8 +355,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
}
}
private fun updateAppTheme(appThemeMode: AppThemeMode) {
val mode = when (appThemeMode) {
@ -374,7 +391,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
if (intent != null) {
deepLinksRegistry.launch(intent)
handleDeepLink(intent)
}
}
@ -455,9 +472,24 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
}
if (routingFeatureToggle.isDeepLinkNavigationEnabled && intent != null) {
handleDeepLink(intent)
}
viewModel.checkForUnfinishedBackup()
}
private fun handleDeepLink(intent: Intent) {
if (routingFeatureToggle.isDeepLinkNavigationEnabled) {
val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri()
val receivedDeepLink = intent.data ?: deepLinkExtras ?: return
deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope)
} else {
deepLinksRegistry.launch(intent)
}
}
private fun observePolkadotAccountHealthCheck() {
lifecycleScope.launch {
getPolkadotCheckHasResetUseCase()

View file

@ -30,6 +30,7 @@ import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
@ -228,6 +229,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val userWalletBuilderFactory: UserWalletBuilder.Factory
get() = entryPoint.getUserWalletBuilderFactory()
private val apiConfigsManager: ApiConfigsManager
get() = entryPoint.getApiConfigsManager()
// endregion
private val appScope = MainScope()
@ -275,6 +279,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
}
fun init() {
apiConfigsManager.initialize()
store = createReduxStore()
tangemAppLoggerInitializer.initialize()
@ -285,12 +291,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async {
featureTogglesManager.init()
},
async {
excludedBlockchainsManager.init()
},
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
)
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}

View file

@ -104,7 +104,6 @@ sealed class AnalyticsParam {
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"
const val FIRMWARE = "Firmware"
const val USER_WALLET_ID = "User Wallet ID"
const val CURRENCY = "Currency"
const val ERROR_DESCRIPTION = "Error Description"
const val ERROR_CODE = "Error Code"

View file

@ -1,14 +1,23 @@
package com.tangem.tap.common.analytics.handlers.amplitude
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.AnalyticsUserIdHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
class AmplitudeAnalyticsHandler(
private val client: AmplitudeAnalyticsClient,
) : AnalyticsHandler {
) : AnalyticsHandler, AnalyticsUserIdHandler {
override fun id(): String = ID
override fun setUserId(userId: String) {
client.setUserId(userId)
}
override fun clearUserId() {
client.clearUserId()
}
override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params)
}

View file

@ -4,13 +4,14 @@ import android.app.Application
import com.amplitude.api.Amplitude
import com.amplitude.api.AmplitudeClient
import com.tangem.core.analytics.api.EventLogger
import com.tangem.core.analytics.api.UserIdHolder
import com.tangem.utils.converter.Converter
import org.json.JSONObject
/**
[REDACTED_AUTHOR]
*/
interface AmplitudeAnalyticsClient : EventLogger
interface AmplitudeAnalyticsClient : EventLogger, UserIdHolder
internal class AmplitudeClient(
application: Application,
@ -24,6 +25,14 @@ internal class AmplitudeClient(
client.enableForegroundTracking(application)
}
override fun setUserId(userId: String) {
client.setUserId(userId)
}
override fun clearUserId() {
client.setUserId(null)
}
override fun logEvent(event: String, params: Map<String, String>) {
client.logEvent(event, ParamsToJSONObjectConverter().convert(params))
}

View file

@ -12,6 +12,16 @@ internal class AmplitudeLogClient(
private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(AmplitudeAnalyticsHandler.ID, jsonConverter)
private var userId: String? = null
override fun setUserId(userId: String) {
this.userId = userId
}
override fun clearUserId() {
this.userId = null
}
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.handlers.firebase
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.api.AnalyticsUserIdHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
@ -10,12 +11,20 @@ import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
class FirebaseAnalyticsHandler(
private val client: FirebaseAnalyticsClient,
) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler {
) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler, AnalyticsUserIdHandler {
private val errorConverter = AnalyticsErrorConverter()
override fun id(): String = ID
override fun setUserId(userId: String) {
client.setUserId(userId)
}
override fun clearUserId() {
client.clearUserId()
}
override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params)
}

View file

@ -8,11 +8,12 @@ import com.google.firebase.crashlytics.recordException
import com.google.firebase.ktx.Firebase
import com.tangem.core.analytics.api.ExceptionLogger
import com.tangem.core.analytics.api.EventLogger
import com.tangem.core.analytics.api.UserIdHolder
/**
[REDACTED_AUTHOR]
*/
interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger
interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger, UserIdHolder
internal class FirebaseClient : FirebaseAnalyticsClient {
@ -21,6 +22,14 @@ internal class FirebaseClient : FirebaseAnalyticsClient {
private val eventConverter = FirebaseAnalyticsEventConverter()
override fun setUserId(userId: String) {
Firebase.analytics.setUserId(userId)
}
override fun clearUserId() {
Firebase.analytics.setUserId(null)
}
override fun logEvent(event: String, params: Map<String, String>) {
fbAnalytics.logEvent(
eventConverter.convertEventName(event),

View file

@ -11,6 +11,15 @@ internal class FirebaseLogClient(
) : FirebaseAnalyticsClient {
private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(FirebaseAnalyticsHandler.ID, jsonConverter)
private var userId: String? = null
override fun setUserId(userId: String) {
this.userId = userId
}
override fun clearUserId() {
this.userId = null
}
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)

View file

@ -40,9 +40,6 @@ class CardContextInterceptor(
params[AnalyticsParam.BATCH] = card.batchId
params[AnalyticsParam.PRODUCT_TYPE] = getProductType()
params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue
if (userWalletId != null) {
params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue
}
ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let {
params[AnalyticsParam.CURRENCY] = it.value

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.extensions
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
/**
@ -12,6 +13,11 @@ import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterc
* Sets the new context
*/
fun Analytics.setContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (userWalletId != null) {
setUserId(userWalletId.stringValue)
}
addParamsInterceptor(LinkedCardContextInterceptor(scanResponse))
}
@ -19,6 +25,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
* Erases the context
*/
fun Analytics.eraseContext() {
clearUserId()
removeParamsInterceptor(LinkedCardContextInterceptor.id())
}
@ -26,6 +33,11 @@ fun Analytics.eraseContext() {
* Adds a new context and keeps a previous context as the parent of the new one
*/
fun Analytics.addContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (userWalletId != null) {
setUserId(userWalletId.stringValue)
}
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext)

View file

@ -1,34 +1,5 @@
package com.tangem.tap.common.extensions
import android.graphics.Bitmap
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.util.Hashtable
@Suppress("MagicNumber")
fun String.toQrCode(): Bitmap {
val hintMap = Hashtable<EncodeHintType, Any>()
hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage
hintMap[EncodeHintType.MARGIN] = 2
val qrCodeWriter = QRCodeWriter()
val size = 256
val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap)
val width = bitMatrix.width
val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until width) {
bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
}
}
return bmp
}
fun String.removePrefixOrNull(prefix: String): String? = when {
startsWith(prefix) -> substring(prefix.length)
else -> null

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchainsdk.utils.amountToCreateAccount
import com.tangem.common.services.Result
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.domain.TapError

View file

@ -3,8 +3,9 @@ package com.tangem.tap.data
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider {
internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider {
override suspend fun getToken(): String {
return FirebaseMessaging.getInstance().token.await()
}

View file

@ -10,7 +10,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -70,7 +69,6 @@ internal object ManageTokensDomainModule {
customTokensRepository: CustomTokensRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
derivationsRepository: DerivationsRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
@ -83,7 +81,6 @@ internal object ManageTokensDomainModule {
customTokensRepository = customTokensRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
derivationsRepository = derivationsRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,

View file

@ -13,7 +13,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import dagger.Module
@ -72,7 +71,6 @@ object MarketsDomainModule {
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -84,7 +82,6 @@ object MarketsDomainModule {
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,

View file

@ -1,9 +1,12 @@
package com.tangem.tap.di.domain
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.quotes.single.SingleQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -72,10 +75,11 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun providesGetNFTNetworkStatusUseCase(networksRepository: NetworksRepository): GetNFTNetworkStatusUseCase =
GetNFTNetworkStatusUseCase(
networksRepository = networksRepository,
)
fun providesGetNFTNetworkStatusUseCase(
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
): GetNFTNetworkStatusUseCase {
return GetNFTNetworkStatusUseCase(singleNetworkStatusSupplier = singleNetworkStatusSupplier)
}
@Provides
@Singleton
@ -83,4 +87,53 @@ internal object NFTDomainModule {
GetNFTExploreUrlUseCase(
nftRepository = nftRepository,
)
@Provides
@Singleton
fun provideGetNFTPriceUseCase(
nftRepository: NFTRepository,
singleQuoteSupplier: SingleQuoteSupplier,
): GetNFTPriceUseCase {
return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier)
}
@Provides
@Singleton
fun provideFetchNFTPriceUseCase(
nftRepository: NFTRepository,
singleQuoteFetcher: SingleQuoteFetcher,
): FetchNFTPriceUseCase {
return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher)
}
@Provides
@Singleton
fun provideEnableWalletNFTUseCase(walletsRepository: WalletsRepository): EnableWalletNFTUseCase {
return EnableWalletNFTUseCase(walletsRepository)
}
@Provides
@Singleton
fun provideDisableWalletNFTUseCase(
walletsRepository: WalletsRepository,
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
): DisableWalletNFTUseCase {
return DisableWalletNFTUseCase(walletsRepository, nftRepository, currenciesRepository)
}
@Provides
@Singleton
fun provideGetWalletNFTEnabledUseCase(walletsRepository: WalletsRepository): GetWalletNFTEnabledUseCase {
return GetWalletNFTEnabledUseCase(walletsRepository)
}
@Provides
@Singleton
fun provideClearNFTCacheUseCase(
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository)
}
}

View file

@ -1,10 +1,13 @@
package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase
import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Module
import dagger.Provides
@ -28,12 +31,10 @@ internal object NotificationsDomainModule {
@Singleton
fun providesSendPushTokenUseCase(
notificationsRepository: NotificationsRepository,
getApplicationIdUseCase: GetApplicationIdUseCase,
pushNotificationsTokenProvider: PushNotificationsTokenProvider,
): SendPushTokenUseCase {
return SendPushTokenUseCase(
notificationsRepository = notificationsRepository,
getApplicationIdUseCase = getApplicationIdUseCase,
pushNotificationsTokenProvider = pushNotificationsTokenProvider,
)
}
@ -57,4 +58,10 @@ internal object NotificationsDomainModule {
notificationsRepository = notificationsRepository,
)
}
@Provides
@Singleton
fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles {
return DefaultNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository
@ -18,7 +19,10 @@ import com.tangem.domain.tokens.*
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.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -37,7 +41,6 @@ internal object TokensDomainModule {
@Singleton
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -47,7 +50,6 @@ internal object TokensDomainModule {
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -62,7 +64,6 @@ internal object TokensDomainModule {
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -71,7 +72,6 @@ internal object TokensDomainModule {
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -121,8 +121,8 @@ internal object TokensDomainModule {
fun provideGetCurrencyUseCase(
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyStatusUpdatesUseCase {
return GetCurrencyStatusUpdatesUseCase(
): GetSingleCryptoCurrencyStatusUseCase {
return GetSingleCryptoCurrencyStatusUseCase(
currencyStatusOperations = baseCurrencyStatusOperations,
dispatchers = dispatchers,
)
@ -147,7 +147,6 @@ internal object TokensDomainModule {
fun provideGetCurrencyWarningsUseCase(
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
currencyChecksRepository: CurrencyChecksRepository,
dispatchers: CoroutineDispatcherProvider,
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
@ -155,31 +154,17 @@ internal object TokensDomainModule {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
dispatchers = dispatchers,
currencyChecksRepository = currencyChecksRepository,
dispatchers = dispatchers,
currencyStatusOperations = baseCurrencyStatusOperations,
)
}
@Provides
@Singleton
fun provideGetPrimaryCurrencyUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currencyStatusOperations = currencyStatusOperations,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideFetchCurrencyStatusUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -188,7 +173,6 @@ internal object TokensDomainModule {
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
@ -203,7 +187,6 @@ internal object TokensDomainModule {
fun provideFetchCardTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -212,7 +195,6 @@ internal object TokensDomainModule {
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -222,14 +204,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun providesGetCryptoCurrencyStatusSyncUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
): GetCryptoCurrencyStatusSyncUseCase {
return GetCryptoCurrencyStatusSyncUseCase(currencyStatusOperations)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase {
@ -328,14 +302,10 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideUpdateDelayedCurrencyStatusUseCase(
networksRepository: NetworksRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): UpdateDelayedNetworkStatusUseCase {
return UpdateDelayedNetworkStatusUseCase(
networksRepository = networksRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -413,7 +383,6 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -427,7 +396,6 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
@ -447,7 +415,6 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -461,7 +428,6 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,

View file

@ -4,9 +4,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.*
@ -43,7 +41,6 @@ internal object TransactionDomainModule {
transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SendTransactionUseCase {
return SendTransactionUseCase(
demoConfig = DemoConfig(),
@ -51,7 +48,6 @@ internal object TransactionDomainModule {
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -61,17 +57,13 @@ internal object TransactionDomainModule {
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -6,6 +6,8 @@ import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
@ -28,6 +30,17 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@Provides
fun providesUserWalletsSyncDelegate(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsSyncDelegate {
return DefaultUserWalletsSyncDelegate(
userWalletsListManager = userWalletsListManager,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase {
@ -102,10 +115,13 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesRenameWalletUseCase(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
): RenameWalletUseCase {
return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers)
return RenameWalletUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
)
}
@Provides
@ -197,4 +213,36 @@ internal object WalletsDomainModule {
nftFeatureToggles = nftFeatureToggles,
)
}
@Provides
@Singleton
fun providesUpdateRemoteWalletsInfoUseCase(
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
): UpdateRemoteWalletsInfoUseCase {
return UpdateRemoteWalletsInfoUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
)
}
@Provides
@Singleton
fun providesGetSavedWalletChangesIdUseCase(
userWalletsListManager: UserWalletsListManager,
): GetSavedWalletChangesUseCase {
return GetSavedWalletChangesUseCase(
userWalletsListManager = userWalletsListManager,
)
}
@Provides
@Singleton
fun providesAssociateWalletsWithApplicationIdUseCase(
walletsRepository: WalletsRepository,
): AssociateWalletsWithApplicationIdUseCase {
return AssociateWalletsWithApplicationIdUseCase(
walletsRepository = walletsRepository,
)
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.routing
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.RoutingFeatureToggle
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.tap.routing.ProxyAppRouter
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.configurator.MutableAppRouterConfig
@ -31,4 +33,10 @@ internal object AppRouterModule {
@Provides
@Singleton
fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig()
@Provides
@Singleton
fun provideRoutingFeatureToggle(featureTogglesManager: FeatureTogglesManager): RoutingFeatureToggle {
return RoutingFeatureToggle(featureTogglesManager)
}
}

View file

@ -15,9 +15,9 @@ import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.BackendId
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -41,7 +41,7 @@ internal class DefaultDerivationsRepository(
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
}
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.ID>) {
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
derivePublicKeysByNetworks(

View file

@ -8,10 +8,10 @@ import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.operations.derivation.ExtendedPublicKeysMap
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
@ -49,7 +49,7 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
val config = CardConfig.createConfig(scanResponse.card)
return mapNotNull { network ->
val blockchain = Blockchain.fromId(id = network.id.value)
val blockchain = Blockchain.fromId(id = network.rawId)
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
findNewDerivations(curve = curve, scanResponse = scanResponse, network = network)
@ -74,7 +74,7 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
}
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = this.id.value)
val blockchain = Blockchain.fromId(id = rawId)
return buildList {
add(blockchain.getDerivationPath(curve = curve))

View file

@ -0,0 +1,11 @@
package com.tangem.tap.domain.notifications
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
internal class DefaultNotificationsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : NotificationsFeatureToggles {
override val isNotificationsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED")
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.tasks.visa
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
@ -14,22 +13,18 @@ import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.datasource.local.visa.VisaOtpData
import com.tangem.datasource.local.visa.hasSavedOTP
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
@ -95,23 +90,7 @@ class VisaCardActivationTask @AssistedInject constructor(
context.signAuthorizationChallenge(mode.authorizationChallenge)
}
is VisaCardActivationTaskMode.SignOnly -> {
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
context.signData(
mode.dataToSignByCardWallet,
derivedPublicKey,
)
context.signData(mode.dataToSignByCardWallet)
}
}
}
@ -164,16 +143,7 @@ class VisaCardActivationTask @AssistedInject constructor(
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) }
.value
@ -190,10 +160,7 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await()
signData(
dataToSign = dataToSign,
derivedPublicKey = derivedPublicKey,
)
signData(dataToSign = dataToSign)
}
}
@ -201,15 +168,16 @@ class VisaCardActivationTask @AssistedInject constructor(
signedChallenge: VisaAuthSignedChallenge,
cardWalletAddress: String,
): Either<TangemError, VisaDataToSignByCardWallet> = either {
catch(
block = {
val tokens = visaAuthRepository.getAccessTokens(signedChallenge)
.getOrElse { raise(it.tangemError) }
visaAuthTokenStorage.store(cardId, tokens)
val remoteState = visaActivationRepository.getActivationRemoteState()
.getOrElse { raise(it.tangemError) }
if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) {
raise(VisaActivationError.WrongRemoteState.tangemError)
return raise(VisaActivationError.WrongRemoteState.tangemError)
}
visaActivationRepository.getCardWalletAcceptanceData(
@ -217,12 +185,7 @@ class VisaCardActivationTask @AssistedInject constructor(
activationOrderInfo = remoteState.activationOrderInfo,
cardWalletAddress = cardWalletAddress,
),
)
},
catch = {
raise(VisaAuthorizationAPIError.tangemError)
},
)
).getOrElse { raise(it.tangemError) }
}
private suspend fun SessionContext.createWallet(): CompletionResult<Unit> {
@ -294,7 +257,6 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.signData(
dataToSign: VisaDataToSignByCardWallet,
derivedPublicKey: ExtendedPublicKey,
): CompletionResult<VisaCardActivationResponse> {
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
@ -306,7 +268,6 @@ class VisaCardActivationTask @AssistedInject constructor(
val task = SignHashCommand(
hash = dataToSign.hashToSign.hexToBytes(),
walletPublicKey = wallet.publicKey,
derivationPath = VisaUtilities.visaDefaultDerivationPath,
)
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
@ -325,7 +286,7 @@ class VisaCardActivationTask @AssistedInject constructor(
handleSignedData(
dataToSign = dataToSign,
response = result.data,
derivedPublicKey = derivedPublicKey,
walletPublicKey = wallet.publicKey,
)
}
is CompletionResult.Failure -> {
@ -335,23 +296,9 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
private suspend fun SessionContext.deriveKey(publicKey: ByteArray): CompletionResult<ExtendedPublicKey> {
val derivationPath = VisaUtilities.visaDefaultDerivationPath
?: return CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError)
val derivationTask = DeriveWalletPublicKeyTask(publicKey, derivationPath)
val derivationTaskResult = suspendCancellableCoroutine { continuation ->
derivationTask.run(session) { result ->
continuation.resume(result)
}
}
return derivationTaskResult
}
private suspend fun SessionContext.handleSignedData(
dataToSign: VisaDataToSignByCardWallet,
derivedPublicKey: ExtendedPublicKey,
walletPublicKey: ByteArray,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val otp = otpStorage.getOTP(cardId) ?: run {
@ -362,7 +309,7 @@ class VisaCardActivationTask @AssistedInject constructor(
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = response.signature,
hash = dataToSign.hashToSign.hexToBytes(),
publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(),
publicKey = walletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
val signedActivationData = dataToSign.sign(

View file

@ -40,17 +40,12 @@ class VisaCustomerWalletApproveTask(
}
if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) {
// TODO TVF-21
callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve")))
callback(CompletionResult.Failure(VisaActivationError.VisaCardForApproval.tangemError))
return
}
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) {
callback(
CompletionResult.Failure(
TangemSdkError.Underlying("Use tangem wallet specified during visa registration"), // TODO TVF-21
),
)
callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError))
return
}

View file

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

View file

@ -2,29 +2,24 @@ package com.tangem.tap.domain.visa
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.error.VisaCardScanError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.operations.attestation.AttestWalletKeyTask
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
@ -61,54 +56,19 @@ internal class VisaCardScanHandler @Inject constructor(
session = session,
)
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
val activationInput =
VisaActivationInput(card.cardId, card.cardPublicKey.toHexString(), card.isAccessCodeSet)
val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput)
return CompletionResult.Success(activationStatus)
}
return context.deriveKey(wallet)
}
private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult<VisaCardActivationStatus> {
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while first scan")
return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError)
}
val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath)
val derivationTaskResult = suspendCancellableCoroutine { continuation ->
derivationTask.run(session) { result ->
continuation.resume(result)
}
}
return handleDerivationResponse(derivationTaskResult)
}
private suspend fun SessionContext.handleDerivationResponse(
result: CompletionResult<ExtendedPublicKey>,
): CompletionResult<VisaCardActivationStatus> {
return when (result) {
is CompletionResult.Success -> {
Timber.i("Start task for loading challenge for Visa wallet")
handleWalletAuthorization()
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
return context.handleWalletAuthorization()
}
private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult<VisaCardActivationStatus> {
Timber.i("Started handling authorization using Visa wallet")
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while handling wallet authorization")
return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError)
}
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
@ -116,43 +76,35 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError)
}
val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(VisaCardScanError.FailedToFindDerivedWalletKey.tangemError)
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(extendedPublicKey.publicKey)
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey)
.getOrElse {
return CompletionResult.Failure(it.tangemError)
}
Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = runCatching {
// TODO [REDACTED_TASK_KEY]
error("sign and get specific error to switch to card_id flow")
visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
}.getOrElse {
Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization(
cardWalletAddress = walletAddress.value,
)
val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
.getOrElse {
Timber.i("Failed to get Access token for Wallet public key authorization")
return CompletionResult.Failure(it.tangemError)
}
val signChallengeResult = signChallengeWithWallet(
publicKey = wallet.publicKey,
derivationPath = derivationPath,
nonce = challengeResponse.challenge,
)
return when (signChallengeResult) {
is CompletionResult.Success -> {
val signature = signChallengeResult.data.cardSignature ?: run {
Timber.i("Failed to sign challenge with Wallet public key")
return CompletionResult.Failure(VisaCardScanError.FailedToSignChallenge.tangemError)
}
Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
cardWalletAddress = walletAddress.value,
signedChallenge = challengeResponse
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
signedChallenge = challengeResponse.toSignedChallenge(signature.toHexString()),
)
}
is CompletionResult.Failure -> {
@ -166,15 +118,18 @@ internal class VisaCardScanHandler @Inject constructor(
cardWalletAddress: String,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
}.getOrElse {
Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization(
cardWalletAddress = cardWalletAddress,
)
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
.getOrElse {
Timber.i("Failed to get Access token for Wallet public key authorization.")
return if (
it is VisaApiError.ProductInstanceIsNotActivated ||
it is VisaApiError.ProductInstanceNotFoundActivationRequired
) {
Timber.i("Proceeding with card authorization.")
handleCardAuthorization(cardWalletAddress = cardWalletAddress)
} else {
CompletionResult.Failure(it.tangemError)
}
}
Timber.i("Authorized using Wallet public key successfully")
@ -190,14 +145,12 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting authorization challenge to sign")
val challengeResponse = runCatching {
visaAuthRepository.getCardAuthChallenge(
val challengeResponse = visaAuthRepository.getCardAuthChallenge(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey.toHexString(),
)
}.getOrElse {
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
).getOrElse {
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.errorCode}")
return CompletionResult.Failure(it.tangemError)
}
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
@ -217,17 +170,14 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
@Suppress("UnusedPrivateMember")
val authorizationTokensResponse = runCatching {
visaAuthRepository.getAccessTokens(
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(
signedChallenge = challengeResponse.toSignedChallenge(
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
salt = attestCardKeyResponse.salt.toHexString(),
),
)
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
).getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}")
return CompletionResult.Failure(it.tangemError)
}
visaAuthTokenStorage.store(
@ -235,11 +185,9 @@ internal class VisaCardScanHandler @Inject constructor(
tokens = authorizationTokensResponse,
)
val activationRemoteState = runCatching {
visaActivationRepository.getActivationRemoteState()
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
val activationRemoteState = visaActivationRepository.getActivationRemoteState().getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}")
return CompletionResult.Failure(it.tangemError)
}
val error = when (activationRemoteState) {
@ -271,13 +219,11 @@ internal class VisaCardScanHandler @Inject constructor(
private suspend fun SessionContext.signChallengeWithWallet(
publicKey: ByteArray,
derivationPath: DerivationPath,
nonce: String,
): CompletionResult<SignHashResponse> {
val signHashCommand = SignHashCommand(
hash = nonce.hexToBytes(),
walletPublicKey = publicKey,
derivationPath = derivationPath,
): CompletionResult<AttestWalletKeyResponse> {
val signHashCommand = AttestWalletKeyTask(
publicKey = publicKey,
challenge = nonce.hexToBytes(),
)
return suspendCancellableCoroutine {
signHashCommand.run(session) { result ->

View file

@ -13,6 +13,7 @@ import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
@ -64,14 +65,18 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
if (isNewWc) stub.updateSessions() else legacy.updateSessions()
}
override fun pair(uri: String, source: SourceType) {
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
val src = when (source) {
SourceType.QR -> WcPairRequest.Source.QR
SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK
SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD
SourceType.ETC -> WcPairRequest.Source.ETC
}
if (isNewWc) wcPairService.pair(WcPairRequest(uri, src)) else legacy.pair(uri, source)
if (isNewWc) {
wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId))
} else {
legacy.pair(userWalletId = userWalletId, uri = uri, source = source)
}
}
override fun disconnect(topic: String) {
@ -110,7 +115,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository
override fun updateSessions() = Unit
override fun pair(uri: String, source: SourceType) = Unit
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit
override fun disconnect(topic: String) = Unit
@ -405,7 +410,7 @@ internal class DefaultLegacyWalletConnectRepository(
this.userNamespaces = userNamespaces
}
override fun pair(uri: String, source: SourceType) {
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source))
WalletKit.pair(
params = Wallet.Params.Pair(uri),

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.flow.Flow
@ -19,7 +20,7 @@ interface LegacyWalletConnectRepository {
fun updateSessions()
fun pair(uri: String, source: SourceType)
fun pair(userWalletId: UserWalletId, uri: String, source: SourceType)
fun disconnect(topic: String)

View file

@ -3,8 +3,8 @@ package com.tangem.tap.domain.walletconnect2.domain
import arrow.core.flatten
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session
@ -12,6 +12,7 @@ import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsReposit
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.filterNotNull
@ -139,7 +140,11 @@ class WalletConnectInteractor(
if (deeplinkStack.empty()) return
val lastDeeplink = deeplinkStack.pop()
val action = WalletConnectAction
.OpenSession(lastDeeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
.OpenSession(
wcUri = lastDeeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
}.onFailure {
Timber.e("WC deeplink handling failed. $it")
@ -393,7 +398,11 @@ class WalletConnectInteractor(
}
if (isWalletConnectReadyForDeepLinks) {
val action = WalletConnectAction.OpenSession(deeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
val action = WalletConnectAction.OpenSession(
wcUri = deeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
} else {
deeplinkStack.push(deeplink)
@ -410,7 +419,7 @@ class WalletConnectInteractor(
private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List<Network>): List<Account> {
val walletManagers = networks.mapNotNull {
val blockchain = Blockchain.fromId(it.id.value)
val blockchain = Blockchain.fromId(it.rawId)
walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
@ -14,6 +15,7 @@ sealed class WalletConnectAction : Action {
data class OpenSession(
val wcUri: String,
val source: SourceType,
val userWalletId: UserWalletId,
) : WalletConnectAction() {
enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC }
}

View file

@ -63,7 +63,11 @@ class WalletConnectMiddleware {
is WalletConnectAction.OpenSession -> {
val index = action.wcUri.indexOf("@")
when (action.wcUri[index + 1]) {
'2' -> walletConnectRepository.pair(uri = action.wcUri, source = action.source)
'2' -> walletConnectRepository.pair(
uri = action.wcUri,
source = action.source,
userWalletId = action.userWalletId,
)
'1' -> {
store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest)
store.dispatchOnMain(

View file

@ -22,10 +22,10 @@ import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultWalletConnectComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
@Assisted params: WalletConnectComponent.Params,
) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber<WalletConnectState> {
private val model: WalletConnectModel = getOrCreateModel()
private val model: WalletConnectModel = getOrCreateModel(params)
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(model.updateState(store.state.walletConnectState))
@ -65,6 +65,9 @@ internal class DefaultWalletConnectComponent @AssistedInject constructor(
@AssistedFactory
interface Factory : WalletConnectComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectComponent
override fun create(
context: AppComponentContext,
params: WalletConnectComponent.Params,
): DefaultWalletConnectComponent
}
}

View file

@ -4,10 +4,12 @@ import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
@ -22,13 +24,22 @@ import javax.inject.Inject
internal class WalletConnectModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<WalletConnectComponent.Params>()
init {
modelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() }
.map { WalletConnectAction.OpenSession(it, WalletConnectAction.OpenSession.SourceType.QR) }
.map {
WalletConnectAction.OpenSession(
wcUri = it,
source = WalletConnectAction.OpenSession.SourceType.QR,
userWalletId = params.userWalletId,
)
}
.collect { store.dispatch(it) }
}
}

View file

@ -2,7 +2,9 @@ package com.tangem.tap.features.details.ui.walletconnect.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface WalletConnectComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, WalletConnectComponent>
data class Params(val userWalletId: UserWalletId)
interface Factory : ComponentFactory<Params, WalletConnectComponent>
}

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.keyboard.KeyboardValidator
import com.tangem.common.routing.RoutingFeatureToggle
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
@ -22,6 +23,10 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
@ -31,6 +36,9 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase
import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.features.onramp.deeplink.OnrampDeepLink
import com.tangem.tap.common.extensions.setContext
@ -38,11 +46,15 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@Suppress("LongParameterList")
@HiltViewModel
@ -56,7 +68,6 @@ internal class MainViewModel @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val apiConfigsManager: ApiConfigsManager,
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
@GlobalUiMessageSender private val messageSender: UiMessageSender,
private val keyboardValidator: KeyboardValidator,
@ -67,6 +78,14 @@ internal class MainViewModel @Inject constructor(
private val onboardingRepository: OnboardingRepository,
private val deepLinksRegistry: DeepLinksRegistry,
private val onrampDeepLinkFactory: OnrampDeepLink.Factory,
private val notificationsToggles: NotificationsFeatureToggles,
private val getApplicationIdUseCase: GetApplicationIdUseCase,
private val subscribeOnWalletsUseCase: GetSavedWalletChangesUseCase,
private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase,
private val updateRemoteWalletsInfoUseCase: UpdateRemoteWalletsInfoUseCase,
private val sendPushTokenUseCase: SendPushTokenUseCase,
private val apiConfigsManager: ApiConfigsManager,
routingFeatureToggle: RoutingFeatureToggle,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -77,33 +96,39 @@ internal class MainViewModel @Inject constructor(
private set
init {
/**
* Run any data initialization here that needs to happen before the app starts
* and is hidden behind the SplashScreen
*/
loadApplicationResources()
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
/** Run any API data load here that runs in parallel and does not block the app from starting */
launchAPIRequests {
launch { fetchHotCryptoUseCase() }
viewModelScope.launch {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
}
launch { fetchAppCurrenciesUseCase() }
launch { fetchStakingTokens() }
launch { initPushNotifications() }
}
viewModelScope.launch { fetchHotCryptoUseCase() }
viewModelScope.launch { incrementAppLaunchCounterUseCase() }
updateAppCurrencies()
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
fetchStakingTokens()
deleteDeprecatedLogsUseCase()
sendKeyboardIdentifierEvent()
preloadImages()
if (!routingFeatureToggle.isDeepLinkNavigationEnabled) {
initializeDeepLinks()
}
}
fun checkForUnfinishedBackup() {
viewModelScope.launch(dispatchers.main) {
@ -114,16 +139,41 @@ internal class MainViewModel @Inject constructor(
/** Loading the resources needed to run the application */
private fun loadApplicationResources() {
viewModelScope.launch(dispatchers.main) {
apiConfigsManager.initialize()
viewModelScope.launch {
launchAPIRequests {
launch { blockchainSDKFactory.init() }
launch {
withTimeout(timeMillis = 1.seconds.inWholeMilliseconds) { fetchUserCountry() }
}
}
blockchainSDKFactory.init()
prepareSelectedWalletFeedback()
isSplashScreenShown = false
}
}
private suspend fun fetchUserCountry() {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
}
}
private fun launchAPIRequests(function: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch {
if (BuildConfig.TESTER_MENU_ENABLED) {
apiConfigsManager.isInitialized
.filter { it }
.first() // wait until isInitialized becomes true
function()
} else {
function()
}
}
}
private fun prepareSelectedWalletFeedback() {
userWalletsListManager.selectedUserWallet
.distinctUntilChanged()
@ -134,19 +184,11 @@ internal class MainViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun updateAppCurrencies() {
viewModelScope.launch(dispatchers.main) {
fetchAppCurrenciesUseCase.invoke()
}
}
private fun fetchStakingTokens() {
viewModelScope.launch(dispatchers.main) {
private suspend fun fetchStakingTokens() {
fetchStakingTokensUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") }
.onRight { Timber.d("Staking token list was fetched successfully") }
}
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
@ -343,4 +385,20 @@ internal class MainViewModel @Inject constructor(
private fun initializeDeepLinks() {
deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope))
}
private suspend fun initPushNotifications() {
if (notificationsToggles.isNotificationsEnabled) {
getApplicationIdUseCase().onRight { applicationId ->
sendPushTokenUseCase(applicationId = applicationId)
associateWalletsWithApplicationId(applicationId = applicationId)
updateRemoteWalletsInfoUseCase(applicationId = applicationId)
}.onLeft { Timber.e(it.toString()) }
}
}
private fun associateWalletsWithApplicationId(applicationId: ApplicationId) {
subscribeOnWalletsUseCase().onEach { wallets ->
associateWalletsWithApplicationIdUseCase(applicationId, wallets)
}.launchIn(viewModelScope)
}
}

View file

@ -5,10 +5,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
@ -79,7 +79,7 @@ object TradeCryptoMiddleware {
val status = action.cryptoCurrencyStatus
val currency = status.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
val blockchain = Blockchain.fromId(currency.network.rawId)
val exchangeManager = store.state.globalState.exchangeManager
val topUrl = exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,

View file

@ -13,4 +13,10 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle
override fun getCardId(): String {
return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: ""
}
override fun getCardsPublicKeys(): Map<String, String> {
return userWalletsListManager.userWalletsSync.associate {
it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString()
}
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -44,7 +44,7 @@ internal class CryptoCurrencyConverter(
}
override fun convertBack(value: CryptoCurrency): Currency {
val blockchain = Blockchain.fromId(value.network.id.value)
val blockchain = Blockchain.fromId(value.network.rawId)
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) {
is CryptoCurrency.Coin -> Currency.Blockchain(

View file

@ -10,9 +10,9 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
@ -68,7 +68,7 @@ class CurrencyExchangeManager(
walletAddress: String,
isDarkTheme: Boolean,
): String? {
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
val blockchain = Blockchain.fromId(cryptoCurrency.network.rawId)
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val urlBuilder = getExchangeUrlBuilder(action)

View file

@ -12,9 +12,9 @@ import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.repository.CurrenciesRepository

View file

@ -2,8 +2,8 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

View file

@ -10,8 +10,8 @@ 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
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
@ -77,7 +77,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
val blockchain = Blockchain.fromId(cryptoCurrency.network.rawId)
val builder = Uri.Builder()
.scheme(ExchangeUrlBuilder.SCHEME)

View file

@ -10,8 +10,8 @@ import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
@ -133,7 +133,7 @@ class MoonPayService(
): String? {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException()
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
val blockchain = Blockchain.fromId(cryptoCurrency.network.rawId)
val supportedCurrency = blockchain.moonPaySupportedCurrency ?: return null
val moonpayCurrency = status?.availableForSell?.firstOrNull {
when (cryptoCurrency) {

View file

@ -22,6 +22,7 @@ import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.utils.ChildFactory
import com.tangem.tap.routing.utils.DeepLinkFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -34,6 +35,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val appRouterConfig: AppRouterConfig,
private val uiDependencies: UiDependencies,
private val wcRoutingComponentFactory: WcRoutingComponent.Factory,
private val deeplinkFactory: DeepLinkFactory,
) : RoutingComponent,
AppComponentContext by context,
SnackbarHandler {
@ -60,7 +62,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
stack.subscribe(lifecycle) { stack ->
val stackItems = stack.items.map { it.configuration }
wcRoutingComponent.onAppRouteChange(stack.active.configuration)
deeplinkFactory.checkRoutingReadiness(stack.active.configuration)
if (appRouterConfig.stack != stackItems) {
appRouterConfig.stack = stackItems
}

View file

@ -291,13 +291,13 @@ internal class ChildFactory @Inject constructor(
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
createComponentChild(
context = context,
params = Unit,
params = RedesignedWalletConnectComponent.Params(route.userWalletId),
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
createComponentChild(
context = context,
params = Unit,
params = WalletConnectComponent.Params(route.userWalletId),
componentFactory = walletConnectComponentFactory,
)
}

View file

@ -0,0 +1,142 @@
package com.tangem.tap.routing.utils
import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.transformLatest
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ActivityScoped
internal class DeepLinkFactory @Inject constructor(
private val onrampDeepLink: OnrampDeepLinkHandler.Factory,
private val sellDeepLink: SellDeepLinkHandler.Factory,
private val buyDeepLink: BuyDeepLinkHandler.Factory,
private val referralDeepLink: ReferralDeepLinkHandler.Factory,
private val walletConnectDeepLink: WalletConnectDeepLinkHandler.Factory,
private val walletDeepLink: WalletDeepLinkHandler.Factory,
private val tokenDetailsDeepLink: TokenDetailsDeepLinkHandler.Factory,
) {
private val permittedAppRoute = MutableStateFlow(false)
private var lastDeepLink: Uri? = null
private val deepLinkHandlerJobHolder = JobHolder()
@OptIn(ExperimentalCoroutinesApi::class)
fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope) {
lastDeepLink = deeplinkUri
Timber.i(
"""
Received deep link intent
|- Received URI: $deeplinkUri
""".trimIndent(),
)
permittedAppRoute
.transformLatest<Boolean, Unit> { isPermitted ->
if (isPermitted) {
lastDeepLink?.let {
launchDeepLink(it, coroutineScope)
}
lastDeepLink = null
}
}
.launchIn(coroutineScope)
.saveIn(deepLinkHandlerJobHolder)
}
/**
* Check if app is ready to handle deeplink
*/
fun checkRoutingReadiness(appRoute: AppRoute) {
permittedAppRoute.value = when (appRoute) {
AppRoute.Initial,
AppRoute.Home,
is AppRoute.Welcome,
is AppRoute.Disclaimer,
is AppRoute.Stories,
is AppRoute.Onboarding,
-> false
else -> true
}
}
private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope) {
when (deeplinkUri.scheme) {
DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope)
DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri)
else -> {
Timber.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri
""".trimIndent(),
)
}
}
}
private fun handleTangemDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) {
val queryParams = getQueryParams(deeplinkUri)
when (deeplinkUri.host) {
DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Sell.host -> sellDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Buy.host -> buyDeepLink.create(coroutineScope)
DeepLinkRoute.Referral.host -> referralDeepLink.create()
DeepLinkRoute.Wallet.host -> walletDeepLink.create()
DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create(coroutineScope, queryParams)
else -> {
Timber.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri
|- With params: $queryParams
""".trimIndent(),
)
}
}
}
private fun getQueryParams(uri: Uri): Map<String, String> {
val params = mutableMapOf<String, String>()
uri.queryParameterNames.forEach { paramName ->
val paramValue = uri.getQueryParameter(paramName)
if (paramName.validate() && paramValue?.validate() == true) {
params[paramName] = paramValue
}
}
return params
}
/**
* Check for malicious symbol in uri part
*/
private fun String.validate(): Boolean {
val regex = DEEPLINK_VALIDATION_REGEX.toRegex()
return !regex.containsMatchIn(this)
}
private companion object {
const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]"
}
}

View file

@ -0,0 +1,324 @@
package com.tangem.tap.routing.utils
import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
class DeepLinkFactoryTest {
private val onrampDeepLinkFactory = mockk<OnrampDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val sellDeepLinkFactory = mockk<SellDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val buyDeepLinkFactory = mockk<BuyDeepLinkHandler.Factory>(relaxed = true) {
every { create(any()) } returns mockk()
}
private val referralDeepLinkFactory = mockk<ReferralDeepLinkHandler.Factory>(relaxed = true) {
every { create() } returns mockk()
}
private val walletConnectDeepLinkFactory = mockk<WalletConnectDeepLinkHandler.Factory>(relaxed = true) {
every { create(any()) } returns mockk()
}
private val walletDeepLinkFactory = mockk<WalletDeepLinkHandler.Factory>(relaxed = true) {
every { create() } returns mockk()
}
private val tokenDetailsDeepLinkFactory = mockk<TokenDetailsDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val mockedUri = mockk<Uri>(relaxed = true)
private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
private val deepLinkFactory = DeepLinkFactory(
onrampDeepLinkFactory,
sellDeepLinkFactory,
buyDeepLinkFactory,
referralDeepLinkFactory,
walletConnectDeepLinkFactory,
walletDeepLinkFactory,
tokenDetailsDeepLinkFactory,
)
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
testDispatcher = StandardTestDispatcher()
testScope = TestScope(testDispatcher)
Dispatchers.setMain(testDispatcher)
every { mockedUri.path } returns "/path"
every { mockedUri.toString() } returns "https://example.com/path?query=param"
every { mockedUri.authority } returns "example.com"
every { mockedUri.port } returns 443 // Default HTTPS port
every { mockedUri.fragment } returns null // No fragment in this URI
Timber.uprootAll() // Disable Timber logging for tests
}
@OptIn(ExperimentalCoroutinesApi::class)
@After
fun tearDown() {
// Reset the main dispatcher
Dispatchers.resetMain()
// Clean up test coroutines
testScope.cancel()
}
@Test
fun `handleDeeplink stores uri and launches when permitted`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.host } returns "onramp"
every { mockedUri.query } returns "param=value"
every { mockedUri.queryParameterNames } returns setOf("param")
every { mockedUri.getQueryParameter("param") } returns "value"
// Set permittedAppRoute to true
deepLinkFactory.handleDeeplink(mockedUri, testScope)
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
advanceUntilIdle()
// Verify onramp handler was called
verify {
onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value")))
}
}
@Test
fun `handleDeeplink does not launch when not permitted`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.host } returns "onramp"
every { mockedUri.query } returns "param=value"
every { mockedUri.queryParameterNames } returns setOf("param")
every { mockedUri.getQueryParameter("param") } returns "value"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
deepLinkFactory.checkRoutingReadiness(AppRoute.Initial)
advanceUntilIdle()
// Verify no handler was called
verify(inverse = true) { onrampDeepLinkFactory.create(any(), any()) }
}
@Test
fun `launchDeepLink handles tangem scheme correctly`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.host } returns "onramp"
every { mockedUri.query } returns "param=value"
every { mockedUri.queryParameterNames } returns setOf("param")
every { mockedUri.getQueryParameter("param") } returns "value"
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify {
onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value")))
}
}
@Test
fun `launchDeepLink handles wc scheme correctly`() = runTest {
every { mockedUri.scheme } returns "wc"
every { mockedUri.host } returns ""
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify {
walletConnectDeepLinkFactory.create(eq(mockedUri))
}
}
@Test
fun `launchDeepLink ignores unknown scheme`() = runTest {
every { mockedUri.scheme } returns "https"
every { mockedUri.host } returns "example.com"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify(inverse = true) {
onrampDeepLinkFactory.create(any(), any())
sellDeepLinkFactory.create(any(), any())
buyDeepLinkFactory.create(any())
referralDeepLinkFactory.create()
walletConnectDeepLinkFactory.create(any())
walletDeepLinkFactory.create()
tokenDetailsDeepLinkFactory.create(any(), any())
}
}
@Test
fun `handleTangemDeepLinks routes to correct handler`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.query } returns "param=value"
every { mockedUri.queryParameterNames } returns setOf("param")
every { mockedUri.getQueryParameter("param") } returns "value"
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
// Test Onramp
every { mockedUri.host } returns "onramp"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Test Sell
every { mockedUri.host } returns "redirect_sell"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { sellDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Test Token Details
every { mockedUri.host } returns "token"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { tokenDetailsDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Reset params
every { mockedUri.queryParameterNames } returns emptySet()
every { mockedUri.getQueryParameter(any()) } returns ""
// Test Buy
every { mockedUri.host } returns "redirect"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { buyDeepLinkFactory.create(eq(testScope)) }
// Test Referral
every { mockedUri.host } returns "referral"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { referralDeepLinkFactory.create() }
// Test Wallet
every { mockedUri.host } returns "main"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { walletDeepLinkFactory.create() }
}
@Test
fun `handleTangemDeepLinks incorrect host`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.host } returns "unknown"
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify(inverse = true) {
onrampDeepLinkFactory.create(any(), any())
sellDeepLinkFactory.create(any(), any())
buyDeepLinkFactory.create(any())
referralDeepLinkFactory.create()
walletConnectDeepLinkFactory.create(any())
walletDeepLinkFactory.create()
tokenDetailsDeepLinkFactory.create(any(), any())
}
}
@Test
fun `getParams filters malicious parameters`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.host } returns "onramp"
every { mockedUri.query } returns "safe=ok&malicious=%3Cscript%3E&quote=O%27Brien"
every { mockedUri.queryParameterNames } returns setOf("safe", "malicious", "quote")
every { mockedUri.getQueryParameter("safe") } returns "ok"
every { mockedUri.getQueryParameter("malicious") } returns "<script>"
every { mockedUri.getQueryParameter("quote") } returns "O'Brien"
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("safe" to "ok"))) }
}
@Test
fun `validate detects malicious characters`() = runTest {
every { mockedUri.scheme } returns "tangem"
every { mockedUri.host } returns "onramp"
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
every { mockedUri.query } returns "safe=ok"
every { mockedUri.queryParameterNames } returns setOf("safe")
every { mockedUri.getQueryParameter("safe") } returns "ok"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("safe" to "ok"))) }
every { mockedUri.query } returns "param123=ok"
every { mockedUri.queryParameterNames } returns setOf("param123")
every { mockedUri.getQueryParameter("param123") } returns "ok"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param123" to "ok"))) }
every { mockedUri.query } returns "unsafe=<script>"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "<script>"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=O'Brien"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "O'Brien"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=test;"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "test;"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=test+attack"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "test+attack"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=test\\path"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "test\\path"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
}
}

View file

@ -12,6 +12,7 @@ android {
dependencies {
/* Core */
implementation(projects.core.decompose)
implementation(projects.core.configToggles)
/* Domain */
implementation(projects.domain.qrScanning.models)

View file

@ -7,10 +7,10 @@ import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@ -122,7 +122,7 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions")
data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions")
@Serializable
data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") {

View file

@ -0,0 +1,35 @@
package com.tangem.common.routing
sealed class DeepLinkRoute {
abstract val host: String
data object Onramp : DeepLinkRoute() {
override val host: String = "onramp"
}
data object Sell : DeepLinkRoute() {
override val host: String = "redirect_sell"
}
data object Buy : DeepLinkRoute() {
override val host: String = "redirect"
}
data object Referral : DeepLinkRoute() {
override val host: String = "referral"
}
data object Wallet : DeepLinkRoute() {
override val host: String = "main"
}
data object TokenDetails : DeepLinkRoute() {
override val host: String = "token"
}
}
enum class DeepLinkScheme(val scheme: String) {
Tangem(scheme = "tangem"),
WalletConnect(scheme = "wc"),
}

View file

@ -0,0 +1,11 @@
package com.tangem.common.routing
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
class RoutingFeatureToggle(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isDeepLinkNavigationEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "DEEPLINK_NAVIGATION_ENABLED")
}

View file

@ -19,6 +19,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
@ -30,4 +31,6 @@ dependencies {
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
implementation(deps.test.junit5)
}

View file

@ -2,9 +2,9 @@ package com.tangem.common.test.domain.network
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
import java.math.BigDecimal
/**
@ -14,36 +14,41 @@ object MockNetworkStatusFactory {
private val defaultNetwork = MockCryptoCurrencyFactory().ethereum.network
fun createVerified(network: Network = defaultNetwork): NetworkStatus {
fun createVerified(
network: Network = defaultNetwork,
source: StatusSource = StatusSource.ACTUAL,
transform: (NetworkStatus.Verified) -> NetworkStatus.Verified = { it },
): NetworkStatus {
return NetworkStatus(
network = network,
value = NetworkStatus.Verified(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x123",
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),
amounts = mapOf(),
pendingTransactions = mapOf(),
source = StatusSource.ACTUAL,
),
source = source,
)
.let(transform),
)
}
fun createNoAccount(network: Network = defaultNetwork): NetworkStatus {
fun createNoAccount(network: Network = defaultNetwork, source: StatusSource = StatusSource.ACTUAL): NetworkStatus {
return NetworkStatus(
network = network,
value = NetworkStatus.NoAccount(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x123",
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),
amountToCreateAccount = BigDecimal.ONE,
errorMessage = "",
source = StatusSource.ACTUAL,
source = source,
),
)
}

View file

@ -11,9 +11,9 @@ import com.tangem.data.common.currency.getNetworkDerivationPath
import com.tangem.data.common.currency.getNetworkStandardType
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
/**
[REDACTED_AUTHOR]
@ -47,16 +47,18 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
}
fun createCoin(blockchain: Blockchain): CryptoCurrency {
val network = Network(
id = Network.ID(blockchain.id),
backendId = blockchain.toNetworkId(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(
val derivationPath = getNetworkDerivationPath(
blockchain = blockchain,
extraDerivationPath = null,
cardDerivationStyleProvider = scanResponse.derivationStyleProvider,
),
)
val network = Network(
id = Network.ID(blockchain.id, derivationPath),
backendId = blockchain.toNetworkId(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = derivationPath,
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
@ -69,6 +71,12 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
// Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood
fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency {
val derivationPath = Network.DerivationPath.Custom(
value = derivationBlockchain.derivationPath(
scanResponse.derivationStyleProvider.getDerivationStyle(),
)!!.rawPath,
)
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
@ -76,15 +84,11 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
suffix = CryptoCurrency.ID.Suffix.RawID(blockchain.id),
),
network = Network(
id = Network.ID(value = blockchain.id),
id = Network.ID(value = blockchain.id, derivationPath),
backendId = "NEVER-MIND",
name = blockchain.fullName,
currencySymbol = "NEVER-MIND",
derivationPath = Network.DerivationPath.Custom(
value = derivationBlockchain.derivationPath(
scanResponse.derivationStyleProvider.getDerivationStyle(),
)!!.rawPath,
),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = true,
@ -102,7 +106,13 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
fun createToken(blockchain: Blockchain): CryptoCurrency {
return factory.createToken(
sdkToken = Token(symbol = "NEVER-MIND", contractAddress = "NEVER-MIND", decimals = 8),
sdkToken = Token(
name = "NEVER-MIND",
symbol = "NEVER-MIND",
contractAddress = "NEVER-MIND",
decimals = 8,
id = "NEVER-MIND",
),
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,

View file

@ -0,0 +1,64 @@
package com.tangem.common.test.domain.walletmanager
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.walletmanager.model.Address
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class MockUpdateWalletManagerResultFactory {
fun createUnreachable(): UpdateWalletManagerResult {
return UpdateWalletManagerResult.Unreachable(selectedAddress = null, addresses = null)
}
fun createUnreachableWithAddress(): UpdateWalletManagerResult {
return UpdateWalletManagerResult.Unreachable(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)),
)
}
fun createNoAccount(): UpdateWalletManagerResult {
return UpdateWalletManagerResult.NoAccount(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)),
amountToCreateAccount = BigDecimal.ONE,
errorMessage = "",
)
}
fun createVerified(): UpdateWalletManagerResult {
return UpdateWalletManagerResult.Verified(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)),
currenciesAmounts = setOf(
CryptoCurrencyAmount.Coin(value = BigDecimal.ONE),
),
currentTransactions = setOf(
CryptoCurrencyTransaction.Coin(txInfo),
),
)
}
private companion object {
val txInfo = TxInfo(
txHash = "erroribus",
timestampInMillis = 2771,
isOutgoing = false,
destinationType = TxInfo.DestinationType.Single(
addressType = TxInfo.AddressType.User(address = "0x1"),
),
sourceType = TxInfo.SourceType.Single(address = "0x2"),
interactionAddressType = null,
status = TxInfo.TransactionStatus.Confirmed,
type = TxInfo.TransactionType.Transfer,
amount = BigDecimal.ONE,
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.common.test.utils
import org.junit.jupiter.params.provider.MethodSource
/**
[REDACTED_AUTHOR]
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MethodSource("provideTestModels")
annotation class ProvideTestModels

View file

@ -4,8 +4,8 @@ import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

View file

@ -5,9 +5,9 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
private const val DSC_ADDRESS_NAME = "DSC"
private const val DEL_ADDRESS_NAME = "Main"
@ -59,7 +59,7 @@ private fun Set<NetworkAddress.Address>.mapToAddressModels(name: TextReference,
}
private fun Network.getAddressDisplayName(addressType: NetworkAddress.Address.Type): TextReference {
return when (id.value) {
return when (rawId) {
"decimal", "decimal/test" -> {
when (addressType) {
NetworkAddress.Address.Type.Primary -> stringReference(value = DEL_ADDRESS_NAME)

View file

@ -6,8 +6,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -41,9 +41,11 @@ data class TokenReceiveBottomSheetConfig(
@Immutable
sealed class Asset {
abstract val displaySymbol: TextReference
data class Currency(val name: String, val symbol: String) : Asset() {
override val displaySymbol = stringReference(symbol)
}
data object NFT : Asset() {
override val displaySymbol = resourceReference(R.string.common_nft)
}

View file

@ -4,14 +4,13 @@ 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.NotificationUM.Warning
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.uncapped
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -460,7 +459,7 @@ object NotificationsFactory {
onCloseClick: (Class<out NotificationUM>) -> Unit,
) {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.rawId)
val threshold = getTezosThreshold()
val isTotalBalance = enteredAmountValue >= balance && balance > threshold
if (!ignoreAmountReduce && isTotalBalance && isTezos) {

View file

@ -141,7 +141,7 @@ class TokenItemStateConverter(
}
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data)
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.id.value).orZero()
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero()
private fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState {
return when (val value = currencyStatus.value) {

View file

@ -22,4 +22,7 @@ dependencies {
/** Core shouldn't depend on core, but in case with utils and logging its necessary */
implementation(projects.core.utils)
/** For calculating user id hash */
implementation(tangemDeps.card.core)
}

View file

@ -1,5 +1,7 @@
package com.tangem.core.analytics
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.*
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
@ -19,7 +21,8 @@ interface GlobalAnalyticsEventHandler :
AnalyticsFilterHolder,
ParamsInterceptorHolder,
AnalyticsErrorHandler,
AnalyticsExceptionHandler
AnalyticsExceptionHandler,
AnalyticsUserIdHandler
object Analytics : GlobalAnalyticsEventHandler {
@ -57,6 +60,26 @@ object Analytics : GlobalAnalyticsEventHandler {
return paramsInterceptors.remove(interceptorId)
}
override fun setUserId(userId: String) {
analyticsScope.launch {
val userIdHash = userId.calculateSha256().toHexString()
analyticsMutex.withLock {
analyticsHandlers.filterIsInstance<AnalyticsUserIdHandler>()
.forEach { handler -> handler.setUserId(userIdHash) }
}
}
}
override fun clearUserId() {
analyticsScope.launch {
analyticsMutex.withLock {
analyticsHandlers.filterIsInstance<AnalyticsUserIdHandler>()
.forEach { handler -> handler.clearUserId() }
}
}
}
override fun send(event: AnalyticsEvent) {
analyticsScope.launch {
event.params = applyParamsInterceptors(event)

View file

@ -18,6 +18,11 @@ interface AnalyticsExceptionHandler {
fun sendException(event: ExceptionAnalyticsEvent)
}
interface AnalyticsUserIdHandler {
fun setUserId(userId: String)
fun clearUserId()
}
interface AnalyticsHandler : AnalyticsEventHandler {
fun id(): String

View file

@ -0,0 +1,8 @@
package com.tangem.core.analytics.api
interface UserIdHolder {
fun setUserId(userId: String)
fun clearUserId()
}

View file

@ -51,10 +51,6 @@
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "QUOTES_LOADING_REFACTORING_ENABLED",
"version": "5.24.0"
@ -62,5 +58,13 @@
{
"name": "STAKING_LOADING_REFACTORING_ENABLED",
"version": "5.25.0"
},
{
"name": "DEEPLINK_NAVIGATION_ENABLED",
"version": "5.25.0"
},
{
"name": "PUSH_NOTIFICATIONS_ENABLED",
"version": "undefined"
}
]

View file

@ -23,6 +23,7 @@ dependencies {
/** Project */
implementation(projects.core.analytics)
implementation(projects.core.utils)
implementation(projects.core.res)
implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.core)

View file

@ -11,4 +11,9 @@ interface AuthProvider {
fun getCardPublicKey(): String
fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import kotlinx.coroutines.flow.StateFlow
/**
* Api configs manager
@ -10,8 +11,11 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
*/
interface ApiConfigsManager {
/** Flag that determines whether the manager is initialized */
val isInitialized: StateFlow<Boolean>
/** Initialize resources */
fun initialize() {}
fun initialize()
/** Get environment config by [id] */
fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig

View file

@ -29,7 +29,13 @@ internal class DevApiConfigsManager(
private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment })
override val isInitialized: StateFlow<Boolean> get() = _isInitialized.asStateFlow()
private val _isInitialized = MutableStateFlow(value = false)
override fun initialize() {
_isInitialized.value = false
// We can't use appPreferencesStore.getObjectMap as base flow,
// because we should keep possibility to work with configs synchronous.
// See [getBaseUrl]
@ -42,8 +48,12 @@ internal class DevApiConfigsManager(
savedEnvironments[config.id.name] ?: currentEnvironment
}
}
if (!_isInitialized.value) {
_isInitialized.value = true
}
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.main))
}
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
}
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {

View file

@ -3,6 +3,8 @@ package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Implementation of [ApiConfigsManager] in PROD environment
@ -13,6 +15,10 @@ internal class ProdApiConfigsManager(
private val apiConfigs: ApiConfigs,
) : ApiConfigsManager {
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
override fun initialize() = Unit
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val config = apiConfigs.firstOrNull { it.id == id }
?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")

View file

@ -154,7 +154,7 @@ interface TangemTechApi {
suspend fun updatePushTokenForApplicationId(
@Path("application_id") applicationId: String,
@Body body: NotificationApplicationCreateBody,
): ApiResponse<String>
): ApiResponse<Unit>
@PATCH("user-wallets/wallets/{wallet_id}/notify")
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@ -172,6 +172,9 @@ interface TangemTechApi {
@GET("user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
@GET("user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
companion object {

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardInfoBody(
@Json(name = "card_id") val cardId: String,
@Json(name = "card_public_key") val cardPublicKey: String,
)

View file

@ -9,6 +9,7 @@ data class UserTokensResponse(
@Json(name = "version") val version: Int = 0,
@Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType,
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
@Json(name = "tokens") val tokens: List<Token> = emptyList(),
) {
@ -21,6 +22,7 @@ data class UserTokensResponse(
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "contractAddress") val contractAddress: String?,
@Json(name = "addresses") val addresses: List<String>? = null,
) {
override fun equals(other: Any?): Boolean {
val otherToken = other as? Token ?: return false

View file

@ -6,4 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletIdBody(
@Json(name = "id") val walletId: String,
@Json(name = "name") val name: String,
@Json(name = "cards") val cards: List<CardInfoBody>,
)

View file

@ -25,16 +25,18 @@ interface TangemVisaApi {
// region: auth
@POST("v1/auth/challenge")
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse
suspend fun generateNonceByCardWallet(
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VisaErrorResponse(
@Json(name = "error") val error: Error,
) {
@JsonClass(generateAdapter = true)
data class Error(
@Json(name = "code") val code: Int,
)
}

View file

@ -43,6 +43,10 @@ internal object NetworkModule {
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
private val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
)
@Provides
@Singleton
fun provideApiConfigManager(
@ -317,7 +321,7 @@ internal object NetworkModule {
}
b
}
.addLoggers(context)
.addLoggers(context = context, id = id)
.clientBuilder()
.build(),
)
@ -325,6 +329,12 @@ internal object NetworkModule {
.create(T::class.java)
}
private fun OkHttpClient.Builder.addLoggers(context: Context, id: ApiConfig.ID): OkHttpClient.Builder {
if (id in excludedApiForLogging) return this
return addLoggers(context)
}
private data class Timeouts(
val callTimeoutSeconds: Long? = null,
val connectTimeoutSeconds: Long? = null,

View file

@ -1,57 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.DefaultNetworksStatusesStore
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NetworksStatusesStoreModule {
@Singleton
@Provides
fun providePersistenceNetworksStatusesStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): DataStore<Map<String, Set<NetworkStatusDM>>> {
return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<NetworkStatusDM>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
@Singleton
@Provides
fun provideNetworksStatusesStore(
persistenceNetworksStatusesStore: DataStore<Map<String, Set<NetworkStatusDM>>>,
): NetworksStatusesStore {
return DefaultNetworksStatusesStore(
runtimeDataStore = RuntimeDataStore(),
persistenceDataStore = persistenceNetworksStatusesStore,
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di.local
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.DefaultUserTokensResponseStore
import com.tangem.datasource.local.token.UserTokensResponseStore
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 LocalTokenModule {
@Provides
@Singleton
fun provideUserTokensResponseStore(appPreferencesStore: AppPreferencesStore): UserTokensResponseStore {
return DefaultUserTokensResponseStore(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -43,6 +43,7 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
bittensorOnfinalityApiKey = value.bittensorOnfinalityKey,
koinosProApiKey = value.koinosProApiKey,
alephiumApiKey = value.alephiumTangemApiKey,
moralisApiKey = value.moralisApiKey,
)
}

View file

@ -1,201 +0,0 @@
package com.tangem.datasource.local.network
import androidx.datastore.core.DataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.converter.NetworkDerivationPathConverter
import com.tangem.datasource.local.network.converter.NetworkStatusConverter
import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
private typealias NetworkStatusesByWalletId = Map<String, Set<NetworkStatusDM>>
internal class DefaultNetworksStatusesStore(
private val runtimeDataStore: RuntimeDataStore<Set<NetworkStatus>>,
private val persistenceDataStore: DataStore<NetworkStatusesByWalletId>,
) : NetworksStatusesStore {
private val mutex = Mutex()
override fun get(key: UserWalletId): Flow<Set<NetworkStatus>> {
return runtimeDataStore.get(provideStringKey(key))
}
override fun get(key: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>> = channelFlow {
val cachedStatuses = persistenceDataStore.data.firstOrNull()
?.get(key.stringValue)
?.mapNotNullTo(mutableSetOf()) { cached ->
val network = networks.firstOrNull {
it.id == cached.networkId &&
it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath)
}
?: return@mapNotNullTo null
NetworkStatusConverter(network = network, isCached = true).convert(value = cached)
}
.orEmpty()
if (cachedStatuses.isNotEmpty()) {
send(cachedStatuses)
}
runtimeDataStore.get(provideStringKey(key))
.onEach { runtimeStatuses ->
val mergedStatuses = mergeStatuses(
networks = networks,
cachedStatuses = cachedStatuses,
runtimeStatuses = runtimeStatuses,
)
send(mergedStatuses)
}
.launchIn(scope = this)
}
override suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>? {
val runtimeStatuses = runtimeDataStore.getSyncOrNull(key = provideStringKey(key)) ?: return null
val networks = runtimeStatuses.map(NetworkStatus::network).toSet()
val cachedStatuses = persistenceDataStore.data.firstOrNull()
?.get(key.stringValue)
?.mapNotNullTo(mutableSetOf()) { cached ->
val network = networks.firstOrNull {
it.id == cached.networkId &&
it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath)
}
?: return@mapNotNullTo null
NetworkStatusConverter(network = network, isCached = true).convert(value = cached)
}
.orEmpty()
return mergeStatuses(
networks = networks,
cachedStatuses = cachedStatuses,
runtimeStatuses = runtimeStatuses,
)
}
override suspend fun store(key: UserWalletId, value: NetworkStatus) {
storeAll(key = key, values = setOf(value))
}
override suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>) {
mutex.withLock {
coroutineScope {
launch { storeInRuntimeStore(key = key, statuses = values) }
launch { storeInPersistenceStore(userWalletId = key, statuses = values) }
}
}
}
override suspend fun refresh(key: UserWalletId, networks: Set<Network>) {
mutex.withLock {
val currentStatuses = getSyncOrNull(key).orEmpty()
storeInRuntimeStore(
key = key,
statuses = networks.mapNotNullTo(hashSetOf()) { network ->
val status = currentStatuses.firstOrNull {
it.network.id == network.id && it.network.derivationPath == network.derivationPath
} ?: return@mapNotNullTo null
status.copy(
value = status.value.copySealed(source = StatusSource.CACHE),
)
},
)
}
}
/**
* Merge [cachedStatuses] with [runtimeStatuses]
* The resulting set contains statuses from both sets.
* If a status with the same network is in both sets, the status from [runtimeStatuses] is used.
*/
private fun mergeStatuses(
networks: Set<Network>,
cachedStatuses: Set<NetworkStatus>,
runtimeStatuses: Set<NetworkStatus>,
): Set<NetworkStatus> {
return networks.mapNotNullTo(hashSetOf()) { network ->
val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network }
if (runtimeStatus == null) {
getCachedStatusIfPossible(
cachedStatuses = cachedStatuses,
network = network,
source = StatusSource.CACHE,
)
} else if (runtimeStatus.value is NetworkStatus.Unreachable) {
getCachedStatusIfPossible(
cachedStatuses = cachedStatuses,
network = network,
source = StatusSource.ONLY_CACHE,
)
?: runtimeStatus
} else {
runtimeStatus
}
}
}
private fun getCachedStatusIfPossible(
cachedStatuses: Set<NetworkStatus>,
network: Network,
source: StatusSource,
): NetworkStatus? {
val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null
val updatedCachedStatus = when (val status = cached.value) {
is NetworkStatus.NoAccount -> status.copy(source = source)
is NetworkStatus.Verified -> status.copy(source = source)
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> null
}
return if (updatedCachedStatus != null) {
cached.copy(value = updatedCachedStatus)
} else {
null
}
}
private suspend fun storeInRuntimeStore(key: UserWalletId, statuses: Set<NetworkStatus>) {
val updatedValues = getSyncOrNull(key).orEmpty()
.addOrReplace(items = statuses) { prev, new -> prev.network == new.network }
runtimeDataStore.store(key = provideStringKey(key), value = updatedValues)
}
private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, statuses: Set<NetworkStatus>) {
// Converter will return null if the network status is not supported
val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet()
persistenceDataStore.updateData { storedStatuses ->
storedStatuses.toMutableMap().apply {
val updatedValues = this[userWalletId.stringValue].orEmpty()
.addOrReplace(newStatuses) { prev, new ->
prev.networkId == new.networkId && prev.derivationPath == new.derivationPath
}
this[userWalletId.stringValue] = updatedValues
}
}
}
private fun provideStringKey(key: UserWalletId): String {
return "network_statuses_${key.stringValue}"
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.local.network
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface NetworksStatusesStore {
fun get(key: UserWalletId): Flow<Set<NetworkStatus>>
fun get(key: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>?
suspend fun store(key: UserWalletId, value: NetworkStatus)
suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>)
suspend fun refresh(key: UserWalletId, networks: Set<Network>)
}

View file

@ -1,60 +0,0 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.utils.converter.TwoWayConverter
import timber.log.Timber
/**
* Converter from [Set<NetworkStatusDM.Address>] to [NetworkAddress] and vice versa
*
[REDACTED_AUTHOR]
*/
class NetworkAddressConverter(
private val selectedAddress: String,
) : TwoWayConverter<Set<NetworkStatusDM.Address>, NetworkAddress> {
override fun convert(value: Set<NetworkStatusDM.Address>): NetworkAddress {
val defaultAddress = value
.firstOrNull { it.value == selectedAddress }
?.let(::toNetworkAddress)
requireNotNull(defaultAddress) { "Selected address must not be null" }
return if (value.size != 1) {
NetworkAddress.Selectable(
defaultAddress = defaultAddress,
availableAddresses = value.mapTo(destination = hashSetOf(), transform = ::toNetworkAddress),
)
} else {
NetworkAddress.Single(defaultAddress = defaultAddress)
}
}
override fun convertBack(value: NetworkAddress): Set<NetworkStatusDM.Address> {
return value.availableAddresses
.map { address ->
NetworkStatusDM.Address(
value = address.value,
type = when (address.type) {
NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary
NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary
},
)
}
.toSet()
}
private fun toNetworkAddress(address: NetworkStatusDM.Address): NetworkAddress.Address {
val type = when (address.type) {
NetworkStatusDM.Address.Type.Primary -> NetworkAddress.Address.Type.Primary
NetworkStatusDM.Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary
}
if (address.value.isBlank()) {
Timber.w("Address value is blank")
}
return NetworkAddress.Address(value = address.value, type = type)
}
}

View file

@ -1,47 +0,0 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.utils.converter.Converter
/**
* Converter from [NetworkStatusDM] to [NetworkStatus]
*
* @property network network
* @property isCached flag that determines whether the status is a cache
*
[REDACTED_AUTHOR]
*/
internal class NetworkStatusConverter(
private val network: Network,
private val isCached: Boolean,
) : Converter<NetworkStatusDM, NetworkStatus> {
override fun convert(value: NetworkStatusDM): NetworkStatus {
val address = NetworkAddressConverter(selectedAddress = value.selectedAddress)
.convert(value = value.availableAddresses)
val status = when (value) {
is NetworkStatusDM.Verified -> {
NetworkStatus.Verified(
address = address,
amounts = NetworkAmountsConverter.convert(value = value.amounts),
pendingTransactions = mapOf(),
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
}
is NetworkStatusDM.NoAccount -> {
NetworkStatus.NoAccount(
address = address,
amountToCreateAccount = value.amountToCreateAccount,
errorMessage = value.errorMessage,
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
}
}
return NetworkStatus(network = network, value = status)
}
}

View file

@ -2,29 +2,63 @@ package com.tangem.datasource.local.network.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.tokens.model.Network
import com.tangem.datasource.local.network.entity.NetworkStatusDM.NoAccount
import com.tangem.datasource.local.network.entity.NetworkStatusDM.Verified
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal
/**
* Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount].
*
* @see [com.tangem.domain.tokens.model.NetworkStatus]
*/
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
sealed interface NetworkStatusDM {
val networkId: Network.ID
/** Network id */
val networkId: ID
/** Derivation path */
val derivationPath: DerivationPath
/** Selected address */
val selectedAddress: String
/** Available address */
val availableAddresses: Set<Address>
/**
* Verified
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amounts amounts
*/
@NameLabel("amounts")
data class Verified(
@Json(name = "network_id") override val networkId: Network.ID,
@Json(name = "network_id") override val networkId: ID,
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
) : NetworkStatusDM
/**
* No account
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amountToCreateAccount amount to create account
* @property errorMessage error message
*/
@NameLabel("amount_to_create_account")
data class NoAccount(
@Json(name = "network_id") override val networkId: Network.ID,
@Json(name = "network_id") override val networkId: ID,
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@ -32,6 +66,11 @@ sealed interface NetworkStatusDM {
@Json(name = "error_message") val errorMessage: String,
) : NetworkStatusDM
@JsonClass(generateAdapter = true)
data class ID(
@Json(name = "value") val value: String,
)
@JsonClass(generateAdapter = true)
data class DerivationPath(
@Json(name = "value") val value: String,

View file

@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft
import androidx.datastore.core.DataStore
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.local.nft.custom.NFTPriceId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
internal class DefaultNFTPersistenceStore(
private val collectionsPersistenceStore: DataStore<List<NFTCollection>>,
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
private val pricesPersistenceStore: DataStore<List<NFTPriceId>>,
) : NFTPersistenceStore {
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore(
}
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTAsset.SalePrice?> = pricesPersistenceStore.data
.map { it[assetId] }
.map { data -> data.associate { it.assetId to it.price }[assetId] }
override suspend fun getSalePricesSync(): Map<NFTAsset.Identifier, NFTAsset.SalePrice>? = pricesPersistenceStore
.data
.firstOrNull()
?.associate { it.assetId to it.price }
override suspend fun saveCollections(collections: List<NFTCollection>) {
collectionsPersistenceStore.updateData {
@ -41,9 +43,14 @@ internal class DefaultNFTPersistenceStore(
override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) {
pricesPersistenceStore.updateData {
it.toMutableMap().apply { this[assetId] = salePrice }
it.toMutableList() + NFTPriceId(assetId = assetId, price = salePrice)
}
}
override suspend fun clear() {
collectionsPersistenceStore.updateData { emptyList() }
pricesPersistenceStore.updateData { emptyList() }
}
private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier) = assets.firstOrNull { it.identifier == assetId }
}

View file

@ -2,11 +2,11 @@ package com.tangem.datasource.local.nft
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.Network
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.domain.tokens.model.Network
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
@ -71,6 +71,16 @@ internal class DefaultNFTRuntimeStore(
}
}
override suspend fun clear() {
collectionsRuntimeStore.store(
NFTCollections(
network = network,
content = NFTCollections.Content.Collections(null, StatusSource.ONLY_CACHE),
),
)
pricesRuntimeStore.store(emptyMap())
}
private fun NFTCollections.getCollection(collectionId: NFTCollection.Identifier): NFTCollection? =
(content as? NFTCollections.Content.Collections)
?.collections
@ -105,7 +115,6 @@ internal class DefaultNFTRuntimeStore(
-> assets
is NFTCollection.Assets.Value -> assets.copy(
items = assets.items
.filter { !it.name.isNullOrEmpty() }
.sortedBy { it.name }
.map { asset ->
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
@ -122,7 +131,7 @@ internal class DefaultNFTRuntimeStore(
)
}
?.filter { it.count > 0 }
?.sortedBy { it.name },
?.sortedBy { it.name?.lowercase() },
source = this.source,
)

View file

@ -18,4 +18,6 @@ interface NFTPersistenceStore {
suspend fun saveCollections(collections: List<NFTCollection>)
suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice)
suspend fun clear()
}

View file

@ -5,13 +5,12 @@ import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.nft.custom.NFTPriceId
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.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
@ -48,8 +47,8 @@ class NFTPersistenceStoreFactory @Inject constructor(
// 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(),
types = listTypes<NFTPriceId>(),
defaultValue = emptyList(),
),
)
}
@ -65,7 +64,7 @@ class NFTPersistenceStoreFactory @Inject constructor(
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
private fun Network.ID.formatted(): String = value
private fun Network.ID.formatted(): String = rawId.value
.filter(Char::isLetterOrDigit)
.lowercase()

View file

@ -23,4 +23,6 @@ interface NFTRuntimeStore {
suspend fun saveCollections(collections: NFTCollections)
suspend fun saveSalePrice(salePrice: NFTSalePrice)
suspend fun clear()
}

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.nft
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import javax.inject.Inject
import javax.inject.Singleton

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