Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-05 16:18:11 +03:00
commit c8c64e882b
1244 changed files with 26701 additions and 16971 deletions

3
.gitignore vendored
View file

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

View file

@ -111,6 +111,8 @@ dependencies {
implementation(projects.domain.networks)
implementation(projects.domain.quotes)
implementation(projects.domain.notifications)
implementation(projects.domain.notifications.models)
implementation(projects.domain.notifications.toggles)
implementation(projects.common)
implementation(projects.common.routing)
@ -175,8 +177,6 @@ dependencies {
implementation(projects.features.tokendetails.impl)
implementation(projects.features.manageTokens.api)
implementation(projects.features.manageTokens.impl)
implementation(projects.features.send.api)
implementation(projects.features.send.impl)
implementation(projects.features.sendV2.api)
implementation(projects.features.sendV2.impl)
implementation(projects.features.qrScanning.api)

View file

@ -128,7 +128,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*"
android:host="onramp"
android:scheme="tangem" />
</intent-filter>
@ -150,10 +150,64 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*"
android:host="referral"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="main"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="token"
android:scheme="tangem" />
</intent-filter>
<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="staking"
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="markets"
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_chart"
android:scheme="tangem" />
</intent-filter>
</activity>
<!-- Disable android.startup completely. Used for Worker according doc -->

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

View file

@ -15,6 +15,7 @@ import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
@ -33,7 +34,7 @@ import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
@ -141,5 +142,7 @@ interface ApplicationEntryPoint {
fun getOnlineCardVerifier(): OnlineCardVerifier
fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory
fun getColdUserWalletBuilderFactory(): ColdUserWalletBuilder.Factory
fun getApiConfigsManager(): ApiConfigsManager
}

View file

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

View file

@ -30,6 +30,7 @@ import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
@ -52,7 +53,7 @@ import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
@ -225,8 +226,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val onlineCardVerifier: OnlineCardVerifier
get() = entryPoint.getOnlineCardVerifier()
private val userWalletBuilderFactory: UserWalletBuilder.Factory
get() = entryPoint.getUserWalletBuilderFactory()
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory
get() = entryPoint.getColdUserWalletBuilderFactory()
private val apiConfigsManager: ApiConfigsManager
get() = entryPoint.getApiConfigsManager()
// endregion
@ -275,6 +279,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
}
fun init() {
apiConfigsManager.initialize()
store = createReduxStore()
tangemAppLoggerInitializer.initialize()
@ -285,12 +291,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async {
featureTogglesManager.init()
},
async {
excludedBlockchainsManager.init()
},
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
)
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
@ -366,7 +368,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
settingsManager = settingsManager,
uiMessageSender = uiMessageSender,
onlineCardVerifier = onlineCardVerifier,
userWalletBuilderFactory = userWalletBuilderFactory,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
),
),
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,8 @@ import coil.request.ImageRequest
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.WEBLINK_KEY
import com.tangem.core.deeplink.converter.PayloadToDeeplinkConverter
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
@ -36,9 +38,11 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
val deeplink = PayloadToDeeplinkConverter.convert(message.data)
val intent = Intent(applicationContext, MainActivity::class.java).apply {
putExtra(DEEPLINK_KEY, message.data[DEEPLINK_KEY])
putExtra(DEEPLINK_KEY, deeplink)
putExtra(WEBLINK_KEY, message.data[WEBLINK_KEY])
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}

View file

