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

@ -0,0 +1,53 @@
package com.tangem.common.routing.deeplink
import com.tangem.common.routing.deeplink.DeeplinkConst.TANGEM_SCHEME
/** 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") */
fun setScheme(scheme: String): DeepLinkBuilder {
this.scheme = scheme
return this
}
/** 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 */
fun addPathParam(param: String): DeepLinkBuilder {
pathParams.add(param)
return this
}
/** 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 */
fun build(): String {
val path = if (pathParams.isEmpty()) {
action
} else {
"$action/${pathParams.joinToString("/")}"
}
val queryString = if (queryParams.isEmpty()) {
""
} else {
"?" + queryParams.entries.joinToString("&") { "${it.key}=${it.value}" }
}
return "$scheme://$path$queryString"
}
}

View file

@ -0,0 +1,15 @@
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"
const val TYPE_KEY = "type"
const val TOKEN_ID_KEY = "token_id"
const val DERIVATION_PATH_KEY = "derivation_path"
const val TRANSACTION_ID_KEY = "transaction_id"
const val NAME_KEY = "name"
}

View file

@ -0,0 +1,69 @@
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.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?> {
override fun convert(value: Map<String, String>): String? {
return when {
value[DEEPLINK_KEY] != null -> value[DEEPLINK_KEY]
isTangemPushNotificationPayload(value) -> buildNotificationDeeplink(value)
else -> null
}
}
fun convertBundle(bundle: Bundle?): String? {
if (bundle == null) return null
val bundleDataMap = mutableMapOf<String, String>()
for (key in bundle.keySet()) {
val value = bundle.getString(key)
if (value != null) {
bundleDataMap[key] = value
}
}
return convert(bundleDataMap)
}
@Suppress("ReturnCount")
private fun buildNotificationDeeplink(payload: Map<String, String>): String? {
val type = payload[TYPE_KEY] ?: return null
val networkId = payload[NETWORK_ID_KEY] ?: return null
val tokenId = payload[TOKEN_ID_KEY] ?: return null
val walletId = payload[WALLET_ID_KEY] ?: return null
val derivationPath = payload[DERIVATION_PATH_KEY].orEmpty()
val transactionId = payload[TRANSACTION_ID_KEY]
val name = payload[NAME_KEY]
return DeepLinkBuilder().setScheme(DeepLinkScheme.Tangem.scheme).apply {
setAction(DeepLinkRoute.TokenDetails.host)
addQueryParam(NETWORK_ID_KEY, networkId)
addQueryParam(TOKEN_ID_KEY, tokenId)
addQueryParam(TYPE_KEY, type)
addQueryParam(WALLET_ID_KEY, walletId)
if (derivationPath.isNotBlank()) {
addQueryParam(DERIVATION_PATH_KEY, derivationPath)
}
transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) }
name?.let { addQueryParam(NAME_KEY, it) }
}.build()
}
private fun isTangemPushNotificationPayload(payload: Map<String, String>): Boolean {
return payload.containsKey(TYPE_KEY) &&
payload.containsKey(NETWORK_ID_KEY) &&
payload.containsKey(TOKEN_ID_KEY) &&
payload.containsKey(WALLET_ID_KEY)
}
}

View file

