Updated on 2026-08-14
This commit is contained in:
commit
e0033cef82
1726 changed files with 83681 additions and 14949 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -304,7 +304,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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue