Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 14:50:24 +03:00
commit 50fe9e603d
1919 changed files with 94940 additions and 15178 deletions

View file

@ -10,31 +10,33 @@ android {
}
dependencies {
/* Core */
implementation(projects.core.decompose)
implementation(projects.core.configToggles)
implementation(projects.core.utils)
/* Domain */
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking)
implementation(projects.domain.markets.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.nft.models)
implementation(projects.domain.feedback.models)
implementation(projects.domain.visa.models)
/* Libs - Other */
// region Kotlin
api(deps.kotlin.serialization)
implementation(deps.androidx.core.ktx)
// endregion
/* Tests */
testImplementation(deps.test.junit5)
// region Core
api(projects.core.analytics.models)
api(projects.core.decompose)
api(projects.core.utils)
// endregion
// region Domain
api(projects.domain.appCurrency.models)
api(projects.domain.feedback.models)
api(projects.domain.markets.models)
api(projects.domain.models)
api(projects.domain.nft.models)
api(projects.domain.onramp.models)
api(projects.domain.staking)
api(projects.domain.tokens.models)
implementation(projects.domain.visa.models)
// endregion
// region Tests
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
// endregion
}

View file

@ -6,6 +6,7 @@ import android.annotation.SuppressLint
import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.AddressBookOpenMode
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.navigation.Route
@ -100,9 +101,19 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/details/security")
/**
* Support chat (Usedesk) screen.
*
* @property walletMetaInfo wallet meta info; its user wallet id is sent to Usedesk as the client email
* @property source source of the chat opening (Settings by default, Swap for FR-02). Drives both
* analytics and the Usedesk additional fields mapped by the component.
* @property prefilledMessage optional message sent to the chat on behalf of the user right after init
*/
@Serializable
data class Usedesk(
val walletMetaInfo: WalletMetaInfo,
val source: AnalyticsParam.ScreensSources = AnalyticsParam.ScreensSources.Settings,
val prefilledMessage: String? = null,
) : AppRoute(path = "/usedesk/${walletMetaInfo.userWalletId}")
@Serializable
@ -175,8 +186,15 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class AddressBook(
val predefinedAddress: String? = null,
) : AppRoute(path = "/address_book/predefinedAddress/$predefinedAddress")
val addressBookOpenMode: AddressBookOpenMode = AddressBookOpenMode.Default,
) : AppRoute(
path = when (addressBookOpenMode) {
is AddressBookOpenMode.WithContactCreation ->
"/address_book/${addressBookOpenMode.address}-${addressBookOpenMode.networkId}"
is AddressBookOpenMode.ContactSelection -> "/address_book/select/${addressBookOpenMode.networkId}"
AddressBookOpenMode.Default -> "/address_book"
},
)
@Serializable
data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") {
@ -188,6 +206,7 @@ sealed class AppRoute(val path: String) : Route {
is Send -> "/$networkName"
WalletConnect -> ""
MainScreen -> ""
AddressBook -> ""
}
data class Send(val networkName: String) : Source()
@ -195,6 +214,8 @@ sealed class AppRoute(val path: String) : Route {
data object WalletConnect : Source()
data object MainScreen : Source()
data object AddressBook : Source()
}
}
@ -325,11 +346,6 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
@Serializable
data class SwapCrypto(
val userWalletId: UserWalletId,
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
/**
* Onboarding V2
* @property scanResponse scan response, determines onboarding route by the product type
@ -503,12 +519,33 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data object FromBannerInSettings : Mode()
@Serializable
data object MobileOnboardingDeeplink : Mode()
}
}
@Serializable
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
@Serializable
data class VirtualAccountOnboarding(
val mode: Mode,
) : AppRoute(path = "/virtual_account_onboarding/$mode") {
@Serializable
sealed class Mode {
@Serializable
data class Deeplink(val userWalletId: UserWalletId, val deeplink: String) : Mode()
@Serializable
data class FromMain(val userWalletId: UserWalletId) : Mode()
@Serializable
data class FromDetailsScreen(val userWalletId: UserWalletId) : Mode()
}
}
@Serializable
data class Survey(val token: String, val displayId: String? = null) : AppRoute(path = "/survey")

View file

@ -68,6 +68,10 @@ sealed class DeepLinkRoute {
override val host: String = "onboard-visa"
}
data object OnboardVirtualAccounts : DeepLinkRoute() {
override val host: String = "onboard-virtual-account"
}
data object PayApp : DeepLinkRoute() {
override val host: String = "tangem.com"
}
@ -91,6 +95,10 @@ sealed class DeepLinkRoute {
data object Survey : DeepLinkRoute() {
override val host: String = "survey"
}
data object Campaigns : DeepLinkRoute() {
override val host: String = "campaigns"
}
}
enum class DeepLinkScheme(val scheme: String) {

View file

@ -16,6 +16,7 @@ object DeeplinkConst {
const val PROMO_CODE_KEY = "promo_code"
const val REF_KEY = "ref"
const val CAMPAIGN_KEY = "campaign"
const val CAMPAIGN_ID_KEY = "campaignId"
const val NAME_KEY = "name"
const val ORDER_KEY = "order"
const val INTERVAL_KEY = "interval"

View file

@ -0,0 +1,75 @@
package com.tangem.common.routing.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.OnrampSource
import java.net.URI
/**
* Classification of a marketing-banner deeplink used to decide how a banner tap is routed.
*
* Only the `tangem://` scheme with a swap/buy host triggers contextual in-app routing; everything
* else (external `https://` T&S links, unknown hosts, malformed input) is [EXTERNAL] and handed off
* to the generic deeplink launcher. This mirrors iOS `DefaultIncomingLinkParser`, where
* `https://tangem.com/...` links always resolve to an external link, not an in-app destination.
*/
enum class MarketingDeeplink {
/** `tangem://swap` — open swap for the current token. */
SWAP,
/** `tangem://buy` — open onramp for the current token. */
BUY,
/** External or unrecognized link — route through the generic deeplink launcher. */
EXTERNAL,
}
// TODO: [temporary] Banner taps are intercepted in-host and mapped to an AppRoute directly because the
// shared tangem://swap and tangem://buy deeplinks open context-less screens (generic swap / BuyCrypto
// hub) with no current-token prefill. Replace with targeted swap/onramp deeplinks and drop this
// interception: [REDACTED_JIRA]
/**
* Resolves a marketing-banner [link] into a [MarketingDeeplink]. Never throws: malformed input
* degrades to [MarketingDeeplink.EXTERNAL].
*/
fun resolveMarketingDeeplink(link: String): MarketingDeeplink {
val uri = runCatching { URI(link) }.getOrNull() ?: return MarketingDeeplink.EXTERNAL
if (!uri.scheme.equals(DeepLinkScheme.Tangem.scheme, ignoreCase = true)) {
return MarketingDeeplink.EXTERNAL
}
return when (uri.host) {
DeepLinkRoute.Swap.host -> MarketingDeeplink.SWAP
DeepLinkRoute.Buy.host -> MarketingDeeplink.BUY
else -> MarketingDeeplink.EXTERNAL
}
}
/**
* Builds the contextual in-app route for a marketing-banner deeplink on a token-scoped screen (staking,
* yield, swap, onramp): swap for the current token, or onramp to buy it. Returns `null` for
* [MarketingDeeplink.EXTERNAL] so the caller falls back to the generic deeplink launcher.
*/
fun MarketingDeeplink.toContextualRoute(
userWalletId: UserWalletId,
currency: CryptoCurrency,
screenSource: AnalyticsParam.ScreensSources,
onrampSource: OnrampSource = OnrampSource.MARKETING_BANNER,
): AppRoute? = when (this) {
MarketingDeeplink.SWAP -> AppRoute.Swap(
userWalletId = userWalletId,
fromCryptoCurrency = currency,
screenSource = screenSource.value,
)
MarketingDeeplink.BUY -> AppRoute.Onramp(
userWalletId = userWalletId,
currency = currency,
source = onrampSource,
)
MarketingDeeplink.EXTERNAL -> null
}

View file

@ -0,0 +1,26 @@
package com.tangem.common.routing.entity
import kotlinx.serialization.Serializable
/** How the address book is opened — part of the navigation contract, carried by [com.tangem.common.routing.AppRoute.AddressBook]. */
@Serializable
sealed interface AddressBookOpenMode {
@Serializable
data object Default : AddressBookOpenMode
@Serializable
data class WithContactCreation(
val address: String,
val networkId: String,
) : AddressBookOpenMode
/**
* Opened from the Send flow to pick a recipient. The list is filtered by [networkId] (the current send network),
* and the chosen contact's address is delivered back via `ContactSelectionTrigger` rather than navigation.
*/
@Serializable
data class ContactSelection(
val networkId: String,
) : AddressBookOpenMode
}

View file

@ -0,0 +1,42 @@
package com.tangem.common.routing.deeplink
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class MarketingDeeplinkTest {
@ParameterizedTest
@MethodSource("provideTestModels")
fun resolveMarketingDeeplink(model: ResolveModel) {
// Act
val actual = resolveMarketingDeeplink(model.link)
// Assert
assertThat(actual).isEqualTo(model.expected)
}
internal data class ResolveModel(val link: String, val expected: MarketingDeeplink)
private fun provideTestModels() = listOf(
// tangem:// swap/buy -> contextual
ResolveModel(link = "tangem://swap", expected = MarketingDeeplink.SWAP),
ResolveModel(link = "tangem://buy", expected = MarketingDeeplink.BUY),
ResolveModel(link = "tangem://swap?foo=bar", expected = MarketingDeeplink.SWAP),
ResolveModel(link = "tangem://buy/extra", expected = MarketingDeeplink.BUY),
ResolveModel(link = "TANGEM://swap", expected = MarketingDeeplink.SWAP),
// tangem:// other hosts -> external
ResolveModel(link = "tangem://token", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "tangem://promo", expected = MarketingDeeplink.EXTERNAL),
// https T&S links -> external (iOS treats these as .link too)
ResolveModel(link = "https://tangem.com/swap", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "https://tangem.com/buy", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "https://tangem.com/promo/summer", expected = MarketingDeeplink.EXTERNAL),
// other schemes / garbage -> external
ResolveModel(link = "wc://connect", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "not a uri", expected = MarketingDeeplink.EXTERNAL),
ResolveModel(link = "", expected = MarketingDeeplink.EXTERNAL),
)
}

View file

@ -9,30 +9,36 @@ android {
}
dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.data.common)
implementation(projects.data.staking)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.card)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.libs.blockchainSdk)
implementation(deps.androidx.datastore)
// region Other libraries
implementation(deps.jodatime)
implementation(deps.test.coroutine)
// endregion
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
// region Tangem SDK
api(tangemDeps.blockchain)
api(tangemDeps.card.core)
// endregion
implementation(deps.test.junit5)
implementation(deps.test.truth)
// region Core
api(projects.core.datasource)
implementation(projects.core.utils)
// endregion
// region Data
implementation(projects.data.common)
// endregion
// region Domain
api(projects.domain.card)
api(projects.domain.models)
implementation(projects.domain.wallets)
// endregion
// region Domain models
api(projects.domain.staking.models)
// endregion
// region Libs
api(projects.libs.blockchainSdk)
// endregion
}