@ -0,0 +1,111 @@
package com.tangem.common.routing.deeplink
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
internal class DeepLinkBuilderTest {
private lateinit var deepLinkBuilder: DeepLinkBuilder
@Before
fun setup() {
deepLinkBuilder = DeepLinkBuilder()
}
@Test
fun `GIVEN default builder WHEN build THEN should return default scheme`() {
// WHEN
val result = deepLinkBuilder.build()
// THEN
assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://")
}
@Test
fun `GIVEN custom scheme WHEN setScheme THEN should use custom scheme`() {
// GIVEN
val customScheme = "https"
// WHEN
val result = deepLinkBuilder
.setScheme(customScheme)
.build()
// THEN
assertThat(result).isEqualTo("$customScheme://")
}
@Test
fun `GIVEN action WHEN setAction THEN should include action in path`() {
// GIVEN
val action = "wallet"
// WHEN
val result = deepLinkBuilder
.setAction(action)
.build()
// THEN
assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://$action")
}
@Test
fun `GIVEN path params WHEN addPathParam THEN should include params in path`() {
// GIVEN
val action = "wallet"
val param1 = "123"
val param2 = "456"
// WHEN
val result = deepLinkBuilder
.setAction(action)
.addPathParam(param1)
.addPathParam(param2)
.build()
// THEN
assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://$action/$param1/$param2")
}
@Test
fun `GIVEN query params WHEN addQueryParam THEN should include params in query string`() {
// GIVEN
val action = "wallet"
val key1 = "param1"
val value1 = "value1"
val key2 = "param2"
val value2 = "value2"
// WHEN
val result = deepLinkBuilder
.setAction(action)
.addQueryParam(key1, value1)
.addQueryParam(key2, value2)
.build()
// THEN
assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://$action?$key1=$value1&$key2=$value2")
}
@Test
fun `GIVEN complex deep link WHEN build THEN should construct correct URI`() {
// GIVEN
val scheme = "https"
val action = "wallet"
val pathParam = "123"
val queryKey = "token"
val queryValue = "abc"
// WHEN
val result = deepLinkBuilder
.setScheme(scheme)
.setAction(action)
.addPathParam(pathParam)
.addQueryParam(queryKey, queryValue)
.build()
// THEN
assertThat(result).isEqualTo("$scheme://$action/$pathParam?$queryKey=$queryValue")
}
}

View file

@ -0,0 +1,148 @@
package com.tangem.common.routing.deeplink
import com.google.common.truth.Truth.assertThat
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 {
@Test
fun `GIVEN payload with deeplink key WHEN convert THEN should return deeplink value`() {
// GIVEN
val payload = mapOf(
DEEPLINK_KEY to "tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&user_wallet_id=wallet123" +
"&derivation_path=m'0'0'0",
)
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isEqualTo(
"tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&user_wallet_id=wallet123&derivation_path=m'0'0'0",
)
}
@Test
fun `GIVEN valid push notification payload with all vital values WHEN convert THEN should return correct deeplink`() {
// GIVEN
val payload = mapOf(
TYPE_KEY to "token",
NETWORK_ID_KEY to "ethereum",
TOKEN_ID_KEY to "0x123",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isEqualTo(
"tangem://token?network_id=ethereum&token_id=0x123&type=token&user_wallet_id=wallet123&derivation_path=m'0'0'0",
)
}
@Test
fun `GIVEN push notification payload without derivationPath WHEN convert THEN should return correct deeplink without derivation_path`() {
// GIVEN
val payload = mapOf(
TYPE_KEY to "token",
NETWORK_ID_KEY to "ethereum",
TOKEN_ID_KEY to "0x123",
WALLET_ID_KEY to "wallet123",
)
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isEqualTo(
"tangem://token?network_id=ethereum&token_id=0x123&type=token&user_wallet_id=wallet123",
)
}
@Test
fun `GIVEN push notification payload with missing type WHEN convert THEN should return null`() {
// GIVEN
val payload = mapOf(
NETWORK_ID_KEY to "ethereum",
TOKEN_ID_KEY to "0x123",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isNull()
}
@Test
fun `GIVEN push notification payload with missing networkId WHEN convert THEN should return null`() {
// GIVEN
val payload = mapOf(
TYPE_KEY to "token",
TOKEN_ID_KEY to "0x123",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isNull()
}
@Test
fun `GIVEN push notification payload with missing tokenId WHEN convert THEN should return null`() {
// GIVEN
val payload = mapOf(
TYPE_KEY to "token",
NETWORK_ID_KEY to "ethereum",
WALLET_ID_KEY to "wallet123",
DERIVATION_PATH_KEY to "m'0'0'0",
)
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isNull()
}
@Test
fun `GIVEN push notification payload with missing walletId WHEN convert THEN should return null`() {
// GIVEN
val payload = mapOf(
TYPE_KEY to "token",
NETWORK_ID_KEY to "ethereum",
TOKEN_ID_KEY to "0x123",
)
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isNull()
}
@Test
fun `GIVEN empty payload WHEN convert THEN should return null`() {
// GIVEN
val payload = emptyMap<String, String>()
// WHEN
val result = PayloadToDeeplinkConverter.convert(payload)
// THEN
assertThat(result).isNull()
}
}