Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-15 18:55:31 +04:00
parent 7503c2817c
commit d6c6cef5f9
30 changed files with 49 additions and 437 deletions

View file

@ -134,7 +134,6 @@ dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.decompose)
implementation(projects.core.deepLinks)
implementation(projects.core.error.ext)
implementation(projects.libs.crypto)
implementation(projects.libs.auth)

View file

@ -26,13 +26,12 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
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.DeepLinksRegistry
import com.tangem.core.deeplink.WEBLINK_KEY
import com.tangem.core.deeplink.converter.PayloadToDeeplinkConverter
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.data.card.sdk.CardSdkOwner
@ -116,9 +115,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@Inject
lateinit var deepLinksRegistry: DeepLinksRegistry
@Inject
lateinit var settingsRepository: SettingsRepository

View file

@ -30,4 +30,10 @@ dependencies {
api(deps.kotlin.serialization)
implementation(deps.androidx.core.ktx)
implementation(deps.timber)
/* Tests */
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -1,51 +1,40 @@
package com.tangem.core.deeplink.converter
package com.tangem.common.routing.deeplink
import com.tangem.core.deeplink.DeeplinkConst.TANGEM_SCHEME
import com.tangem.common.routing.deeplink.DeeplinkConst.TANGEM_SCHEME
/**
* Builder class for constructing deep links with a fluent interface.
*/
/** Builder class for constructing deep links with a fluent interface */
internal class DeepLinkBuilder {
private var scheme: String = TANGEM_SCHEME
private var action: String = ""
private val pathParams: MutableList<String> = mutableListOf()
private val queryParams: MutableMap<String, String> = mutableMapOf()
/**
* Sets the scheme for the deep link (e.g., "tangem", "https")
*/
/** Sets the scheme for the deep link (e.g., "tangem", "https") */
fun setScheme(scheme: String): DeepLinkBuilder {
this.scheme = scheme
return this
}
/**
* Sets the action for the deep link (e.g., "link", "wallet")
*/
/** Sets the action for the deep link (e.g., "link", "wallet") */
fun setAction(action: String): DeepLinkBuilder {
this.action = action
return this
}
/**
* Adds a path parameter to the deep link
*/
/** Adds a path parameter to the deep link */
fun addPathParam(param: String): DeepLinkBuilder {
pathParams.add(param)
return this
}
/**
* Adds a query parameter to the deep link
*/
/** Adds a query parameter to the deep link */
fun addQueryParam(key: String, value: String): DeepLinkBuilder {
queryParams[key] = value
return this
}
/**
* Builds the deep link URI string
*/
/** Builds the deep link URI string */
fun build(): String {
val path = if (pathParams.isEmpty()) {
action

View file

@ -1,6 +1,9 @@
package com.tangem.core.deeplink
package com.tangem.common.routing.deeplink
object DeeplinkConst {
const val DEEPLINK_KEY = "deeplink"
const val WEBLINK_KEY = "link"
const val TANGEM_SCHEME = "tangem"
const val WALLET_ID_KEY = "user_wallet_id"
const val NETWORK_ID_KEY = "network_id"

View file

@ -1,16 +1,16 @@
package com.tangem.core.deeplink.converter
package com.tangem.common.routing.deeplink
import android.os.Bundle
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.core.deeplink.DeeplinkConst.NAME_KEY
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NAME_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.utils.converter.Converter
object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {

View file

@ -1,7 +1,6 @@
package com.tangem.core.deeplink.converter
package com.tangem.common.routing.deeplink
import com.google.common.truth.Truth.assertThat
import com.tangem.core.deeplink.DeeplinkConst
import org.junit.Before
import org.junit.Test

View file

@ -1,12 +1,12 @@
package com.tangem.core.deeplink.converter
package com.tangem.common.routing.deeplink
import com.google.common.truth.Truth.assertThat
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import org.junit.Test
internal class PayloadToDeeplinkConverterTest {

View file

@ -1 +0,0 @@
/build

View file

@ -1,34 +0,0 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.core.deeplink"
}
dependencies {
/* Common */
implementation(projects.common.routing)
/* Core */
implementation(projects.core.decompose)
/* Libs - AndroidX */
implementation(deps.lifecycle.runtime.ktx)
/* Libs - Other */
implementation(deps.timber)
/* DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/* Tests */
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,15 +0,0 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.core.deeplink.global"
}
dependencies {
/* Project */
implementation(projects.core.deepLinks)
}

View file

@ -1,36 +0,0 @@
package com.tangem.core.deeplink
/**
* Represents a deep link.
*/
abstract class DeepLink(val shouldHandleDelayed: Boolean = false) {
/**
* ID of the deep link.
*
* By default, it is the same as the [uri].
* */
val id: String get() = uri
/**
* URI of the deep link.
*
* **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.**
*
* Query parameters will be received automatically.
*
* Path parameters can be added using the following syntax:
* ```kotlin
* "tangem://link" // Without parameters
* "tangem://link/{param1}/{param2}" // With path parameters
* ```
* */
abstract val uri: String
/**
* Method to be called when this deep link is received.
*
* @param params Map of parameters received from the deep link.
* */
abstract fun onReceive(params: Map<String, String>)
}

View file

@ -1,58 +0,0 @@
package com.tangem.core.deeplink
import android.content.Intent
/**
* Key to pass deeplink via intent
*/
const val DEEPLINK_KEY = "deeplink"
const val WEBLINK_KEY = "link"
// TODO: Add tests
/**
* Provides functionality to handle deep links.
*
* Allows deep links to be launched, registered, or unregistered.
*/
interface DeepLinksRegistry {
/**
* Finds matches registered deep links for the given [intent] and launches them.
*
* @return `true` if any deep link was received, `false` otherwise.
*/
fun launch(intent: Intent): Boolean
/**
* Registers the given [deepLink].
*/
fun register(deepLink: DeepLink)
/**
* Registers the given [deepLinks].
*/
fun register(deepLinks: Collection<DeepLink>)
/**
* Unregisters the given [deepLinks].
*/
fun unregister(deepLinks: Collection<DeepLink>)
/**
* Unregisters the given [deepLink].
*/
fun unregister(deepLink: DeepLink)
/**
* Unregisters deep links with the given [ids].
* */
fun unregisterByIds(ids: Collection<String>)
/**
* Triggers run last launched [Intent] with deeplink handlers that can handle delayed deeplink
* of specific [deepLinkClass] after handle [Intent] clear that and second time no intent will be handled
*/
fun triggerDelayedDeeplink(deepLinkClass: Class<out DeepLink>)
fun cancelDelayedDeeplink()
}

View file

@ -1,20 +0,0 @@
package com.tangem.core.deeplink.di
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object DeepLinksModule {
@Provides
@Singleton
fun provideDeepLinksRegistry(): DeepLinksRegistry {
return DefaultDeepLinksRegistry()
}
}

View file

@ -1,201 +0,0 @@
package com.tangem.core.deeplink.impl
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
import timber.log.Timber
internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
private var registries: List<DeepLink> = emptyList()
private var lastDeepLink: Uri? = null
override fun launch(intent: Intent): Boolean {
// Try to get deeplink from data (direct deeplink flow)
// Otherwise, try to get from extras (notification deeplink flow)
val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri()
val received = intent.data ?: deepLinkExtras ?: return false
lastDeepLink = received
var hasMatch = false
Timber.i(
"""
Received deep link intent
|- Received URI: $received
|- Registries: $registries
""".trimIndent(),
)
registries.forEach { deepLink ->
val expected = deepLink.uri.toUri()
if (!isMatches(expected, received)) return@forEach
hasMatch = true
val params = getParams(expected, received)
logMatch(hasMatch, expected, received, params)
deepLink.onReceive(params)
lastDeepLink = null // clear deeplink if it was handled
}
if (!hasMatch) {
logMatch(hasMatch, null, received, null)
}
return hasMatch
}
override fun register(deepLinks: Collection<DeepLink>) {
registries = (registries + deepLinks).distinctBy(DeepLink::id)
Timber.d(
"""
Registered deep links
|- Registries: $registries
""".trimIndent(),
)
}
override fun register(deepLink: DeepLink) {
registries = (registries + deepLink).distinctBy(DeepLink::id)
Timber.d(
"""
Registered deep link
|- Registries: $registries
""".trimIndent(),
)
}
override fun unregister(deepLinks: Collection<DeepLink>) {
registries = registries.filter { it !in deepLinks }
Timber.d(
"""
Unregistered deep links
|- Registries: $registries
""".trimIndent(),
)
}
override fun unregister(deepLink: DeepLink) {
registries = registries.filter { it.id != deepLink.id }
Timber.d(
"""
Unregistered deep link
|- Registries: $registries
""".trimIndent(),
)
}
override fun unregisterByIds(ids: Collection<String>) {
registries = registries.filter { it.id !in ids }
Timber.d(
"""
Unregistered deep links
|- Registries: $registries
""".trimIndent(),
)
}
override fun triggerDelayedDeeplink(deepLinkClass: Class<out DeepLink>) {
val received = lastDeepLink
if (received != null) {
var hasMatch = false
registries
.filterIsInstance(deepLinkClass)
.forEach { deepLink ->
if (!deepLink.shouldHandleDelayed) return@forEach
val expected = deepLink.uri.toUri()
if (!isMatches(expected, received)) return@forEach
hasMatch = true
val params = getParams(expected, received)
logMatch(hasMatch, expected, received, params)
deepLink.onReceive(params)
}
if (!hasMatch) {
logMatch(hasMatch, null, received, null)
}
lastDeepLink = null // clear deeplink in any case handle or not
}
}
override fun cancelDelayedDeeplink() {
lastDeepLink = null
}
private fun logMatch(hasMatch: Boolean, expected: Uri?, received: Uri?, params: Map<String, String>?) {
if (hasMatch) {
Timber.i(
"""
Matched deep link
|- Expected URI: $expected
|- Received URI: $received
|- Params: $params
""".trimIndent(),
)
} else {
Timber.i(
"""
No match found for deep link
|- Received URI: $received
|- Registries: $registries
""".trimIndent(),
)
}
}
private fun isMatches(received: Uri, expected: Uri): Boolean {
if (received == expected) return true
if (received.authority != expected.authority ||
received.pathSegments.size != expected.pathSegments.size
) {
return false
}
received.pathSegments.forEachIndexed { index, receivedSegment ->
val expectedSegment = expected.pathSegments[index]
if (receivedSegment != expectedSegment &&
!(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}"))
) {
return false
}
}
return true
}
private fun getParams(received: Uri, expected: Uri): Map<String, String> {
val params = mutableMapOf<String, String>()
received.pathSegments.forEachIndexed { index, receivedSegment ->
val expectedSegment = expected.pathSegments[index]
if (receivedSegment != expectedSegment &&
receivedSegment.startsWith(prefix = "{") &&
receivedSegment.endsWith(suffix = "}")
) {
val path = receivedSegment
.replace(oldValue = "{", newValue = "")
.replace(oldValue = "}", newValue = "")
params[path] = expectedSegment
}
}
expected.queryParameterNames.forEach { paramName ->
expected.getQueryParameter(paramName)?.let { param ->
params[paramName] = param
}
}
return params
}
}

View file

@ -17,7 +17,6 @@ dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.deepLinks.global)
implementation(projects.core.analytics)
/** Common modules */

View file

@ -74,7 +74,6 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.deepLinks)
/* Common */
implementation(projects.common.ui)

View file

@ -3,7 +3,7 @@ package com.tangem.features.markets.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency

View file

@ -13,7 +13,6 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.deepLinks)
/* Project - Domain */
implementation(projects.domain.onramp.models)

View file

@ -22,7 +22,6 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.deepLinks)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.utils)