View file

@ -22,6 +22,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
private val factory = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains())
val bitcoin by lazy { createCoin(Blockchain.Bitcoin) }
val cardano by lazy { createCoin(blockchain = Blockchain.Cardano) }
val chia by lazy { createCoin(Blockchain.Chia) }
val ethereum by lazy { createCoin(Blockchain.Ethereum) }

View file

@ -9,19 +9,30 @@ android {
}
dependencies {
/** Project - Core */
implementation(projects.core.ui)
implementation(projects.core.utils)
// region Kotlin
api(deps.kotlin.coroutines)
api(deps.kotlin.immutable.collections)
// endregion
/** Compose */
implementation(tangemDeps.vico.core)
implementation(tangemDeps.vico.compose)
implementation(tangemDeps.vico.compose.m3)
implementation(deps.lifecycle.compose)
implementation(deps.compose.foundation)
// region Compose
api(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.kotlin.immutable.collections)
// endregion
// region Other libraries
implementation(deps.androidx.annotation)
implementation(deps.jodatime)
// endregion
// region Vico
implementation(tangemDeps.vico.core)
implementation(tangemDeps.vico.compose)
// endregion
// region Project - Core
implementation(projects.core.ui)
implementation(projects.core.utils)
// endregion
}

View file

@ -1,8 +1,6 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -11,41 +9,59 @@ android {
}
dependencies {
/** Project - Core */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.navigation)
implementation(projects.core.analytics)
/** Project - Common */
implementation(projects.common.uiCharts)
implementation(projects.common.ui)
implementation(projects.common.routing)
// region Kotlin
api(deps.kotlin.coroutines)
api(deps.kotlin.immutable.collections)
// endregion
/** Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)
implementation(projects.domain.staking.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.offramp)
implementation(projects.domain.demo)
implementation(projects.domain.card)
implementation(projects.domain.feedback)
implementation(deps.lifecycle.compose)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
// region Compose
api(deps.compose.foundation)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.material3)
implementation(deps.compose.ui.utils)
implementation(deps.kotlin.immutable.collections)
// endregion
/** DI */
// region Other libraries
implementation(deps.androidx.annotation)
implementation(deps.arrow.core)
implementation(deps.haze)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
/** Test */
testImplementation(projects.test.core)
// region Project - Core
api(projects.core.analytics)
api(projects.core.decompose)
api(projects.core.navigation)
api(projects.core.ui)
api(projects.core.utils)
implementation(projects.core.analytics.models)
// endregion
// region Project - Common
api(projects.common.routing)
api(projects.common.uiCharts)
implementation(projects.common.ui)
// endregion
// region Project - Domain
api(projects.domain.appCurrency.models)
api(projects.domain.card)
api(projects.domain.demo)
api(projects.domain.feedback)
api(projects.domain.models)
api(projects.domain.offramp)
api(projects.domain.tokens)
implementation(projects.domain.onramp.models)
implementation(projects.domain.staking)
implementation(projects.domain.tokens.models)
// endregion
// region Test
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(projects.common.test)
// endregion
}