@ -10,6 +10,7 @@ import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.network.exchangeServices.CardExchangeRules
@ -59,7 +60,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
scope.launch {
val scanResponseProvider: () -> ScanResponse? = {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.selectedUserWalletSync?.scanResponse
userWalletsListManager.selectedUserWalletSync?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
}
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.legacy
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
@ -36,7 +37,8 @@ internal object LegacyMiddleware {
)
store.dispatchWithMain(
DetailsAction.PrepareScreen(
scanResponse = selectedUserWallet.scanResponse,
// TODO [REDACTED_TASK_KEY]
scanResponse = selectedUserWallet.requireColdWallet().scanResponse,
initializedAppSettingsState = initializedAppSettingsStateContent,
),
)

View file

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

View file

@ -1,6 +1,6 @@
package com.tangem.tap.di.data
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.sdk.api.TangemSdkManager
@ -21,9 +21,14 @@ internal object CardDataModule {
fun providesDerivationsRepository(
tangemSdkManager: TangemSdkManager,
userWalletsStore: UserWalletsStore,
excludedBlockchains: ExcludedBlockchains,
networkFactory: NetworkFactory,
dispatchers: CoroutineDispatcherProvider,
): DerivationsRepository {
return DefaultDerivationsRepository(tangemSdkManager, userWalletsStore, excludedBlockchains, dispatchers)
return DefaultDerivationsRepository(
tangemSdkManager = tangemSdkManager,
userWalletsStore = userWalletsStore,
networkFactory = networkFactory,
dispatchers = dispatchers,
)
}
}

View file

@ -10,8 +10,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -70,10 +68,8 @@ internal object ManageTokensDomainModule {
customTokensRepository: CustomTokensRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
derivationsRepository: DerivationsRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
@ -83,10 +79,8 @@ internal object ManageTokensDomainModule {
customTokensRepository = customTokensRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
derivationsRepository = derivationsRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,

View file

@ -13,8 +13,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import dagger.Module
import dagger.Provides
@ -54,16 +52,8 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(
quotesRepository: QuotesRepository,
singleQuoteSupplier: SingleQuoteSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(
quotesRepository = quotesRepository,
singleQuoteSupplier = singleQuoteSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
fun provideGetTokenQuotesUseCase(singleQuoteSupplier: SingleQuoteSupplier): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(singleQuoteSupplier = singleQuoteSupplier)
}
@Provides
@ -72,9 +62,7 @@ object MarketsDomainModule {
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
@ -84,9 +72,7 @@ object MarketsDomainModule {
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository
@ -18,7 +19,9 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -37,9 +40,7 @@ internal object TokensDomainModule {
@Singleton
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
@ -47,9 +48,7 @@ internal object TokensDomainModule {
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
@ -61,8 +60,6 @@ internal object TokensDomainModule {
@Singleton
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -71,8 +68,6 @@ internal object TokensDomainModule {
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
@ -121,8 +116,8 @@ internal object TokensDomainModule {
fun provideGetCurrencyUseCase(
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyStatusUpdatesUseCase {
return GetCurrencyStatusUpdatesUseCase(
): GetSingleCryptoCurrencyStatusUseCase {
return GetSingleCryptoCurrencyStatusUseCase(
currencyStatusOperations = baseCurrencyStatusOperations,
dispatchers = dispatchers,
)
@ -147,7 +142,6 @@ internal object TokensDomainModule {
fun provideGetCurrencyWarningsUseCase(
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
currencyChecksRepository: CurrencyChecksRepository,
dispatchers: CoroutineDispatcherProvider,
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
@ -155,31 +149,16 @@ internal object TokensDomainModule {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
dispatchers = dispatchers,
currencyChecksRepository = currencyChecksRepository,
dispatchers = dispatchers,
currencyStatusOperations = baseCurrencyStatusOperations,
)
}
@Provides
@Singleton
fun provideGetPrimaryCurrencyUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currencyStatusOperations = currencyStatusOperations,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideFetchCurrencyStatusUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -188,8 +167,6 @@ internal object TokensDomainModule {
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
@ -202,8 +179,6 @@ internal object TokensDomainModule {
@Singleton
fun provideFetchCardTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -212,8 +187,6 @@ internal object TokensDomainModule {
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
@ -222,14 +195,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun providesGetCryptoCurrencyStatusSyncUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
): GetCryptoCurrencyStatusSyncUseCase {
return GetCryptoCurrencyStatusSyncUseCase(currencyStatusOperations)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase {
@ -328,14 +293,10 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideUpdateDelayedCurrencyStatusUseCase(
networksRepository: NetworksRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): UpdateDelayedNetworkStatusUseCase {
return UpdateDelayedNetworkStatusUseCase(
networksRepository = networksRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -385,15 +346,11 @@ internal object TokensDomainModule {
@Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -411,9 +368,7 @@ internal object TokensDomainModule {
fun provideBaseCurrenciesStatusesOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -425,9 +380,7 @@ internal object TokensDomainModule {
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
@ -445,9 +398,7 @@ internal object TokensDomainModule {
fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -459,9 +410,7 @@ internal object TokensDomainModule {
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,

View file

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

View file

@ -1,11 +1,14 @@
package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
@ -28,6 +31,17 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@Provides
fun providesUserWalletsSyncDelegate(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsSyncDelegate {
return DefaultUserWalletsSyncDelegate(
userWalletsListManager = userWalletsListManager,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase {
@ -102,10 +116,13 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesRenameWalletUseCase(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
): RenameWalletUseCase {
return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers)
return RenameWalletUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
)
}
@Provides
@ -197,4 +214,68 @@ internal object WalletsDomainModule {
nftFeatureToggles = nftFeatureToggles,
)
}
@Provides
@Singleton
fun providesUpdateRemoteWalletsInfoUseCase(
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
): UpdateRemoteWalletsInfoUseCase {
return UpdateRemoteWalletsInfoUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
)
}
@Provides
@Singleton
fun providesGetSavedWalletChangesIdUseCase(
userWalletsListManager: UserWalletsListManager,
): GetSavedWalletChangesUseCase {
return GetSavedWalletChangesUseCase(
userWalletsListManager = userWalletsListManager,
)
}
@Provides
@Singleton
fun providesAssociateWalletsWithApplicationIdUseCase(
walletsRepository: WalletsRepository,
): AssociateWalletsWithApplicationIdUseCase {
return AssociateWalletsWithApplicationIdUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesSetNotificationsEnabledUseCase(
walletsRepository: WalletsRepository,
currenciesRepository: CurrenciesRepository,
): SetNotificationsEnabledUseCase {
return SetNotificationsEnabledUseCase(
walletsRepository = walletsRepository,
currenciesRepository = currenciesRepository,
)
}
@Provides
@Singleton
fun providesGetWalletNotificationsEnabledUseCase(
walletsRepository: WalletsRepository,
): GetWalletNotificationsEnabledUseCase {
return GetWalletNotificationsEnabledUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesGetIsNotificationsEnabledUseCase(
walletsRepository: WalletsRepository,
): GetIsNotificationsEnabledUseCase {
return GetIsNotificationsEnabledUseCase(
walletsRepository = walletsRepository,
)
}
}

View file

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

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
@ -34,7 +35,7 @@ class TapWalletManager(
}
private suspend fun loadUserWalletData(userWallet: UserWallet) {
Analytics.setContext(userWallet.scanResponse)
Analytics.setContext(userWallet.requireColdWallet().scanResponse) // [REDACTED_TASK_KEY]
val scanResponse = userWallet.scanResponse
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
@ -11,15 +10,16 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.BackendId
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
@ -33,7 +33,7 @@ private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
internal class DefaultDerivationsRepository(
private val tangemSdkManager: TangemSdkManager,
private val userWalletsStore: UserWalletsStore,
private val excludedBlockchains: ExcludedBlockchains,
private val networkFactory: NetworkFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : DerivationsRepository {
@ -41,17 +41,16 @@ internal class DefaultDerivationsRepository(
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
}
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.ID>) {
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
derivePublicKeysByNetworks(
userWalletId = userWalletId,
networks = networkIds.mapNotNull {
getNetwork(
networkFactory.create(
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
extraDerivationPath = null,
scanResponse = userWallet.scanResponse,
excludedBlockchains = excludedBlockchains,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
)
},
)
@ -62,6 +61,8 @@ internal class DefaultDerivationsRepository(
userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
}
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
Timber.d("Nothing to derive")
return
@ -83,17 +84,17 @@ internal class DefaultDerivationsRepository(
): Boolean {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
getNetwork(
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
extraDerivationPath = extraDerivationPath,
scanResponse = userWallet.scanResponse,
excludedBlockchains = excludedBlockchains,
)
},
)
val derivations =
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY]
.findByNetworks(
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
networkFactory.create(
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
extraDerivationPath = extraDerivationPath,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
)
},
)
return derivations.isNotEmpty()
}
@ -108,7 +109,8 @@ internal class DefaultDerivationsRepository(
).doOnSuccess { response ->
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
.doOnSuccess {
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
// TODO [REDACTED_TASK_KEY]
validateDerivations(scanResponse = it.requireColdWallet().scanResponse, derivations = derivations)
return response.entries
}
.doOnFailure { throw it }
@ -138,12 +140,12 @@ internal class DefaultDerivationsRepository(
return withContext(dispatchers.io) {
userWalletsStore.update(
userWalletId = userWalletId,
update = { userWallet -> userWallet.updateDerivedKeys(keys) },
update = { userWallet -> userWallet.requireColdWallet().updateDerivedKeys(keys) }, // TODO [REDACTED_TASK_KEY]
)
}
}
private fun UserWallet.updateDerivedKeys(keys: DerivedKeys): UserWallet {
private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet {
return copy(
scanResponse = scanResponse.copy(
derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,8 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isLocked
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
@ -206,6 +208,7 @@ internal class BiometricUserWalletsListManager(
changeSelectedUserWallet: Boolean,
canOverridePublicInfo: Boolean,
): CompletionResult<Unit> {
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
val encryptionKey = userWallet.scanResponse.card.encryptionKey
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
?: return CompletionResult.Success(Unit) // No encryption key, no need to save

View file

@ -6,28 +6,34 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
internal val UserWallet.sensitiveInformation: UserWalletSensitiveInformation
get() = UserWalletSensitiveInformation(
wallets = scanResponse.card.wallets,
visaCardActivationStatus = scanResponse.visaCardActivationStatus,
)
get() = when (this) {
is UserWallet.Cold -> UserWalletSensitiveInformation(
wallets = scanResponse.card.wallets,
visaCardActivationStatus = scanResponse.visaCardActivationStatus,
)
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
}
internal val UserWallet.publicInformation: UserWalletPublicInformation
get() = UserWalletPublicInformation(
name = name,
walletId = walletId,
cardsInWallet = cardsInWallet,
isMultiCurrency = isMultiCurrency,
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = emptyList(),
get() = when (this) {
is UserWallet.Cold -> UserWalletPublicInformation(
name = name,
walletId = walletId,
cardsInWallet = cardsInWallet,
isMultiCurrency = isMultiCurrency,
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = emptyList(),
),
visaCardActivationStatus = null,
),
visaCardActivationStatus = null,
),
hasBackupError = hasBackupError,
)
hasBackupError = hasBackupError,
)
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
}
internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
return UserWallet(
return UserWallet.Cold(
name = name,
walletId = walletId,
cardsInWallet = cardsInWallet,
@ -42,14 +48,19 @@ internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet>
}
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
return copy(
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = sensitiveInformation.wallets,
),
visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
),
)
return when (this) {
is UserWallet.Cold -> {
copy(
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = sensitiveInformation.wallets,
),
visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
),
)
}
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
}
}
internal fun List<UserWallet>.updateWith(
@ -68,11 +79,16 @@ internal fun List<UserWallet>.updateWith(
internal fun List<UserWallet>.lockAll(): List<UserWallet> = map(UserWallet::lock)
internal fun UserWallet.lock(): UserWallet = copy(
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = emptyList(),
),
visaCardActivationStatus = null,
),
)
internal fun UserWallet.lock(): UserWallet = when (this) {
is UserWallet.Cold -> {
copy(
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = emptyList(),
),
visaCardActivationStatus = null,
),
)
}
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
}

View file

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

View file

@ -17,6 +17,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
@ -133,7 +134,7 @@ class WalletConnectSdkHelper {
fun isDemoCard(): Boolean {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return false
return userWallet.scanResponse.isDemoCard()
return userWallet is UserWallet.Cold && userWallet.scanResponse.isDemoCard()
}
private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {

View file

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

View file

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

View file

@ -1,10 +1,10 @@
package com.tangem.tap.domain.walletconnect2.domain
import arrow.core.flatten
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session
@ -12,6 +12,9 @@ import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsReposit
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.filterNotNull
@ -139,7 +142,11 @@ class WalletConnectInteractor(
if (deeplinkStack.empty()) return
val lastDeeplink = deeplinkStack.pop()
val action = WalletConnectAction
.OpenSession(lastDeeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
.OpenSession(
wcUri = lastDeeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
}.onFailure {
Timber.e("WC deeplink handling failed. $it")
@ -393,7 +400,11 @@ class WalletConnectInteractor(
}
if (isWalletConnectReadyForDeepLinks) {
val action = WalletConnectAction.OpenSession(deeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
val action = WalletConnectAction.OpenSession(
wcUri = deeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
} else {
deeplinkStack.push(deeplink)
@ -401,6 +412,7 @@ class WalletConnectInteractor(
}
private fun getCardId(userWallet: UserWallet): String? {
userWallet.requireColdWallet() // [REDACTED_TASK_KEY]
return if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else { // if wallet has backup, any card from wallet can be used to sign
@ -410,7 +422,8 @@ class WalletConnectInteractor(
private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List<Network>): List<Account> {
val walletManagers = networks.mapNotNull {
val blockchain = Blockchain.fromId(it.id.value)
val blockchain = it.toBlockchain()
walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -86,7 +87,8 @@ internal class CardSettingsModel @Inject constructor(
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet $userWalletId not found") }
cardSdkConfigRepository.isBiometricsRequestPolicy = userWallet.scanResponse.card.isAccessCodeSet &&
cardSdkConfigRepository.isBiometricsRequestPolicy =
userWallet.requireColdWallet().scanResponse.card.isAccessCodeSet && // TODO [REDACTED_TASK_KEY]
settingsRepository.shouldSaveAccessCodes()
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -59,6 +60,7 @@ internal class ResetCardModel @Inject constructor(
// Use only for card-specific data
private val userWallet = getUserWalletUseCase(userWalletId = currentUserWalletId)
.getOrElse { error("Failed to get user wallet: $it") }
.requireColdWallet()
private val currentCardTypesResolver = userWallet.cardTypesResolver
private val currentUserCodeParams = ResetCardUserCodeParams(

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
@ -53,7 +53,7 @@ internal class HomeModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
getUserCountryUseCase: GetUserCountryUseCase,
) : Model() {
@ -117,7 +117,7 @@ internal class HomeModel @Inject constructor(
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = userWalletBuilderFactory.create(scanResponse = scanResponse).build()
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")

View file

@ -100,7 +100,7 @@ private suspend fun readCard() {
}
private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse)
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
val userWallet = userWalletBuilder.build().guard {
Timber.e("User wallet not created")

View file

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

View file

@ -1,15 +1,16 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
@ -79,7 +80,7 @@ object TradeCryptoMiddleware {
val status = action.cryptoCurrencyStatus
val currency = status.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
val blockchain = currency.network.toBlockchain()
val exchangeManager = store.state.globalState.exchangeManager
val topUrl = exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
@ -104,7 +105,7 @@ object TradeCryptoMiddleware {
}
buyErc20TestnetTokens(
card = action.userWallet.scanResponse.card,
card = action.userWallet.requireColdWallet().scanResponse.card, // TODO [REDACTED_TASK_KEY]
walletManager = walletManager,
destinationAddress = currency.contractAddress,
)

View file

@ -15,6 +15,7 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.*
@ -94,7 +95,7 @@ internal class WelcomeMiddleware {
}
.doOnSuccess { selectedUserWallet ->
sendSignedInAnalyticsEvent(
scanResponse = selectedUserWallet.scanResponse,
scanResponse = selectedUserWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
signInType = Basic.SignedIn.SignInType.Biometric,
)
@ -117,7 +118,7 @@ internal class WelcomeMiddleware {
)
scanCardInternal { scanResponse ->
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse)
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
val userWallet = userWalletBuilder.build() ?: return@scanCardInternal

View file

@ -3,14 +3,24 @@ package com.tangem.tap.network.auth
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider {
override fun getCardPublicKey(): String {
return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""
return userWalletsListManager.selectedUserWalletSync
?.requireColdWallet()?.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""
}
override fun getCardId(): String {
return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: ""
return userWalletsListManager.selectedUserWalletSync
?.requireColdWallet()?.scanResponse?.card?.cardId ?: ""
}
override fun getCardsPublicKeys(): Map<String, String> {
return userWalletsListManager.userWalletsSync.filterIsInstance<UserWallet.Cold>().associate {
it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString()
}
}
}

View file

@ -3,8 +3,10 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -25,6 +27,7 @@ internal class CryptoCurrencyConverter(
extraDerivationPath = value.derivationPath,
scanResponse = requireNotNull(
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync
?.requireColdWallet() // TODO [REDACTED_TASK_KEY]
?.scanResponse,
),
),
@ -36,6 +39,7 @@ internal class CryptoCurrencyConverter(
extraDerivationPath = value.derivationPath,
scanResponse = requireNotNull(
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync
?.requireColdWallet() // TODO [REDACTED_TASK_KEY]
?.scanResponse,
),
),
@ -44,7 +48,7 @@ internal class CryptoCurrencyConverter(
}
override fun convertBack(value: CryptoCurrency): Currency {
val blockchain = Blockchain.fromId(value.network.id.value)
val blockchain = value.network.toBlockchain()
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) {
is CryptoCurrency.Coin -> Currency.Blockchain(

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package com.tangem.tap.network.exchangeServices.mercuryo
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
@ -10,8 +10,8 @@ import com.tangem.data.onramp.legacy.mercuryoNetwork
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
@ -77,7 +77,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
val blockchain = cryptoCurrency.network.toBlockchain()
val builder = Uri.Builder()
.scheme(ExchangeUrlBuilder.SCHEME)

View file

@ -2,7 +2,7 @@ package com.tangem.tap.network.exchangeServices.moonpay
import android.net.Uri
import android.util.Base64
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
@ -10,8 +10,8 @@ import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
@ -133,7 +133,7 @@ class MoonPayService(
): String? {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException()
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
val blockchain = cryptoCurrency.network.toBlockchain()
val supportedCurrency = blockchain.moonPaySupportedCurrency ?: return null
val moonpayCurrency = status?.availableForSell?.firstOrNull {
when (cryptoCurrency) {

View file

@ -27,7 +27,7 @@ import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
@ -78,5 +78,5 @@ data class DaggerGraphState(
val uiMessageSender: UiMessageSender? = null,
val onlineCardVerifier: OnlineCardVerifier? = null,
val cardArworksProvider: CardArtworksProvider? = null,
val userWalletBuilderFactory: UserWalletBuilder.Factory? = null,
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
) : StateType

View file

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

View file

@ -12,18 +12,19 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
@ -47,6 +48,7 @@ internal class ChildFactory @Inject constructor(
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val marketsTokenListComponentFactory: MarketsTokenListComponent.Factory,
private val onrampComponentFactory: OnrampComponent.Factory,
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
@ -55,7 +57,6 @@ internal class ChildFactory @Inject constructor(
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val welcomeComponentFactory: WelcomeComponent.Factory,
private val storiesComponentFactory: StoriesComponent.Factory,
private val sendComponentFactory: SendComponent.Factory,
private val stakingComponentFactory: StakingComponent.Factory,
private val swapComponentFactory: SwapComponent.Factory,
private val homeComponentFactory: HomeComponent.Factory,
@ -71,9 +72,8 @@ internal class ChildFactory @Inject constructor(
private val referralComponentFactory: ReferralComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
private val walletComponentFactory: WalletEntryComponent.Factory,
private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory,
private val sendFeatureToggles: SendFeatureToggles,
private val redesignedWalletConnectComponentFactory: RedesignedWalletConnectComponent.Factory,
private val sendComponentFactoryV2: SendComponent.Factory,
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
private val nftComponentFactory: NFTComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val testerRouter: TesterRouter,
@ -252,33 +252,18 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.Send -> {
if (sendFeatureToggles.isSendV2Enabled) {
createComponentChild(
context = context,
params = com.tangem.features.send.v2.api.SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactoryV2,
)
} else {
createComponentChild(
context = context,
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
}
createComponentChild(
context = context,
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactoryV2,
)
}
is AppRoute.Home -> {
createComponentChild(
@ -291,13 +276,13 @@ internal class ChildFactory @Inject constructor(
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
createComponentChild(
context = context,
params = Unit,
params = RedesignedWalletConnectComponent.Params(route.userWalletId),
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
createComponentChild(
context = context,
params = Unit,
params = WalletConnectComponent.Params(route.userWalletId),
componentFactory = walletConnectComponentFactory,
)
}
@ -406,6 +391,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = nftSendComponentFactory,
)
}
is AppRoute.Markets -> {
createComponentChild(
context = context,
params = Unit,
componentFactory = marketsTokenListComponentFactory,
)
}
}
}
}

View file

@ -0,0 +1,143 @@
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.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
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.staking.api.deeplink.StakingDeepLinkHandler
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 com.tangem.utils.extensions.uriValidate
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 stakingDeepLink: StakingDeepLinkHandler.Factory,
private val marketsDeepLink: MarketsDeepLinkHandler.Factory,
private val marketsTokenDetailDeepLink: MarketsTokenDetailDeepLinkHandler.Factory,
) {
private val permittedAppRoute = MutableStateFlow(false)
private var lastDeepLink: Uri? = null
private val deepLinkHandlerJobHolder = JobHolder()
@OptIn(ExperimentalCoroutinesApi::class)
fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) {
lastDeepLink = deeplinkUri
Timber.i(
"""
Received deep link intent
|- Received URI: $deeplinkUri
""".trimIndent(),
)
permittedAppRoute
.transformLatest<Boolean, Unit> { isPermitted ->
if (isPermitted) {
lastDeepLink?.let {
launchDeepLink(it, coroutineScope, isFromOnNewIntent)
}
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, isFromOnNewIntent: Boolean) {
when (deeplinkUri.scheme) {
DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope, isFromOnNewIntent)
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, isFromOnNewIntent: Boolean) {
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 = coroutineScope,
queryParams = queryParams,
isFromOnNewIntent = isFromOnNewIntent,
)
DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Markets.host -> marketsDeepLink.create()
DeepLinkRoute.MarketTokenDetail.host -> marketsTokenDetailDeepLink.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.uriValidate() && paramValue?.uriValidate() == true) {
params[paramName] = paramValue
}
}
return params
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.CompletionResult
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.configs.GenericCardConfig
@ -32,11 +33,11 @@ internal class DefaultDerivationsRepositoryTest {
tangemSdkManager = tangemSdkManager,
userWalletsStore = userWalletsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
excludedBlockchains = ExcludedBlockchains(),
networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()),
)
private val defaultUserWalletId = UserWalletId("011")
private val defaultUserWallet = UserWallet(
private val defaultUserWallet = UserWallet.Cold(
name = "",
walletId = defaultUserWalletId,
cardsInWallet = setOf(),

View file

@ -191,7 +191,7 @@ internal class BiometricUserWalletsListManagerTest(private val model: Model) {
)
fun createUserWallet(id: String, isLocked: Boolean): UserWallet {
return UserWallet(
return UserWallet.Cold(
name = "Wallet $id",
walletId = UserWalletId(stringValue = id),
cardsInWallet = emptySet(),

View file

@ -0,0 +1,364 @@
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.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
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.staking.api.deeplink.StakingDeepLinkHandler
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(), any()) } returns mockk()
}
private val stakingDeepLinkFactory = mockk<StakingDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val marketsDeepLinkFactory = mockk<MarketsDeepLinkHandler.Factory>(relaxed = true) {
every { create() } returns mockk()
}
private val marketsTokenDetailDeepLinkFactory = mockk<MarketsTokenDetailDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val mockedUri = mockk<Uri>(relaxed = true)
private val isFromOnNewIntent: Boolean = false
private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
private val deepLinkFactory = DeepLinkFactory(
onrampDeepLinkFactory,
sellDeepLinkFactory,
buyDeepLinkFactory,
referralDeepLinkFactory,
walletConnectDeepLinkFactory,
walletDeepLinkFactory,
tokenDetailsDeepLinkFactory,
stakingDeepLinkFactory,
marketsDeepLinkFactory,
marketsTokenDetailDeepLinkFactory,
)
@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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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(), 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, isFromOnNewIntent)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Test Sell
every { mockedUri.host } returns "redirect_sell"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { sellDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Test Token Details
every { mockedUri.host } returns "token"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify {
tokenDetailsDeepLinkFactory.create(
eq(testScope),
eq(mapOf("param" to "value")),
eq(isFromOnNewIntent),
)
}
// Test Staking
every { mockedUri.host } returns "staking"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { stakingDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
// Test Market Token Detail
every { mockedUri.host } returns "token_chart"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { marketsTokenDetailDeepLinkFactory.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, isFromOnNewIntent)
advanceUntilIdle()
verify { buyDeepLinkFactory.create(eq(testScope)) }
// Test Referral
every { mockedUri.host } returns "referral"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { referralDeepLinkFactory.create() }
// Test Wallet
every { mockedUri.host } returns "main"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { walletDeepLinkFactory.create() }
// Test Markets
every { mockedUri.host } returns "markets"
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { marketsDeepLinkFactory.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, isFromOnNewIntent)
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(), 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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
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, isFromOnNewIntent)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
}
}

View file

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

View file

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

View file

@ -0,0 +1,47 @@
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"
}
data object Staking : DeepLinkRoute() {
override val host: String = "staking"
}
data object Markets : DeepLinkRoute() {
override val host: String = "markets"
}
data object MarketTokenDetail : DeepLinkRoute() {
override val host: String = "token_chart"
}
}
enum class DeepLinkScheme(val scheme: String) {
Tangem(scheme = "tangem"),
WalletConnect(scheme = "wc"),
}

View file

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

View file

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

View file

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

View file

@ -7,13 +7,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetworkDerivationPath
import com.tangem.data.common.currency.getNetworkStandardType
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
/**
[REDACTED_AUTHOR]
@ -47,16 +46,18 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
}
fun createCoin(blockchain: Blockchain): CryptoCurrency {
val derivationPath = createDerivationPath(
blockchain = blockchain,
extraDerivationPath = null,
cardDerivationStyleProvider = scanResponse.derivationStyleProvider,
)
val network = Network(
id = Network.ID(blockchain.id),
id = Network.ID(blockchain.id, derivationPath),
backendId = blockchain.toNetworkId(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(
blockchain = blockchain,
extraDerivationPath = null,
cardDerivationStyleProvider = scanResponse.derivationStyleProvider,
),
derivationPath = derivationPath,
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
@ -69,6 +70,12 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
// Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood
fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency {
val derivationPath = Network.DerivationPath.Custom(
value = derivationBlockchain.derivationPath(
scanResponse.derivationStyleProvider.getDerivationStyle(),
)!!.rawPath,
)
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
@ -76,15 +83,11 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
suffix = CryptoCurrency.ID.Suffix.RawID(blockchain.id),
),
network = Network(
id = Network.ID(value = blockchain.id),
id = Network.ID(value = blockchain.id, derivationPath),
backendId = "NEVER-MIND",
name = blockchain.fullName,
currencySymbol = "NEVER-MIND",
derivationPath = Network.DerivationPath.Custom(
value = derivationBlockchain.derivationPath(
scanResponse.derivationStyleProvider.getDerivationStyle(),
)!!.rawPath,
),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = true,
@ -102,13 +105,60 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
fun createToken(blockchain: Blockchain): CryptoCurrency {
return factory.createToken(
sdkToken = Token(symbol = "NEVER-MIND", contractAddress = "NEVER-MIND", decimals = 8),
sdkToken = Token(
name = "NEVER-MIND",
symbol = "NEVER-MIND",
contractAddress = "NEVER-MIND",
decimals = 8,
id = "NEVER-MIND",
),
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)!!
}
private fun createDerivationPath(
blockchain: Blockchain,
extraDerivationPath: String?,
cardDerivationStyleProvider: DerivationStyleProvider?,
): Network.DerivationPath {
if (cardDerivationStyleProvider == null) return Network.DerivationPath.None
val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider)
return if (extraDerivationPath.isNullOrBlank()) {
if (defaultDerivationPath.isNullOrBlank()) {
Network.DerivationPath.None
} else {
Network.DerivationPath.Card(defaultDerivationPath)
}
} else {
if (extraDerivationPath == defaultDerivationPath) {
Network.DerivationPath.Card(defaultDerivationPath)
} else {
Network.DerivationPath.Custom(extraDerivationPath)
}
}
}
private fun getDefaultDerivationPath(
blockchain: Blockchain,
derivationStyleProvider: DerivationStyleProvider,
): String? {
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
}
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
else -> Network.StandardType.Unspecified(blockchain.name)
}
}
private companion object {
val defaultScanResponse = MockScanResponseFactory.create(

View file

@ -17,10 +17,10 @@ object MockUserWalletFactory {
derivedKeys = emptyMap(),
)
fun create(scanResponse: ScanResponse = defaultScanResponse): UserWallet {
fun create(scanResponse: ScanResponse = defaultScanResponse): UserWallet.Cold {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()!!
return UserWallet(
return UserWallet.Cold(
walletId = userWalletId,
name = "Wallet 1",
cardsInWallet = emptySet(),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -165,7 +165,7 @@ sealed class NotificationUM(val config: NotificationConfig) {
subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text),
)
data object DestinationMemoRequired : Error(
data object DestinationTagRequired : Error(
title = resourceReference(id = R.string.send_validation_destination_tag_required_title),
subtitle = resourceReference(id = R.string.send_validation_destination_tag_required_description),
)

View file

@ -4,14 +4,13 @@ import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.common.ui.notifications.NotificationUM.Warning
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.uncapped
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -354,9 +353,7 @@ object NotificationsFactory {
error = validationError,
onReduceClick = onReduceClick,
)
is BlockchainSdkError.XRP -> addXRPTransactionValidationError(
error = validationError,
)
is BlockchainSdkError.DestinationTagRequired -> addRequireDestinationTagErrorNotification()
null -> minAdaValue?.let {
add(
NotificationUM.Cardano.MinAdaValueCharged(
@ -437,12 +434,6 @@ object NotificationsFactory {
}
}
private fun MutableList<NotificationUM>.addXRPTransactionValidationError(error: BlockchainSdkError.XRP) {
when (error) {
is BlockchainSdkError.XRP.DestinationMemoRequired -> addRequireDestinationFlagErrorNotification()
}
}
fun MutableList<NotificationUM>.addRentExemptionNotification(rentWarning: CryptoCurrencyWarning.Rent?) {
if (rentWarning == null) return
add(NotificationUM.Solana.RentInfo(rentWarning))
@ -460,7 +451,7 @@ object NotificationsFactory {
onCloseClick: (Class<out NotificationUM>) -> Unit,
) {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.rawId)
val threshold = getTezosThreshold()
val isTotalBalance = enteredAmountValue >= balance && balance > threshold
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
@ -513,7 +504,7 @@ object NotificationsFactory {
}
}
private fun MutableList<NotificationUM>.addRequireDestinationFlagErrorNotification() {
add(NotificationUM.Error.DestinationMemoRequired)
private fun MutableList<NotificationUM>.addRequireDestinationTagErrorNotification() {
add(NotificationUM.Error.DestinationTagRequired)
}
}

View file

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

View file

@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -15,7 +15,6 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -30,10 +29,7 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.wallets.models.UserWalletId
@ -93,7 +89,7 @@ fun UserWalletItem(
@Composable
private fun NameAndInfo(
name: TextReference,
information: TextReference,
information: UserWalletItemUM.Information,
balance: UserWalletItemUM.Balance,
modifier: Modifier = Modifier,
) {
@ -113,8 +109,28 @@ private fun NameAndInfo(
Row(
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedContent(
targetState = information,
label = "Information content",
) { information ->
val informationValue = getInformationValue(information)
if (informationValue == null) {
TextShimmer(
style = TangemTheme.typography.caption2,
text = "aaaaa",
)
} else {
Text(
text = informationValue,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
)
}
}
Text(
text = information.resolveReference() + " $DOT ",
text = " $DOT ",
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
@ -198,11 +214,20 @@ fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair<Stri
is UserWalletItemUM.Balance.Failed -> DASH_SIGN to false
is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false
is UserWalletItemUM.Balance.Loading -> null to false
is UserWalletItemUM.Balance.Locked -> stringResource(R.string.common_locked) to false
is UserWalletItemUM.Balance.Locked -> stringResourceSafe(R.string.common_locked) to false
is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering
}
}
@Composable
fun getInformationValue(information: UserWalletItemUM.Information): String? {
return when (information) {
UserWalletItemUM.Information.Failed -> DASH_SIGN
UserWalletItemUM.Information.Loading -> null
is UserWalletItemUM.Information.Loaded -> information.value.resolveReference()
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@ -255,6 +280,15 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Loading,
balance = UserWalletItemUM.Balance.Loading,
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
@ -279,14 +313,37 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Loading,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Failed,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
isEnabled = true,
onClick = {},
),
)
private fun getInformation(cardCount: Int): TextReference {
return TextReference.PluralRes(
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {
val text = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return UserWalletItemUM.Information.Loaded(text)
}
}
// endregion Preview

View file

@ -15,6 +15,8 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isLocked
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.converter.Converter
/**
@ -53,13 +55,15 @@ class UserWalletItemUMConverter(
}
}
private fun getInfo(userWallet: UserWallet): TextReference {
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
userWallet.requireColdWallet()
val cardCount = userWallet.getCardsCount() ?: 1
return TextReference.PluralRes(
val text = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return UserWalletItemUM.Information.Loaded(text)
}
private fun getBalanceInfo(userWallet: UserWallet): UserWalletItemUM.Balance {

View file

@ -9,7 +9,7 @@ import javax.annotation.concurrent.Immutable
data class UserWalletItemUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val information: Information,
val balance: Balance,
val imageState: ImageState = ImageState.Loading,
val isEnabled: Boolean,
@ -38,6 +38,17 @@ data class UserWalletItemUM(
) : Balance()
}
sealed class Information {
data object Failed : Information()
data object Loading : Information()
data class Loaded(
val value: TextReference,
) : Information()
}
@Immutable
sealed class ImageState {

View file

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

View file

@ -217,5 +217,7 @@ sealed class AnalyticsParam {
const val PAYMENT_METHOD = "Payment Method"
const val WATCHED = "Watched"
const val ACTION = "Action"
const val COLLECTIONS = "Collections"
const val NFT = "Nft"
}
}

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
},
{
"name": "ONRAMP_ENABLED",
"version": "undefined"
"version": "5.24.1"
},
{
"name": "VISA_ONBOARDING_ENABLED",
@ -28,12 +28,12 @@
"version": "undefined"
},
{
"name": "STAKING_CARDANO_ENABLED",
"name": "NFT_MEDIA_CONTENT_ENABLED",
"version": "undefined"
},
{
"name": "TX_HISTORY_REFACTORING_ENABLED",
"version": "5.22.0"
"name": "STAKING_CARDANO_ENABLED",
"version": "undefined"
},
{
"name": "NEW_ARTWORK_LOADING",
@ -43,24 +43,24 @@
"name": "NEW_ATTESTATION_ENABLED",
"version": "5.24.0"
},
{
"name": "SEND_V2_ENABLED",
"version": "5.23.0"
},
{
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "QUOTES_LOADING_REFACTORING_ENABLED",
"version": "5.24.0"
},
{
"name": "STAKING_LOADING_REFACTORING_ENABLED",
"version": "5.25.0"
},
{
"name": "DEEPLINK_NAVIGATION_ENABLED",
"version": "5.25.0"
},
{
"name": "PUSH_NOTIFICATIONS_ENABLED",
"version": "undefined"
},
{
"name": "USEDESK_FEEDBACK_ENABLED",
"version": "undefined"
}
]

View file

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

View file

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

View file

@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
data class AccountSummaryResponse(
@Json(name = "assets_diffs") val assetsDiffs: List<AssetDiff>,
@Json(name = "exposures") val exposures: List<Exposure>,
@Json(name = "traces") val traces: List<Trace>?,
)

View file

@ -16,10 +16,11 @@ data class Asset(
@Json(name = "chain_id") val chainId: Int? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
)
@JsonClass(generateAdapter = true)
data class Transfer(
@Json(name = "value") val value: String,
@Json(name = "raw_value") val rawValue: String,
@Json(name = "value") val value: String?,
@Json(name = "raw_value") val rawValue: String?,
)

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