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 # Google services
app/src/debug/google-services.json
app/src/internal/google-services.json
app/src/external/google-services.json
# Gradle generated files # Gradle generated files
.gradle .gradle

View file

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

View file

@ -128,7 +128,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="*" android:host="onramp"
android:scheme="tangem" /> android:scheme="tangem" />
</intent-filter> </intent-filter>
@ -150,7 +150,29 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <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" /> android:scheme="tangem" />
</intent-filter> </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.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory 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.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
@ -142,4 +143,6 @@ interface ApplicationEntryPoint {
fun getOnlineCardVerifier(): OnlineCardVerifier fun getOnlineCardVerifier(): OnlineCardVerifier
fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory 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.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.RoutingFeatureToggle
import com.tangem.common.routing.entity.SerializableIntent import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.email.EmailSender import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.UiDependencies 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.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder 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.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig 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.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -171,6 +176,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject @Inject
internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector 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() internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode> private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -223,9 +237,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
sendStakingUnsubmittedHashes() sendStakingUnsubmittedHashes()
checkGoogleServicesAvailability() checkGoogleServicesAvailability()
if (intent != null && savedInstanceState == null) { if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) {
// handle intent only on start, not on recreate // handle intent only on start, not on recreate
deepLinksRegistry.launch(intent) handleDeepLink(intent)
} }
lifecycle.addObserver(WindowObscurationObserver) lifecycle.addObserver(WindowObscurationObserver)
@ -341,7 +355,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler)) intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
}
} }
private fun updateAppTheme(appThemeMode: AppThemeMode) { private fun updateAppTheme(appThemeMode: AppThemeMode) {
@ -374,7 +391,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
} }
if (intent != null) { 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() 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() { private fun observePolkadotAccountHealthCheck() {
lifecycleScope.launch { lifecycleScope.launch {
getPolkadotCheckHasResetUseCase() getPolkadotCheckHasResetUseCase()

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,16 @@ internal class AmplitudeLogClient(
private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(AmplitudeAnalyticsHandler.ID, jsonConverter) 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>) { override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params) 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.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler 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.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
@ -10,12 +11,20 @@ import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
class FirebaseAnalyticsHandler( class FirebaseAnalyticsHandler(
private val client: FirebaseAnalyticsClient, private val client: FirebaseAnalyticsClient,
) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler { ) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler, AnalyticsUserIdHandler {
private val errorConverter = AnalyticsErrorConverter() private val errorConverter = AnalyticsErrorConverter()
override fun id(): String = ID 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>) { override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params) client.logEvent(eventId, params)
} }

View file

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

View file

@ -11,6 +11,15 @@ internal class FirebaseLogClient(
) : FirebaseAnalyticsClient { ) : FirebaseAnalyticsClient {
private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(FirebaseAnalyticsHandler.ID, jsonConverter) 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>) { override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params) logger.logEvent(event, params)

View file

@ -40,9 +40,6 @@ class CardContextInterceptor(
params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.BATCH] = card.batchId
params[AnalyticsParam.PRODUCT_TYPE] = getProductType() params[AnalyticsParam.PRODUCT_TYPE] = getProductType()
params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue
if (userWalletId != null) {
params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue
}
ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let { ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let {
params[AnalyticsParam.CURRENCY] = it.value 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.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
/** /**
@ -12,6 +13,11 @@ import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterc
* Sets the new context * Sets the new context
*/ */
fun Analytics.setContext(scanResponse: ScanResponse) { fun Analytics.setContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (userWalletId != null) {
setUserId(userWalletId.stringValue)
}
addParamsInterceptor(LinkedCardContextInterceptor(scanResponse)) addParamsInterceptor(LinkedCardContextInterceptor(scanResponse))
} }
@ -19,6 +25,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
* Erases the context * Erases the context
*/ */
fun Analytics.eraseContext() { fun Analytics.eraseContext() {
clearUserId()
removeParamsInterceptor(LinkedCardContextInterceptor.id()) 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 * Adds a new context and keeps a previous context as the parent of the new one
*/ */
fun Analytics.addContext(scanResponse: ScanResponse) { 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 currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext) val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext)

View file

@ -1,34 +1,5 @@
package com.tangem.tap.common.extensions 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 { fun String.removePrefixOrNull(prefix: String): String? = when {
startsWith(prefix) -> substring(prefix.length) startsWith(prefix) -> substring(prefix.length)
else -> null else -> null

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchainsdk.utils.amountToCreateAccount import com.tangem.blockchainsdk.utils.amountToCreateAccount
import com.tangem.common.services.Result 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.TestActions
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.domain.TapError 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.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider import com.tangem.utils.notifications.PushNotificationsTokenProvider
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import javax.inject.Inject
internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider {
override suspend fun getToken(): String { override suspend fun getToken(): String {
return FirebaseMessaging.getInstance().token.await() 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.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository 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.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -70,7 +69,6 @@ internal object ManageTokensDomainModule {
customTokensRepository: CustomTokensRepository, customTokensRepository: CustomTokensRepository,
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
derivationsRepository: DerivationsRepository, derivationsRepository: DerivationsRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
@ -83,7 +81,6 @@ internal object ManageTokensDomainModule {
customTokensRepository = customTokensRepository, customTokensRepository = customTokensRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
quotesRepository = quotesRepository, 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.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository 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.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import dagger.Module import dagger.Module
@ -72,7 +71,6 @@ object MarketsDomainModule {
derivationsRepository: DerivationsRepository, derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository, marketsTokenRepository: MarketsTokenRepository,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -84,7 +82,6 @@ object MarketsDomainModule {
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository, marketsTokenRepository = marketsTokenRepository,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,

View file

@ -1,9 +1,12 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.* import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository 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.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -72,10 +75,11 @@ internal object NFTDomainModule {
@Provides @Provides
@Singleton @Singleton
fun providesGetNFTNetworkStatusUseCase(networksRepository: NetworksRepository): GetNFTNetworkStatusUseCase = fun providesGetNFTNetworkStatusUseCase(
GetNFTNetworkStatusUseCase( singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
networksRepository = networksRepository, ): GetNFTNetworkStatusUseCase {
) return GetNFTNetworkStatusUseCase(singleNetworkStatusSupplier = singleNetworkStatusSupplier)
}
@Provides @Provides
@Singleton @Singleton
@ -83,4 +87,53 @@ internal object NFTDomainModule {
GetNFTExploreUrlUseCase( GetNFTExploreUrlUseCase(
nftRepository = nftRepository, 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 package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase
import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository 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 com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -28,12 +31,10 @@ internal object NotificationsDomainModule {
@Singleton @Singleton
fun providesSendPushTokenUseCase( fun providesSendPushTokenUseCase(
notificationsRepository: NotificationsRepository, notificationsRepository: NotificationsRepository,
getApplicationIdUseCase: GetApplicationIdUseCase,
pushNotificationsTokenProvider: PushNotificationsTokenProvider, pushNotificationsTokenProvider: PushNotificationsTokenProvider,
): SendPushTokenUseCase { ): SendPushTokenUseCase {
return SendPushTokenUseCase( return SendPushTokenUseCase(
notificationsRepository = notificationsRepository, notificationsRepository = notificationsRepository,
getApplicationIdUseCase = getApplicationIdUseCase,
pushNotificationsTokenProvider = pushNotificationsTokenProvider, pushNotificationsTokenProvider = pushNotificationsTokenProvider,
) )
} }
@ -57,4 +58,10 @@ internal object NotificationsDomainModule {
notificationsRepository = notificationsRepository, 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.exchange.RampStateManager
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier 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.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository 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.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations 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.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -37,7 +41,6 @@ internal object TokensDomainModule {
@Singleton @Singleton
fun provideAddCryptoCurrenciesUseCase( fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -47,7 +50,6 @@ internal object TokensDomainModule {
): AddCryptoCurrenciesUseCase { ): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase( return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -62,7 +64,6 @@ internal object TokensDomainModule {
fun provideFetchTokenListUseCase( fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteFetcher: MultiQuoteFetcher,
@ -71,7 +72,6 @@ internal object TokensDomainModule {
): FetchTokenListUseCase { ): FetchTokenListUseCase {
return FetchTokenListUseCase( return FetchTokenListUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -121,8 +121,8 @@ internal object TokensDomainModule {
fun provideGetCurrencyUseCase( fun provideGetCurrencyUseCase(
baseCurrencyStatusOperations: BaseCurrencyStatusOperations, baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): GetCurrencyStatusUpdatesUseCase { ): GetSingleCryptoCurrencyStatusUseCase {
return GetCurrencyStatusUpdatesUseCase( return GetSingleCryptoCurrencyStatusUseCase(
currencyStatusOperations = baseCurrencyStatusOperations, currencyStatusOperations = baseCurrencyStatusOperations,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
@ -147,7 +147,6 @@ internal object TokensDomainModule {
fun provideGetCurrencyWarningsUseCase( fun provideGetCurrencyWarningsUseCase(
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
currencyChecksRepository: CurrencyChecksRepository, currencyChecksRepository: CurrencyChecksRepository,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
baseCurrencyStatusOperations: BaseCurrencyStatusOperations, baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
@ -155,31 +154,17 @@ internal object TokensDomainModule {
return GetCurrencyWarningsUseCase( return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository, dispatchers = dispatchers,
currencyChecksRepository = currencyChecksRepository, currencyChecksRepository = currencyChecksRepository,
dispatchers = dispatchers,
currencyStatusOperations = baseCurrencyStatusOperations, currencyStatusOperations = baseCurrencyStatusOperations,
) )
} }
@Provides
@Singleton
fun provideGetPrimaryCurrencyUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currencyStatusOperations = currencyStatusOperations,
dispatchers = dispatchers,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideFetchCurrencyStatusUseCase( fun provideFetchCurrencyStatusUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteFetcher: MultiQuoteFetcher,
@ -188,7 +173,6 @@ internal object TokensDomainModule {
): FetchCurrencyStatusUseCase { ): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase( return FetchCurrencyStatusUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
@ -203,7 +187,6 @@ internal object TokensDomainModule {
fun provideFetchCardTokenListUseCase( fun provideFetchCardTokenListUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteFetcher: MultiQuoteFetcher,
@ -212,7 +195,6 @@ internal object TokensDomainModule {
): FetchCardTokenListUseCase { ): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase( return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -222,14 +204,6 @@ internal object TokensDomainModule {
) )
} }
@Provides
@Singleton
fun providesGetCryptoCurrencyStatusSyncUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
): GetCryptoCurrencyStatusSyncUseCase {
return GetCryptoCurrencyStatusSyncUseCase(currencyStatusOperations)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase { fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase {
@ -328,14 +302,10 @@ internal object TokensDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideUpdateDelayedCurrencyStatusUseCase( fun provideUpdateDelayedCurrencyStatusUseCase(
networksRepository: NetworksRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): UpdateDelayedNetworkStatusUseCase { ): UpdateDelayedNetworkStatusUseCase {
return UpdateDelayedNetworkStatusUseCase( return UpdateDelayedNetworkStatusUseCase(
networksRepository = networksRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
) )
} }
@ -413,7 +383,6 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2, quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier, singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -427,7 +396,6 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2, quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier,
@ -447,7 +415,6 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2, quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier, singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -461,7 +428,6 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository, quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2, quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier, 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.demo.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier 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.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.* import com.tangem.domain.transaction.usecase.*
@ -43,7 +41,6 @@ internal object TransactionDomainModule {
transactionRepository: TransactionRepository, transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SendTransactionUseCase { ): SendTransactionUseCase {
return SendTransactionUseCase( return SendTransactionUseCase(
demoConfig = DemoConfig(), demoConfig = DemoConfig(),
@ -51,7 +48,6 @@ internal object TransactionDomainModule {
transactionRepository = transactionRepository, transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
) )
} }
@ -61,17 +57,13 @@ internal object TransactionDomainModule {
cardSdkConfigRepository: CardSdkConfigRepository, cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier, singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): AssociateAssetUseCase { ): AssociateAssetUseCase {
return AssociateAssetUseCase( return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository, cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier, 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.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade 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.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
@ -28,6 +30,17 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal object WalletsDomainModule { internal object WalletsDomainModule {
@Provides
fun providesUserWalletsSyncDelegate(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsSyncDelegate {
return DefaultUserWalletsSyncDelegate(
userWalletsListManager = userWalletsListManager,
dispatchers = dispatchers,
)
}
@Provides @Provides
@Singleton @Singleton
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase {
@ -102,10 +115,13 @@ internal object WalletsDomainModule {
@Provides @Provides
@Singleton @Singleton
fun providesRenameWalletUseCase( fun providesRenameWalletUseCase(
userWalletsListManager: UserWalletsListManager, walletsRepository: WalletsRepository,
dispatchers: CoroutineDispatcherProvider, userWalletsSyncDelegate: UserWalletsSyncDelegate,
): RenameWalletUseCase { ): RenameWalletUseCase {
return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers) return RenameWalletUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
)
} }
@Provides @Provides
@ -197,4 +213,36 @@ internal object WalletsDomainModule {
nftFeatureToggles = nftFeatureToggles, 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 package com.tangem.tap.di.routing
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.RoutingFeatureToggle
import com.tangem.core.analytics.api.AnalyticsExceptionHandler 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.ProxyAppRouter
import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.configurator.MutableAppRouterConfig import com.tangem.tap.routing.configurator.MutableAppRouterConfig
@ -31,4 +33,10 @@ internal object AppRouterModule {
@Provides @Provides
@Singleton @Singleton
fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig() 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.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.BackendId import com.tangem.domain.card.BackendId
import com.tangem.domain.card.repository.DerivationsRepository 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.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.UserWallet
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -41,7 +41,7 @@ internal class DefaultDerivationsRepository(
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) 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") val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
derivePublicKeysByNetworks( derivePublicKeysByNetworks(

View file

@ -8,10 +8,10 @@ import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider 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.KeyWalletPublicKey
import com.tangem.domain.models.scan.ScanResponse 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 import com.tangem.operations.derivation.ExtendedPublicKeysMap
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>> 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> { private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
val config = CardConfig.createConfig(scanResponse.card) val config = CardConfig.createConfig(scanResponse.card)
return mapNotNull { network -> 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 val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
findNewDerivations(curve = curve, scanResponse = scanResponse, network = network) 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> { private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = this.id.value) val blockchain = Blockchain.fromId(id = rawId)
return buildList { return buildList {
add(blockchain.getDerivationPath(curve = curve)) 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.Either
import arrow.core.getOrElse import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either import arrow.core.raise.either
import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
@ -14,22 +13,18 @@ import com.tangem.common.extensions.toHexString
import com.tangem.common.map import com.tangem.common.map
import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource
import com.tangem.core.error.ext.tangemError 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.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.datasource.local.visa.VisaOtpData
import com.tangem.datasource.local.visa.hasSavedOTP 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.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError 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.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.GenerateOTPCommand import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.sign.SignHashResponse
@ -95,23 +90,7 @@ class VisaCardActivationTask @AssistedInject constructor(
context.signAuthorizationChallenge(mode.authorizationChallenge) context.signAuthorizationChallenge(mode.authorizationChallenge)
} }
is VisaCardActivationTaskMode.SignOnly -> { is VisaCardActivationTaskMode.SignOnly -> {
val wallet = context.signData(mode.dataToSignByCardWallet)
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,
)
} }
} }
} }
@ -164,16 +143,7 @@ class VisaCardActivationTask @AssistedInject constructor(
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) { val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey)
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) } .getOrElse { return CompletionResult.Failure(it.tangemError) }
.value .value
@ -190,10 +160,7 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await() otpTaskDeferred.await()
signData( signData(dataToSign = dataToSign)
dataToSign = dataToSign,
derivedPublicKey = derivedPublicKey,
)
} }
} }
@ -201,28 +168,24 @@ class VisaCardActivationTask @AssistedInject constructor(
signedChallenge: VisaAuthSignedChallenge, signedChallenge: VisaAuthSignedChallenge,
cardWalletAddress: String, cardWalletAddress: String,
): Either<TangemError, VisaDataToSignByCardWallet> = either { ): Either<TangemError, VisaDataToSignByCardWallet> = either {
catch( val tokens = visaAuthRepository.getAccessTokens(signedChallenge)
block = { .getOrElse { raise(it.tangemError) }
val tokens = visaAuthRepository.getAccessTokens(signedChallenge)
visaAuthTokenStorage.store(cardId, tokens) visaAuthTokenStorage.store(cardId, tokens)
val remoteState = visaActivationRepository.getActivationRemoteState() val remoteState = visaActivationRepository.getActivationRemoteState()
if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) { .getOrElse { raise(it.tangemError) }
raise(VisaActivationError.WrongRemoteState.tangemError)
}
visaActivationRepository.getCardWalletAcceptanceData( if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) {
VisaCardWalletDataToSignRequest( return raise(VisaActivationError.WrongRemoteState.tangemError)
activationOrderInfo = remoteState.activationOrderInfo, }
cardWalletAddress = cardWalletAddress,
), visaActivationRepository.getCardWalletAcceptanceData(
) VisaCardWalletDataToSignRequest(
}, activationOrderInfo = remoteState.activationOrderInfo,
catch = { cardWalletAddress = cardWalletAddress,
raise(VisaAuthorizationAPIError.tangemError) ),
}, ).getOrElse { raise(it.tangemError) }
)
} }
private suspend fun SessionContext.createWallet(): CompletionResult<Unit> { private suspend fun SessionContext.createWallet(): CompletionResult<Unit> {
@ -294,7 +257,6 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.signData( private suspend fun SessionContext.signData(
dataToSign: VisaDataToSignByCardWallet, dataToSign: VisaDataToSignByCardWallet,
derivedPublicKey: ExtendedPublicKey,
): CompletionResult<VisaCardActivationResponse> { ): CompletionResult<VisaCardActivationResponse> {
val card = val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
@ -306,7 +268,6 @@ class VisaCardActivationTask @AssistedInject constructor(
val task = SignHashCommand( val task = SignHashCommand(
hash = dataToSign.hashToSign.hexToBytes(), hash = dataToSign.hashToSign.hexToBytes(),
walletPublicKey = wallet.publicKey, walletPublicKey = wallet.publicKey,
derivationPath = VisaUtilities.visaDefaultDerivationPath,
) )
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
@ -325,7 +286,7 @@ class VisaCardActivationTask @AssistedInject constructor(
handleSignedData( handleSignedData(
dataToSign = dataToSign, dataToSign = dataToSign,
response = result.data, response = result.data,
derivedPublicKey = derivedPublicKey, walletPublicKey = wallet.publicKey,
) )
} }
is CompletionResult.Failure -> { 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( private suspend fun SessionContext.handleSignedData(
dataToSign: VisaDataToSignByCardWallet, dataToSign: VisaDataToSignByCardWallet,
derivedPublicKey: ExtendedPublicKey, walletPublicKey: ByteArray,
response: SignHashResponse, response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> { ): CompletionResult<VisaCardActivationResponse> {
val otp = otpStorage.getOTP(cardId) ?: run { val otp = otpStorage.getOTP(cardId) ?: run {
@ -362,7 +309,7 @@ class VisaCardActivationTask @AssistedInject constructor(
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = response.signature, signature = response.signature,
hash = dataToSign.hashToSign.hexToBytes(), hash = dataToSign.hashToSign.hexToBytes(),
publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(), publicKey = walletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase() ).asRSVLegacyEVM().toHexString().lowercase()
val signedActivationData = dataToSign.sign( val signedActivationData = dataToSign.sign(

View file

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

View file

@ -7,9 +7,6 @@ internal class DefaultTokensFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles { ) : TokensFeatureToggles {
override val isNetworksLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED")
override val isQuotesLoadingRefactoringEnabled: Boolean override val isQuotesLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "QUOTES_LOADING_REFACTORING_ENABLED") 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 arrow.core.getOrElse
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession import com.tangem.common.core.CardSession
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError 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.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.visa.error.VisaActivationError 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.error.VisaCardScanError
import com.tangem.domain.visa.model.* import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.attestation.AttestWalletKeyTask
import com.tangem.operations.sign.SignHashResponse
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -61,54 +56,19 @@ internal class VisaCardScanHandler @Inject constructor(
session = session, session = session,
) )
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
val activationInput = val activationInput =
VisaActivationInput(card.cardId, card.cardPublicKey.toHexString(), card.isAccessCodeSet) VisaActivationInput(card.cardId, card.cardPublicKey.toHexString(), card.isAccessCodeSet)
val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput) val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput)
return CompletionResult.Success(activationStatus) return CompletionResult.Success(activationStatus)
} }
return context.deriveKey(wallet) return context.handleWalletAuthorization()
}
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)
}
}
} }
private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult<VisaCardActivationStatus> { private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult<VisaCardActivationStatus> {
Timber.i("Started handling authorization using Visa wallet") 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 card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { 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) return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError)
} }
val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run { val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey)
Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(VisaCardScanError.FailedToFindDerivedWalletKey.tangemError)
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(extendedPublicKey.publicKey)
.getOrElse { .getOrElse {
return CompletionResult.Failure(it.tangemError) return CompletionResult.Failure(it.tangemError)
} }
Timber.i("Requesting challenge for wallet authorization") Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = runCatching {
// TODO [REDACTED_TASK_KEY] val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
error("sign and get specific error to switch to card_id flow") .getOrElse {
visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) Timber.i("Failed to get Access token for Wallet public key authorization")
}.getOrElse { return CompletionResult.Failure(it.tangemError)
Timber.i( }
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization(
cardWalletAddress = walletAddress.value,
)
}
val signChallengeResult = signChallengeWithWallet( val signChallengeResult = signChallengeWithWallet(
publicKey = wallet.publicKey, publicKey = wallet.publicKey,
derivationPath = derivationPath,
nonce = challengeResponse.challenge, nonce = challengeResponse.challenge,
) )
return when (signChallengeResult) { return when (signChallengeResult) {
is CompletionResult.Success -> { 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") Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens( handleWalletAuthorizationTokens(
cardWalletAddress = walletAddress.value, cardWalletAddress = walletAddress.value,
signedChallenge = challengeResponse signedChallenge = challengeResponse.toSignedChallenge(signature.toHexString()),
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
) )
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
@ -166,16 +118,19 @@ internal class VisaCardScanHandler @Inject constructor(
cardWalletAddress: String, cardWalletAddress: String,
signedChallenge: VisaAuthSignedChallenge, signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> { ): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = runCatching { val authorizationTokensResponse = visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) .getOrElse {
}.getOrElse { Timber.i("Failed to get Access token for Wallet public key authorization.")
Timber.i( return if (
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", it is VisaApiError.ProductInstanceIsNotActivated ||
) it is VisaApiError.ProductInstanceNotFoundActivationRequired
return handleCardAuthorization( ) {
cardWalletAddress = cardWalletAddress, Timber.i("Proceeding with card authorization.")
) handleCardAuthorization(cardWalletAddress = cardWalletAddress)
} } else {
CompletionResult.Failure(it.tangemError)
}
}
Timber.i("Authorized using Wallet public key successfully") Timber.i("Authorized using Wallet public key successfully")
@ -190,14 +145,12 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting authorization challenge to sign") Timber.i("Requesting authorization challenge to sign")
val challengeResponse = runCatching { val challengeResponse = visaAuthRepository.getCardAuthChallenge(
visaAuthRepository.getCardAuthChallenge( cardId = card.cardId,
cardId = card.cardId, cardPublicKey = card.cardPublicKey.toHexString(),
cardPublicKey = card.cardPublicKey.toHexString(), ).getOrElse {
) Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.errorCode}")
}.getOrElse { return CompletionResult.Failure(it.tangemError)
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
} }
Timber.i("Received challenge to sign: ${challengeResponse.challenge}") Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
@ -217,17 +170,14 @@ internal class VisaCardScanHandler @Inject constructor(
} }
} }
@Suppress("UnusedPrivateMember") val authorizationTokensResponse = visaAuthRepository.getAccessTokens(
val authorizationTokensResponse = runCatching { signedChallenge = challengeResponse.toSignedChallenge(
visaAuthRepository.getAccessTokens( signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
signedChallenge = challengeResponse.toSignedChallenge( salt = attestCardKeyResponse.salt.toHexString(),
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(), ),
salt = attestCardKeyResponse.salt.toHexString(), ).getOrElse {
), Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}")
) return CompletionResult.Failure(it.tangemError)
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
} }
visaAuthTokenStorage.store( visaAuthTokenStorage.store(
@ -235,11 +185,9 @@ internal class VisaCardScanHandler @Inject constructor(
tokens = authorizationTokensResponse, tokens = authorizationTokensResponse,
) )
val activationRemoteState = runCatching { val activationRemoteState = visaActivationRepository.getActivationRemoteState().getOrElse {
visaActivationRepository.getActivationRemoteState() Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}")
}.getOrElse { return CompletionResult.Failure(it.tangemError)
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
} }
val error = when (activationRemoteState) { val error = when (activationRemoteState) {
@ -271,13 +219,11 @@ internal class VisaCardScanHandler @Inject constructor(
private suspend fun SessionContext.signChallengeWithWallet( private suspend fun SessionContext.signChallengeWithWallet(
publicKey: ByteArray, publicKey: ByteArray,
derivationPath: DerivationPath,
nonce: String, nonce: String,
): CompletionResult<SignHashResponse> { ): CompletionResult<AttestWalletKeyResponse> {
val signHashCommand = SignHashCommand( val signHashCommand = AttestWalletKeyTask(
hash = nonce.hexToBytes(), publicKey = publicKey,
walletPublicKey = publicKey, challenge = nonce.hexToBytes(),
derivationPath = derivationPath,
) )
return suspendCancellableCoroutine { return suspendCancellableCoroutine {
signHashCommand.run(session) { result -> 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.WcPairRequest
import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase 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.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
@ -64,14 +65,18 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
if (isNewWc) stub.updateSessions() else legacy.updateSessions() 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) { val src = when (source) {
SourceType.QR -> WcPairRequest.Source.QR SourceType.QR -> WcPairRequest.Source.QR
SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK
SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD
SourceType.ETC -> WcPairRequest.Source.ETC 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) { override fun disconnect(topic: String) {
@ -110,7 +115,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository
override fun updateSessions() = Unit 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 override fun disconnect(topic: String) = Unit
@ -405,7 +410,7 @@ internal class DefaultLegacyWalletConnectRepository(
this.userNamespaces = userNamespaces 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)) analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source))
WalletKit.pair( WalletKit.pair(
params = Wallet.Params.Pair(uri), params = Wallet.Params.Pair(uri),

View file

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

View file

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

View file

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

View file

@ -63,7 +63,11 @@ class WalletConnectMiddleware {
is WalletConnectAction.OpenSession -> { is WalletConnectAction.OpenSession -> {
val index = action.wcUri.indexOf("@") val index = action.wcUri.indexOf("@")
when (action.wcUri[index + 1]) { 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' -> { '1' -> {
store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest) store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest)
store.dispatchOnMain( store.dispatchOnMain(

View file

@ -22,10 +22,10 @@ import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
internal class DefaultWalletConnectComponent @AssistedInject constructor( internal class DefaultWalletConnectComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext, @Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit, @Assisted params: WalletConnectComponent.Params,
) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber<WalletConnectState> { ) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber<WalletConnectState> {
private val model: WalletConnectModel = getOrCreateModel() private val model: WalletConnectModel = getOrCreateModel(params)
private var screenState: MutableState<WalletConnectScreenState> = private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(model.updateState(store.state.walletConnectState)) mutableStateOf(model.updateState(store.state.walletConnectState))
@ -65,6 +65,9 @@ internal class DefaultWalletConnectComponent @AssistedInject constructor(
@AssistedFactory @AssistedFactory
interface Factory : WalletConnectComponent.Factory { 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 arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model 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.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState 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.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -22,13 +24,22 @@ import javax.inject.Inject
internal class WalletConnectModel @Inject constructor( internal class WalletConnectModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
paramsContainer: ParamsContainer,
) : Model() { ) : Model() {
private val params = paramsContainer.require<WalletConnectComponent.Params>()
init { init {
modelScope.launch { modelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT) listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() } .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) } .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.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface WalletConnectComponent : ComposableContentComponent { 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 androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.keyboard.KeyboardValidator import com.tangem.common.keyboard.KeyboardValidator
import com.tangem.common.routing.RoutingFeatureToggle
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent 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.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase 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.onboarding.repository.OnboardingRepository
import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.promo.GetStoryContentUseCase 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.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager 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.feature.swap.analytics.StoriesEvents
import com.tangem.features.onramp.deeplink.OnrampDeepLink import com.tangem.features.onramp.deeplink.OnrampDeepLink
import com.tangem.tap.common.extensions.setContext 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.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@Suppress("LongParameterList") @Suppress("LongParameterList")
@HiltViewModel @HiltViewModel
@ -56,7 +68,6 @@ internal class MainViewModel @Inject constructor(
private val userWalletsListManager: UserWalletsListManager, private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val apiConfigsManager: ApiConfigsManager,
private val fetchUserCountryUseCase: FetchUserCountryUseCase, private val fetchUserCountryUseCase: FetchUserCountryUseCase,
@GlobalUiMessageSender private val messageSender: UiMessageSender, @GlobalUiMessageSender private val messageSender: UiMessageSender,
private val keyboardValidator: KeyboardValidator, private val keyboardValidator: KeyboardValidator,
@ -67,6 +78,14 @@ internal class MainViewModel @Inject constructor(
private val onboardingRepository: OnboardingRepository, private val onboardingRepository: OnboardingRepository,
private val deepLinksRegistry: DeepLinksRegistry, private val deepLinksRegistry: DeepLinksRegistry,
private val onrampDeepLinkFactory: OnrampDeepLink.Factory, 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, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() { ) : ViewModel() {
@ -77,32 +96,38 @@ internal class MainViewModel @Inject constructor(
private set private set
init { init {
/**
* Run any data initialization here that needs to happen before the app starts
* and is hidden behind the SplashScreen
*/
loadApplicationResources() 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 { launch { fetchAppCurrenciesUseCase() }
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it") launch { fetchStakingTokens() }
}
launch { initPushNotifications() }
} }
viewModelScope.launch { fetchHotCryptoUseCase() } viewModelScope.launch { incrementAppLaunchCounterUseCase() }
updateAppCurrencies()
observeFlips() observeFlips()
displayBalancesHidingStatusToast() displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification() displayHiddenBalancesModalNotification()
fetchStakingTokens()
deleteDeprecatedLogsUseCase() deleteDeprecatedLogsUseCase()
sendKeyboardIdentifierEvent() sendKeyboardIdentifierEvent()
preloadImages() preloadImages()
initializeDeepLinks() if (!routingFeatureToggle.isDeepLinkNavigationEnabled) {
initializeDeepLinks()
}
} }
fun checkForUnfinishedBackup() { fun checkForUnfinishedBackup() {
@ -114,16 +139,41 @@ internal class MainViewModel @Inject constructor(
/** Loading the resources needed to run the application */ /** Loading the resources needed to run the application */
private fun loadApplicationResources() { private fun loadApplicationResources() {
viewModelScope.launch(dispatchers.main) { viewModelScope.launch {
apiConfigsManager.initialize() launchAPIRequests {
launch { blockchainSDKFactory.init() }
launch {
withTimeout(timeMillis = 1.seconds.inWholeMilliseconds) { fetchUserCountry() }
}
}
blockchainSDKFactory.init()
prepareSelectedWalletFeedback() prepareSelectedWalletFeedback()
isSplashScreenShown = false 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() { private fun prepareSelectedWalletFeedback() {
userWalletsListManager.selectedUserWallet userWalletsListManager.selectedUserWallet
.distinctUntilChanged() .distinctUntilChanged()
@ -134,18 +184,10 @@ internal class MainViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
private fun updateAppCurrencies() { private suspend fun fetchStakingTokens() {
viewModelScope.launch(dispatchers.main) { fetchStakingTokensUseCase()
fetchAppCurrenciesUseCase.invoke() .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") }
} .onRight { Timber.d("Staking token list was fetched successfully") }
}
private fun fetchStakingTokens() {
viewModelScope.launch(dispatchers.main) {
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() { private fun observeFlips() {
@ -343,4 +385,20 @@ internal class MainViewModel @Inject constructor(
private fun initializeDeepLinks() { private fun initializeDeepLinks() {
deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope)) 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.AppRoute
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics 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.onramp.model.OnrampSource
import com.tangem.domain.tokens.legacy.TradeCryptoAction 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.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
@ -79,7 +79,7 @@ object TradeCryptoMiddleware {
val status = action.cryptoCurrencyStatus val status = action.cryptoCurrencyStatus
val currency = status.currency 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 exchangeManager = store.state.globalState.exchangeManager
val topUrl = exchangeManager.getUrl( val topUrl = exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy, action = CurrencyExchangeManager.Action.Buy,

View file

@ -13,4 +13,10 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle
override fun getCardId(): String { override fun getCardId(): String {
return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: "" 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.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CryptoCurrencyFactory 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.common.extensions.inject
import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
@ -44,7 +44,7 @@ internal class CryptoCurrencyConverter(
} }
override fun convertBack(value: CryptoCurrency): Currency { 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") if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) { return when (value) {
is CryptoCurrency.Coin -> Currency.Blockchain( 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.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading 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.CardDTO
import com.tangem.domain.models.scan.ScanResponse 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.inject
import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
@ -68,7 +68,7 @@ class CurrencyExchangeManager(
walletAddress: String, walletAddress: String,
isDarkTheme: Boolean, isDarkTheme: Boolean,
): String? { ): String? {
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) val blockchain = Blockchain.fromId(cryptoCurrency.network.rawId)
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl() if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val urlBuilder = getExchangeUrlBuilder(action) 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.core.lce.Lce
import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase 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.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.repository.CurrenciesRepository 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.lce.Lce
import com.tangem.domain.core.utils.lceLoading 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.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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.lceContent
import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading 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.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeService
@ -77,7 +77,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
): String { ): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException() 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() val builder = Uri.Builder()
.scheme(ExchangeUrlBuilder.SCHEME) .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.lceContent
import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading 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.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeService
@ -133,7 +133,7 @@ class MoonPayService(
): String? { ): String? {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException() 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 supportedCurrency = blockchain.moonPaySupportedCurrency ?: return null
val moonpayCurrency = status?.availableForSell?.firstOrNull { val moonpayCurrency = status?.availableForSell?.firstOrNull {
when (cryptoCurrency) { 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.component.RoutingComponent.Child
import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.utils.ChildFactory import com.tangem.tap.routing.utils.ChildFactory
import com.tangem.tap.routing.utils.DeepLinkFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -34,6 +35,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val appRouterConfig: AppRouterConfig, private val appRouterConfig: AppRouterConfig,
private val uiDependencies: UiDependencies, private val uiDependencies: UiDependencies,
private val wcRoutingComponentFactory: WcRoutingComponent.Factory, private val wcRoutingComponentFactory: WcRoutingComponent.Factory,
private val deeplinkFactory: DeepLinkFactory,
) : RoutingComponent, ) : RoutingComponent,
AppComponentContext by context, AppComponentContext by context,
SnackbarHandler { SnackbarHandler {
@ -60,7 +62,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
stack.subscribe(lifecycle) { stack -> stack.subscribe(lifecycle) { stack ->
val stackItems = stack.items.map { it.configuration } val stackItems = stack.items.map { it.configuration }
wcRoutingComponent.onAppRouteChange(stack.active.configuration) wcRoutingComponent.onAppRouteChange(stack.active.configuration)
deeplinkFactory.checkRoutingReadiness(stack.active.configuration)
if (appRouterConfig.stack != stackItems) { if (appRouterConfig.stack != stackItems) {
appRouterConfig.stack = stackItems appRouterConfig.stack = stackItems
} }

View file

@ -291,13 +291,13 @@ internal class ChildFactory @Inject constructor(
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
createComponentChild( createComponentChild(
context = context, context = context,
params = Unit, params = RedesignedWalletConnectComponent.Params(route.userWalletId),
componentFactory = redesignedWalletConnectComponentFactory, componentFactory = redesignedWalletConnectComponentFactory,
) )
} else { } else {
createComponentChild( createComponentChild(
context = context, context = context,
params = Unit, params = WalletConnectComponent.Params(route.userWalletId),
componentFactory = walletConnectComponentFactory, 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 { dependencies {
/* Core */ /* Core */
implementation(projects.core.decompose) implementation(projects.core.decompose)
implementation(projects.core.configToggles)
/* Domain */ /* Domain */
implementation(projects.domain.qrScanning.models) 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.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -122,7 +122,7 @@ sealed class AppRoute(val path: String) : Route {
} }
@Serializable @Serializable
data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions") data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions")
@Serializable @Serializable
data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") { 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.models)
implementation(projects.domain.staking.models) implementation(projects.domain.staking.models)
implementation(projects.domain.tokens.models) implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets) implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
@ -30,4 +31,6 @@ dependencies {
implementation(tangemDeps.blockchain) implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core) 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.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.models.network.NetworkStatus
import java.math.BigDecimal import java.math.BigDecimal
/** /**
@ -14,36 +14,41 @@ object MockNetworkStatusFactory {
private val defaultNetwork = MockCryptoCurrencyFactory().ethereum.network 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( return NetworkStatus(
network = network, network = network,
value = NetworkStatus.Verified( value = NetworkStatus.Verified(
address = NetworkAddress.Single( address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address( defaultAddress = NetworkAddress.Address(
value = "0x123", value = "0x1",
type = NetworkAddress.Address.Type.Primary, type = NetworkAddress.Address.Type.Primary,
), ),
), ),
amounts = mapOf(), amounts = mapOf(),
pendingTransactions = 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( return NetworkStatus(
network = network, network = network,
value = NetworkStatus.NoAccount( value = NetworkStatus.NoAccount(
address = NetworkAddress.Single( address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address( defaultAddress = NetworkAddress.Address(
value = "0x123", value = "0x1",
type = NetworkAddress.Address.Type.Primary, type = NetworkAddress.Address.Type.Primary,
), ),
), ),
amountToCreateAccount = BigDecimal.ONE, amountToCreateAccount = BigDecimal.ONE,
errorMessage = "", 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.data.common.currency.getNetworkStandardType
import com.tangem.domain.common.configs.GenericCardConfig import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.util.derivationStyleProvider 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.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -47,16 +47,18 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
} }
fun createCoin(blockchain: Blockchain): CryptoCurrency { fun createCoin(blockchain: Blockchain): CryptoCurrency {
val derivationPath = getNetworkDerivationPath(
blockchain = blockchain,
extraDerivationPath = null,
cardDerivationStyleProvider = scanResponse.derivationStyleProvider,
)
val network = Network( val network = Network(
id = Network.ID(blockchain.id), id = Network.ID(blockchain.id, derivationPath),
backendId = blockchain.toNetworkId(), backendId = blockchain.toNetworkId(),
name = blockchain.fullName, name = blockchain.fullName,
isTestnet = blockchain.isTestnet(), isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath( derivationPath = derivationPath,
blockchain = blockchain,
extraDerivationPath = null,
cardDerivationStyleProvider = scanResponse.derivationStyleProvider,
),
currencySymbol = blockchain.currency, currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain), standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, 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 // Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood
fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency { fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency {
val derivationPath = Network.DerivationPath.Custom(
value = derivationBlockchain.derivationPath(
scanResponse.derivationStyleProvider.getDerivationStyle(),
)!!.rawPath,
)
return CryptoCurrency.Token( return CryptoCurrency.Token(
id = CryptoCurrency.ID( id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
@ -76,15 +84,11 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
suffix = CryptoCurrency.ID.Suffix.RawID(blockchain.id), suffix = CryptoCurrency.ID.Suffix.RawID(blockchain.id),
), ),
network = Network( network = Network(
id = Network.ID(value = blockchain.id), id = Network.ID(value = blockchain.id, derivationPath),
backendId = "NEVER-MIND", backendId = "NEVER-MIND",
name = blockchain.fullName, name = blockchain.fullName,
currencySymbol = "NEVER-MIND", currencySymbol = "NEVER-MIND",
derivationPath = Network.DerivationPath.Custom( derivationPath = derivationPath,
value = derivationBlockchain.derivationPath(
scanResponse.derivationStyleProvider.getDerivationStyle(),
)!!.rawPath,
),
isTestnet = false, isTestnet = false,
standardType = Network.StandardType.ERC20, standardType = Network.StandardType.ERC20,
hasFiatFeeRate = true, hasFiatFeeRate = true,
@ -102,7 +106,13 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
fun createToken(blockchain: Blockchain): CryptoCurrency { fun createToken(blockchain: Blockchain): CryptoCurrency {
return factory.createToken( 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, blockchain = blockchain,
extraDerivationPath = null, extraDerivationPath = null,
scanResponse = scanResponse, 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.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.tokens.model.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList 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.combinedReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.models.network.NetworkAddress
private const val DSC_ADDRESS_NAME = "DSC" private const val DSC_ADDRESS_NAME = "DSC"
private const val DEL_ADDRESS_NAME = "Main" 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 { private fun Network.getAddressDisplayName(addressType: NetworkAddress.Address.Type): TextReference {
return when (id.value) { return when (rawId) {
"decimal", "decimal/test" -> { "decimal", "decimal/test" -> {
when (addressType) { when (addressType) {
NetworkAddress.Address.Type.Primary -> stringReference(value = DEL_ADDRESS_NAME) 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.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -41,9 +41,11 @@ data class TokenReceiveBottomSheetConfig(
@Immutable @Immutable
sealed class Asset { sealed class Asset {
abstract val displaySymbol: TextReference abstract val displaySymbol: TextReference
data class Currency(val name: String, val symbol: String) : Asset() { data class Currency(val name: String, val symbol: String) : Asset() {
override val displaySymbol = stringReference(symbol) override val displaySymbol = stringReference(symbol)
} }
data object NFT : Asset() { data object NFT : Asset() {
override val displaySymbol = resourceReference(R.string.common_nft) 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.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.utils.getFiatString 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.extensions.networkIconResId
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.format.bigdecimal.uncapped
import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency 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.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -460,7 +459,7 @@ object NotificationsFactory {
onCloseClick: (Class<out NotificationUM>) -> Unit, onCloseClick: (Class<out NotificationUM>) -> Unit,
) { ) {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO 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 threshold = getTezosThreshold()
val isTotalBalance = enteredAmountValue >= balance && balance > threshold val isTotalBalance = enteredAmountValue >= balance && balance > threshold
if (!ignoreAmountReduce && isTotalBalance && isTezos) { if (!ignoreAmountReduce && isTotalBalance && isTezos) {

View file

@ -141,7 +141,7 @@ class TokenItemStateConverter(
} }
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data) 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 { private fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState {
return when (val value = currencyStatus.value) { 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 */ /** Core shouldn't depend on core, but in case with utils and logging its necessary */
implementation(projects.core.utils) implementation(projects.core.utils)
/** For calculating user id hash */
implementation(tangemDeps.card.core)
} }

View file

@ -1,5 +1,7 @@
package com.tangem.core.analytics 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.api.*
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
@ -19,7 +21,8 @@ interface GlobalAnalyticsEventHandler :
AnalyticsFilterHolder, AnalyticsFilterHolder,
ParamsInterceptorHolder, ParamsInterceptorHolder,
AnalyticsErrorHandler, AnalyticsErrorHandler,
AnalyticsExceptionHandler AnalyticsExceptionHandler,
AnalyticsUserIdHandler
object Analytics : GlobalAnalyticsEventHandler { object Analytics : GlobalAnalyticsEventHandler {
@ -57,6 +60,26 @@ object Analytics : GlobalAnalyticsEventHandler {
return paramsInterceptors.remove(interceptorId) 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) { override fun send(event: AnalyticsEvent) {
analyticsScope.launch { analyticsScope.launch {
event.params = applyParamsInterceptors(event) event.params = applyParamsInterceptors(event)

View file

@ -18,6 +18,11 @@ interface AnalyticsExceptionHandler {
fun sendException(event: ExceptionAnalyticsEvent) fun sendException(event: ExceptionAnalyticsEvent)
} }
interface AnalyticsUserIdHandler {
fun setUserId(userId: String)
fun clearUserId()
}
interface AnalyticsHandler : AnalyticsEventHandler { interface AnalyticsHandler : AnalyticsEventHandler {
fun id(): String 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", "name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined" "version": "undefined"
}, },
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "5.23.0"
},
{ {
"name": "QUOTES_LOADING_REFACTORING_ENABLED", "name": "QUOTES_LOADING_REFACTORING_ENABLED",
"version": "5.24.0" "version": "5.24.0"
@ -62,5 +58,13 @@
{ {
"name": "STAKING_LOADING_REFACTORING_ENABLED", "name": "STAKING_LOADING_REFACTORING_ENABLED",
"version": "5.25.0" "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 */ /** Project */
implementation(projects.core.analytics) implementation(projects.core.analytics)
implementation(projects.core.utils) implementation(projects.core.utils)
implementation(projects.core.res)
implementation(projects.libs.auth) implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models) implementation(projects.domain.appTheme.models)
implementation(projects.domain.core) implementation(projects.domain.core)

View file

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

View file

@ -29,7 +29,13 @@ internal class DevApiConfigsManager(
private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment }) 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() { override fun initialize() {
_isInitialized.value = false
// We can't use appPreferencesStore.getObjectMap as base flow, // We can't use appPreferencesStore.getObjectMap as base flow,
// because we should keep possibility to work with configs synchronous. // because we should keep possibility to work with configs synchronous.
// See [getBaseUrl] // See [getBaseUrl]
@ -42,8 +48,12 @@ internal class DevApiConfigsManager(
savedEnvironments[config.id.name] ?: currentEnvironment 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 { 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.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/** /**
* Implementation of [ApiConfigsManager] in PROD environment * Implementation of [ApiConfigsManager] in PROD environment
@ -13,6 +15,10 @@ internal class ProdApiConfigsManager(
private val apiConfigs: ApiConfigs, private val apiConfigs: ApiConfigs,
) : ApiConfigsManager { ) : ApiConfigsManager {
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
override fun initialize() = Unit
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val config = apiConfigs.firstOrNull { it.id == id } 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") ?: 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( suspend fun updatePushTokenForApplicationId(
@Path("application_id") applicationId: String, @Path("application_id") applicationId: String,
@Body body: NotificationApplicationCreateBody, @Body body: NotificationApplicationCreateBody,
): ApiResponse<String> ): ApiResponse<Unit>
@PATCH("user-wallets/wallets/{wallet_id}/notify") @PATCH("user-wallets/wallets/{wallet_id}/notify")
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit> 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}") @GET("user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse> 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 // endregion
companion object { 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 = "version") val version: Int = 0,
@Json(name = "group") val group: GroupType, @Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType, @Json(name = "sort") val sort: SortType,
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
@Json(name = "tokens") val tokens: List<Token> = emptyList(), @Json(name = "tokens") val tokens: List<Token> = emptyList(),
) { ) {
@ -21,6 +22,7 @@ data class UserTokensResponse(
@Json(name = "symbol") val symbol: String, @Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int, @Json(name = "decimals") val decimals: Int,
@Json(name = "contractAddress") val contractAddress: String?, @Json(name = "contractAddress") val contractAddress: String?,
@Json(name = "addresses") val addresses: List<String>? = null,
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
val otherToken = other as? Token ?: return false val otherToken = other as? Token ?: return false

View file

@ -6,4 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class WalletIdBody( data class WalletIdBody(
@Json(name = "id") val walletId: String, @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 // region: auth
@POST("v1/auth/challenge") @POST("v1/auth/challenge")
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge") @POST("v1/auth/challenge")
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse suspend fun generateNonceByCardWallet(
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token") @POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token") @POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh") @POST("v1/auth/token/refresh")
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse> 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 TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
private val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
)
@Provides @Provides
@Singleton @Singleton
fun provideApiConfigManager( fun provideApiConfigManager(
@ -317,7 +321,7 @@ internal object NetworkModule {
} }
b b
} }
.addLoggers(context) .addLoggers(context = context, id = id)
.clientBuilder() .clientBuilder()
.build(), .build(),
) )
@ -325,6 +329,12 @@ internal object NetworkModule {
.create(T::class.java) .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( private data class Timeouts(
val callTimeoutSeconds: Long? = null, val callTimeoutSeconds: Long? = null,
val connectTimeoutSeconds: 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, bittensorOnfinalityApiKey = value.bittensorOnfinalityKey,
koinosProApiKey = value.koinosProApiKey, koinosProApiKey = value.koinosProApiKey,
alephiumApiKey = value.alephiumTangemApiKey, 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.Json
import com.squareup.moshi.JsonClass 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 dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal 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 { sealed interface NetworkStatusDM {
val networkId: Network.ID /** Network id */
val networkId: ID
/** Derivation path */
val derivationPath: DerivationPath val derivationPath: DerivationPath
/** Selected address */
val selectedAddress: String val selectedAddress: String
/** Available address */
val availableAddresses: Set<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") @NameLabel("amounts")
data class Verified( 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 = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String, @Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>, @Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@Json(name = "amounts") val amounts: Map<String, BigDecimal>, @Json(name = "amounts") val amounts: Map<String, BigDecimal>,
) : NetworkStatusDM ) : 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") @NameLabel("amount_to_create_account")
data class NoAccount( 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 = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String, @Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>, @Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@ -32,6 +66,11 @@ sealed interface NetworkStatusDM {
@Json(name = "error_message") val errorMessage: String, @Json(name = "error_message") val errorMessage: String,
) : NetworkStatusDM ) : NetworkStatusDM
@JsonClass(generateAdapter = true)
data class ID(
@Json(name = "value") val value: String,
)
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class DerivationPath( data class DerivationPath(
@Json(name = "value") val value: String, @Json(name = "value") val value: String,

View file

@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.local.nft.custom.NFTPriceId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
internal class DefaultNFTPersistenceStore( internal class DefaultNFTPersistenceStore(
private val collectionsPersistenceStore: DataStore<List<NFTCollection>>, private val collectionsPersistenceStore: DataStore<List<NFTCollection>>,
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>, private val pricesPersistenceStore: DataStore<List<NFTPriceId>>,
) : NFTPersistenceStore { ) : NFTPersistenceStore {
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data 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 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 override suspend fun getSalePricesSync(): Map<NFTAsset.Identifier, NFTAsset.SalePrice>? = pricesPersistenceStore
.data .data
.firstOrNull() .firstOrNull()
?.associate { it.assetId to it.price }
override suspend fun saveCollections(collections: List<NFTCollection>) { override suspend fun saveCollections(collections: List<NFTCollection>) {
collectionsPersistenceStore.updateData { collectionsPersistenceStore.updateData {
@ -41,9 +43,14 @@ internal class DefaultNFTPersistenceStore(
override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) { override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) {
pricesPersistenceStore.updateData { 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 } 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.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource 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.NFTAsset
import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.domain.tokens.model.Network
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map 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? = private fun NFTCollections.getCollection(collectionId: NFTCollection.Identifier): NFTCollection? =
(content as? NFTCollections.Content.Collections) (content as? NFTCollections.Content.Collections)
?.collections ?.collections
@ -105,7 +115,6 @@ internal class DefaultNFTRuntimeStore(
-> assets -> assets
is NFTCollection.Assets.Value -> assets.copy( is NFTCollection.Assets.Value -> assets.copy(
items = assets.items items = assets.items
.filter { !it.name.isNullOrEmpty() }
.sortedBy { it.name } .sortedBy { it.name }
.map { asset -> .map { asset ->
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id)) asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
@ -122,7 +131,7 @@ internal class DefaultNFTRuntimeStore(
) )
} }
?.filter { it.count > 0 } ?.filter { it.count > 0 }
?.sortedBy { it.name }, ?.sortedBy { it.name?.lowercase() },
source = this.source, source = this.source,
) )

View file

@ -18,4 +18,6 @@ interface NFTPersistenceStore {
suspend fun saveCollections(collections: List<NFTCollection>) suspend fun saveCollections(collections: List<NFTCollection>)
suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) 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.core.DataStoreFactory
import androidx.datastore.dataStoreFile import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.di.NetworkMoshi 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.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithCustomKeyTypes import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext 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_eth_m4460000_prices
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
fileName = "nft_${userWalletStringId}_${networkStringId}_prices", fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(), types = listTypes<NFTPriceId>(),
defaultValue = emptyMap(), defaultValue = emptyList(),
), ),
) )
} }
@ -65,7 +64,7 @@ class NFTPersistenceStoreFactory @Inject constructor(
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
) )
private fun Network.ID.formatted(): String = value private fun Network.ID.formatted(): String = rawId.value
.filter(Char::isLetterOrDigit) .filter(Char::isLetterOrDigit)
.lowercase() .lowercase()

View file

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

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.nft package com.tangem.datasource.local.nft
import com.tangem.datasource.local.datastore.RuntimeSharedStore 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.Inject
import javax.inject.Singleton import javax.inject.Singleton

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