View file

@ -8,47 +8,70 @@ android {
namespace = "com.tangem.common.ui"
}
dependencies {
api(projects.common)
/** Compose */
implementation(deps.compose.material3)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
// region Kotlin
api(deps.kotlin.immutable.collections)
implementation(deps.kotlin.coroutines)
// endregion
// region Compose
api(deps.compose.foundation)
api(deps.compose.material3)
implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
// endregion
/** Deps */
implementation(deps.kotlin.immutable.collections)
// region Other libraries
api(deps.arrow.core)
implementation(deps.hilt.android)
implementation(deps.androidx.annotation)
implementation(deps.androidx.core.ktx)
implementation(deps.haze)
// endregion
/** Project - Common */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.res)
implementation(projects.libs.crypto)
implementation(projects.libs.blockchainSdk)
/** Project - Domain */
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.domain.card)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.account)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.common)
implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain) {
// region Tangem SDK
api(tangemDeps.blockchain) {
exclude(module = "joda-time")
}
// endregion
/** Tests */
// region Project - Core
api(projects.core.analytics)
api(projects.core.ui)
api(projects.core.utils)
implementation(projects.core.analytics.models)
implementation(projects.core.res)
// endregion
// region Project - Common
// :common is intentionally re-exported (api): common:ui is a ubiquitous UI dependency and many
// feature modules rely on TangemBlogUrlBuilder / common types through it. Demoting to implementation
// cascades across the feature graph, so keep it api despite DAGP's advice (suppressed below).
api(projects.common)
// endregion
// region Project - Domain
api(projects.domain.account)
api(projects.domain.appCurrency.models)
api(projects.domain.common)
api(projects.domain.core)
api(projects.domain.models)
api(projects.domain.onramp.models)
api(projects.domain.staking)
api(projects.domain.tokens.models)
api(projects.domain.transaction.models)
implementation(projects.domain.card)
implementation(projects.domain.staking.models)
// endregion
// region Project - Libs
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)
// endregion
// region Tests
testImplementation(projects.test.core)
// endregion
}