View file

@ -22,7 +22,6 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.core.decompose)
implementation(projects.core.deepLinks)
implementation(projects.libs.crypto)
implementation(projects.common.routing)

View file

@ -47,8 +47,6 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.decompose)
implementation(projects.core.deepLinks)
/** Domain */
implementation(projects.domain.tokens)

View file

@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability

View file

@ -52,7 +52,6 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.navigation)
implementation(projects.core.deepLinks)
implementation(projects.core.pagination)
/** Feature Apis */

View file

@ -2,7 +2,7 @@ package com.tangem.feature.tester.presentation.testpush.viewmodel.transformers.m
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
import com.tangem.domain.markets.TokenMarket
import com.tangem.feature.tester.presentation.testpush.entity.TestPushUM
import com.tangem.utils.transformer.Transformer

View file

@ -53,8 +53,6 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.datasource)
implementation(projects.core.deepLinks)
implementation(projects.core.deepLinks.global)
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.common.ui)

View file

@ -2,13 +2,13 @@ package com.tangem.feature.tokendetails.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network

View file

@ -56,8 +56,6 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.deepLinks)
implementation(projects.core.deepLinks.global)
implementation(projects.core.decompose)
implementation(projects.core.datasource)
implementation(projects.core.res)

View file

@ -156,8 +156,6 @@ include(":core:navigation")
include(":core:res")
include(":core:ui")
include(":core:utils")
include(":core:deep-links")
include(":core:deep-links:global")
include(":core:decompose")
include(":core:pagination")
include(":core:error")