View file

@ -1,8 +1,10 @@
package com.tangem.common.ui.account
import androidx.compose.runtime.Immutable
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
@Immutable
sealed class AccountIconUM {
data class CryptoPortfolio(val value: Icon, val color: Color) : AccountIconUM()

View file

@ -306,7 +306,9 @@ private fun EarnBlockTitle(titleUM: EarnBlockUM.TitleUM, type: Type, modifier: M
iconRes = icon.tone.iconRes(),
tintReference = { icon.tone.tint() },
),
modifier = Modifier.size(TangemTheme.dimens2.x4),
modifier = Modifier
.size(TangemTheme.dimens2.x4)
.testTag(TokenDetailsScreenTestTags.EARN_BLOCK_TITLE_ICON),
)
}
}

View file

@ -1,7 +1,8 @@
package com.tangem.common.ui.navigationButtons
import com.tangem.core.decompose.navigation.Route
interface NavigationModelCallback {
fun onNavigationResult(navigationUM: NavigationUM)
fun onBackClick()
fun onNextClick()
fun onBackClick(currentRoute: Route)
fun onNextClick(currentRoute: Route)
}

View file

@ -244,6 +244,11 @@ sealed class NotificationUM(val config: NotificationConfig) {
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
)
data object HighNetworkFee : Warning(
title = resourceReference(id = R.string.high_fee_warning_title),
subtitle = resourceReference(id = R.string.high_fee_warning_description),
)
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),