Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-26 13:10:44 +05:00
parent 9957e986e1
commit a774347c70
29 changed files with 887 additions and 86 deletions

View file

@ -198,6 +198,8 @@ dependencies {
implementation(projects.features.biometry.impl)
implementation(projects.features.nft.api)
implementation(projects.features.nft.impl)
implementation(projects.features.walletconnect.api)
implementation(projects.features.walletconnect.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

View file

@ -8,4 +8,7 @@ internal class WalletConnectFeatureToggles(
val isSolanaTxSignEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED")
val isRedesignedWalletConnectEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED")
}

View file

@ -22,6 +22,7 @@ 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.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -43,6 +44,7 @@ import com.tangem.utils.Provider
import dagger.hilt.android.scopes.ActivityScoped
import java.util.WeakHashMap
import javax.inject.Inject
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedisegnedWalletConnectComponent
@ActivityScoped
@Suppress("LongParameterList", "LargeClass")
@ -78,8 +80,10 @@ internal class ChildFactory @Inject constructor(
private val walletComponentFactory: WalletEntryComponent.Factory,
private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory,
private val sendFeatureToggles: SendFeatureToggles,
private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory,
private val testerRouter: TesterRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
) {
fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child {
@ -296,11 +300,19 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.WalletConnectSessions -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
}
}
is AppRoute.QrScanning -> {
val source = when (route.source) {
@ -574,11 +586,19 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.WalletConnectSessions -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
}
}
is AppRoute.CurrencyDetails -> {
route.asComponentChild(

View file

@ -78,5 +78,9 @@
{
"name": "SEND_V2_ENABLED",
"version": "undefined"
},
{
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
}
]

View file

@ -126,6 +126,7 @@
</plurals>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_disconnect">Disconnect</string>
<string name="common_done">Done</string>
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
@ -1213,6 +1214,13 @@
<string name="warning_testnet_card_message">This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.</string>
<string name="warning_testnet_card_title">For testing purposes only</string>
<string name="warning_token_balance_not_updated">Balance may be outdated. Refresh the page.</string>
<string name="wc_connections">Connections</string>
<string name="wc_disconnect_all">Disconnect all</string>
<string name="wc_disconnect_all_alert_desc">Text about discnected all dApps</string>
<string name="wc_disconnect_all_alert_title">Disconect All dApps</string>
<string name="wc_new_connection">New connection</string>
<string name="wc_no_sessions_desc">Connect your wallet to a different dApps</string>
<string name="wc_no_sessions_title">No sessions</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>
<string name="welcome_interrupted_backup_alert_resume">Yes, resume</string>

View file

@ -0,0 +1,47 @@
package com.tangem.core.ui.components.dropdownmenu
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Suppress("ComposableEventParameterNaming")
@Composable
fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit, modifier: Modifier = Modifier) {
Text(
modifier = modifier
.clickable {
dismissParent()
item.onClick()
}
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16),
text = item.title.resolveReference(),
style = TangemTheme.typography.button.copy(color = item.textColorProvider()),
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenDetailsAppBarDropdownItem() {
TangemThemePreview {
TangemDropdownItem(
modifier = Modifier.background(TangemTheme.colors.background.primary),
dismissParent = {},
item = TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
)
}
}

View file

@ -1,11 +1,17 @@
@file:Suppress("TopLevelPropertyNaming")
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
package com.tangem.core.ui.components.dropdownmenu
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
@ -15,16 +21,19 @@ import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.tangem.core.ui.res.TangemTheme
import kotlin.math.max
import kotlin.math.min
/**
* Just copy paste [DropdownMenu] from material3 with deleting vertical paddings.
*/
@Composable
internal fun TangemDropdownMenu(
fun TangemDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
offset: DpOffset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
@ -142,10 +151,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect)
menuBounds.width == 0 -> 0f
else -> {
val intersectionCenter =
(
kotlin.math.max(parentBounds.left, menuBounds.left) +
kotlin.math.min(parentBounds.right, menuBounds.right)
) / 2
(max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2
(intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
}
}
@ -155,10 +161,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect)
menuBounds.height == 0 -> 0f
else -> {
val intersectionCenter =
(
kotlin.math.max(parentBounds.top, menuBounds.top) +
kotlin.math.min(parentBounds.bottom, menuBounds.bottom)
) / 2
(max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2
(intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.core.ui.components.dropdownmenu
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
data class TangemDropdownMenuItem(
val title: TextReference,
val textColorProvider: @Composable () -> Color,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="76dp"
android:height="76dp"
android:viewportWidth="76"
android:viewportHeight="76">
<path
android:fillColor="#3B99FC"
android:pathData="M16,0L60,0A16,16 0,0 1,76 16L76,60A16,16 0,0 1,60 76L16,76A16,16 0,0 1,0 60L0,16A16,16 0,0 1,16 0z" />
<group>
<clip-path android:pathData="M11.084,22.167h53.833v31.667h-53.833z" />
<path
android:fillColor="#ffffff"
android:pathData="M22.112,28.351C30.889,20.101 45.126,20.101 53.903,28.351L54.959,29.337C55.403,29.751 55.403,30.417 54.959,30.83L51.348,34.229C51.126,34.429 50.778,34.429 50.556,34.229L49.098,32.856C42.973,27.098 33.042,27.098 26.917,32.856L25.362,34.322C25.139,34.522 24.792,34.522 24.57,34.322L20.945,30.937C20.501,30.524 20.501,29.857 20.945,29.444L22.112,28.351ZM61.376,35.375L64.598,38.4C65.042,38.813 65.042,39.48 64.598,39.893L50.084,53.527C49.639,53.94 48.931,53.94 48.501,53.527L38.209,43.851C38.098,43.745 37.917,43.745 37.806,43.851L27.514,53.527C27.07,53.94 26.362,53.94 25.931,53.527L11.417,39.893C10.973,39.48 10.973,38.813 11.417,38.4L14.639,35.375C15.084,34.962 15.792,34.962 16.223,35.375L26.514,45.051C26.626,45.158 26.806,45.158 26.917,45.051L37.209,35.375C37.653,34.962 38.362,34.962 38.792,35.375L49.084,45.051C49.195,45.158 49.376,45.158 49.487,45.051L59.778,35.375C60.223,34.962 60.931,34.962 61.376,35.375Z" />
</group>
</vector>

View file

@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails
import androidx.compose.ui.graphics.Color
import androidx.paging.PagingData
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
@ -27,7 +28,7 @@ internal object TokenDetailsPreviewData {
onBackClick = {},
tokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig(
persistentListOf(
TokenDetailsAppBarMenuConfig.MenuItem(
TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },

View file

@ -1,14 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import kotlinx.collections.immutable.ImmutableList
internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList<MenuItem>) {
data class MenuItem(
val title: TextReference,
val textColorProvider: @Composable () -> Color,
val onClick: () -> Unit,
)
}
internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList<TangemDropdownMenuItem>)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.getOrElse
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
@ -14,9 +15,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.converter.Converter
@ -86,7 +87,7 @@ internal class TokenDetailsSkeletonStateConverter(
private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig(
items = buildList {
addGenerateXPubMenuItem(cryptoCurrency)
TokenDetailsAppBarMenuConfig.MenuItem(
TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = clickIntents::onHideClick,
@ -94,9 +95,7 @@ internal class TokenDetailsSkeletonStateConverter(
}.toImmutableList(),
)
private fun MutableList<TokenDetailsAppBarMenuConfig.MenuItem>.addGenerateXPubMenuItem(
cryptoCurrency: CryptoCurrency,
) {
private fun MutableList<TangemDropdownMenuItem>.addGenerateXPubMenuItem(cryptoCurrency: CryptoCurrency) {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return
val isBitcoin = isBitcoin(cryptoCurrency.network.id.value)
@ -107,7 +106,7 @@ internal class TokenDetailsSkeletonStateConverter(
if (isBitcoin && hasDerivations) {
add(
TokenDetailsAppBarMenuConfig.MenuItem(
TangemDropdownMenuItem(
title = resourceReference(R.string.token_details_generate_xpub),
textColorProvider = { TangemTheme.colors.text.primary1 },
onClick = clickIntents::onGenerateExtendedKey,

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
@ -376,13 +377,13 @@ internal class TokenDetailsStateFactory(
return copy(
items = buildList {
if (isSupported && hasDerivations) {
TokenDetailsAppBarMenuConfig.MenuItem(
TangemDropdownMenuItem(
title = resourceReference(R.string.token_details_generate_xpub),
textColorProvider = { TangemTheme.colors.text.primary1 },
onClick = clickIntents::onGenerateExtendedKey,
).let(::add)
}
TokenDetailsAppBarMenuConfig.MenuItem(
TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = clickIntents::onHideClick,

View file

@ -3,8 +3,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -14,14 +12,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.features.tokendetails.impl.R
@ -58,10 +54,9 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
expanded = showDropdownMenu,
modifier = Modifier.background(TangemTheme.colors.background.primary),
onDismissRequest = { showDropdownMenu = false },
offset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)),
content = {
config.tokenDetailsAppBarMenuConfig?.items?.fastForEach {
AppBarDropdownItem(
TangemDropdownItem(
item = it,
dismissParent = { showDropdownMenu = false },
)
@ -78,42 +73,6 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
)
}
@Suppress("ComposableEventParameterNaming")
@Composable
private fun AppBarDropdownItem(
item: TokenDetailsAppBarMenuConfig.MenuItem,
dismissParent: () -> Unit,
modifier: Modifier = Modifier,
) {
Text(
modifier = modifier
.clickable {
dismissParent()
item.onClick()
}
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16),
text = item.title.resolveReference(),
style = TangemTheme.typography.body1.copy(color = item.textColorProvider()),
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenDetailsAppBarDropdownItem() {
TangemThemePreview {
AppBarDropdownItem(
modifier = Modifier.background(TangemTheme.colors.background.primary),
dismissParent = {},
item = TokenDetailsAppBarMenuConfig.MenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

1
features/walletconnect/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,18 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.walletconnect.api"
}
dependencies {
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Domain models */
implementation(projects.domain.wallets.models)
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.walletconnect.components
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface WalletConnectEntryComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, WalletConnectEntryComponent>
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,45 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.walletconnect.impl"
}
dependencies {
implementation(projects.features.walletconnect.api)
/** Core */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Domain models */
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** AndroidX */
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.coil)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
/** Other */
implementation(deps.decompose.ext.compose)
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.walletconnect.connections.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.walletconnect.connections.model.WcConnectionsModel
import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent
internal class ConnectionsComponent(
appComponentContext: AppComponentContext,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: WcConnectionsModel = getOrCreateModel()
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
WcConnectionsContent(modifier = modifier, state = state)
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.features.walletconnect.connections.components
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.walletconnect.connections.routes.ConnectionsInnerRoute
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultWalletConnectEntryComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppComponentContext by appComponentContext, WalletConnectEntryComponent {
private val contentNavigation = StackNavigation<ConnectionsInnerRoute>()
private val contentStack = childStack(
source = contentNavigation,
serializer = ConnectionsInnerRoute.serializer(),
initialConfiguration = ConnectionsInnerRoute.Connections,
childFactory = ::screenChild,
)
@Composable
override fun Content(modifier: Modifier) {
val childStack by contentStack.subscribeAsState()
BackHandler(onBack = router::pop)
Children(stack = childStack, animation = stackAnimation()) { child ->
child.instance.Content(modifier = modifier)
}
}
private fun screenChild(
config: ConnectionsInnerRoute,
componentContext: ComponentContext,
): ComposableContentComponent = when (config) {
ConnectionsInnerRoute.Connections -> ConnectionsComponent(childByContext(componentContext))
ConnectionsInnerRoute.QrScan -> TODO("[REDACTED_JIRA]")
}
@AssistedFactory
interface Factory : WalletConnectEntryComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectEntryComponent
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.walletconnect.connections.entity
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import kotlinx.collections.immutable.ImmutableList
internal data class WcConnectionsState(
val topAppBarConfig: WcConnectionsTopAppBarConfig,
val connections: ImmutableList<WcConnectionsUM>,
val onNewConnectionClick: () -> Unit,
)
internal data class WcConnectionsTopAppBarConfig(
val startButtonUM: TopAppBarButtonUM,
val endActions: ImmutableList<TangemDropdownMenuItem>,
)
internal data class WcConnectionsUM(
val userWalletId: String,
val walletName: String,
val connectedApps: ImmutableList<WcConnectedAppInfo>,
)
internal data class WcConnectedAppInfo(
val name: String,
val iconUrl: String,
val subtitle: TextReference = stringReference("Connected App"),
val isVerified: Boolean = false,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,54 @@
package com.tangem.features.walletconnect.connections.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.features.walletconnect.connections.entity.WcConnectionsState
import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData
import com.tangem.features.walletconnect.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@ModelScoped
internal class WcConnectionsModel @Inject constructor(
private val router: Router,
private val uiMessageSender: UiMessageSender,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
val uiState: StateFlow<WcConnectionsState> = MutableStateFlow(
WcConnectionsPreviewData.stateForModel(
onBackClicked = router::pop,
openQrScanScreen = ::openQrScanScreen,
disconnectAll = ::disconnectAll,
),
).asStateFlow()
private fun openQrScanScreen() {
// TODO("[REDACTED_JIRA]")
}
private fun disconnectAll() {
val message = DialogMessage(
title = resourceReference(R.string.wc_disconnect_all_alert_title),
message = resourceReference(R.string.wc_disconnect_all_alert_desc),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_disconnect),
onClick = {
// TODO(wc): disconnect all
},
)
},
secondActionBuilder = { cancelAction() },
)
uiMessageSender.send(message)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.walletconnect.connections.routes
import kotlinx.serialization.Serializable
@Serializable
internal sealed class ConnectionsInnerRoute {
@Serializable
data object Connections : ConnectionsInnerRoute()
@Serializable
data object QrScan : ConnectionsInnerRoute()
}

View file

@ -0,0 +1,299 @@
package com.tangem.features.walletconnect.connections.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import coil.compose.AsyncImage
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo
import com.tangem.features.walletconnect.connections.entity.WcConnectionsState
import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig
import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM
import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData
import com.tangem.features.walletconnect.impl.R
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun WcConnectionsContent(state: WcConnectionsState, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
ConnectionsTopBar(
modifier = Modifier.statusBarsPadding(),
config = state.topAppBarConfig,
showEndButton = state.connections.isNotEmpty(),
)
},
floatingActionButton = {
if (state.connections.isNotEmpty()) {
NewConnectionButton(
onClick = state.onNewConnectionClick,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
},
floatingActionButtonPosition = FabPosition.Center,
content = { innerPadding ->
Content(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
connections = state.connections,
onNewConnectionClick = state.onNewConnectionClick,
)
},
)
}
@Composable
private fun Content(
connections: ImmutableList<WcConnectionsUM>,
onNewConnectionClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (connections.isEmpty()) {
EmptyConnectionsBlock(modifier = modifier, onNewConnectionClick = onNewConnectionClick)
} else {
ConnectionsBlock(modifier = modifier, connections = connections)
}
}
@Composable
private fun EmptyConnectionsBlock(onNewConnectionClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.img_wallet_connect_76),
contentDescription = "Wallet Connect",
modifier = Modifier.size(76.dp),
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing24),
text = stringResourceSafe(R.string.wc_no_sessions_title),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
text = stringResourceSafe(R.string.wc_no_sessions_desc),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body1,
)
NewConnectionButton(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing48),
onClick = onNewConnectionClick,
)
}
}
@Composable
private fun ConnectionsBlock(connections: ImmutableList<WcConnectionsUM>, modifier: Modifier = Modifier) {
Box(modifier = modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing76 + bottomBarHeight,
),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
items(
items = connections,
key = { it.userWalletId },
itemContent = {
ConnectionItem(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
)
.clip(RoundedCornerShape(TangemTheme.dimens.radius14)),
connection = it,
)
},
)
}
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
}
}
@Composable
private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
key("${connection.userWalletId}_${connection.walletName}") {
Text(
modifier = Modifier.padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp),
text = connection.walletName,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
}
connection.connectedApps.fastForEach { appInfo ->
key("${connection.userWalletId}_${connection.walletName}_${appInfo.name}_${appInfo.iconUrl}") {
AppInfoItem(
appInfo = appInfo,
modifier = Modifier
.clickable(onClick = appInfo.onClick)
.padding(all = 12.dp),
)
}
}
}
}
@Composable
private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AsyncImage(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(TangemTheme.dimens.radius8)),
model = appInfo.iconUrl,
contentDescription = appInfo.name,
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
Text(
text = appInfo.name,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
)
Text(
text = appInfo.subtitle.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ConnectionsTopBar(
config: WcConnectionsTopAppBarConfig,
showEndButton: Boolean,
modifier: Modifier = Modifier,
) {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
// TangemTopAppBar is not friendly with dropdown menu
TopAppBar(
modifier = modifier,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
titleContentColor = TangemTheme.colors.icon.primary1,
actionIconContentColor = TangemTheme.colors.icon.primary1,
),
navigationIcon = {
IconButton(onClick = config.startButtonUM.onIconClicked) {
Icon(
painter = painterResource(id = config.startButtonUM.iconRes),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Back",
)
}
},
title = {
Text(
text = stringResourceSafe(R.string.wc_connections),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
actions = {
AnimatedVisibility(showEndButton) {
IconButton(onClick = { showDropdownMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.ic_more_vertical_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "More",
)
}
}
TangemDropdownMenu(
expanded = showDropdownMenu,
modifier = Modifier.background(TangemTheme.colors.background.primary),
onDismissRequest = { showDropdownMenu = false },
content = {
config.endActions.fastForEach {
TangemDropdownItem(
item = it,
dismissParent = { showDropdownMenu = false },
)
}
},
)
},
)
}
@Composable
private fun NewConnectionButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
PrimaryButton(
modifier = modifier,
text = stringResourceSafe(R.string.wc_new_connection),
onClick = onClick,
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun WcConnectionsContentPreview(
@PreviewParameter(WcConnectionsStateProvider::class) params: WcConnectionsState,
) {
TangemThemePreview {
WcConnectionsContent(state = params)
}
}
private class WcConnectionsStateProvider : CollectionPreviewParameterProvider<WcConnectionsState>(
listOf(
WcConnectionsPreviewData.stateWithEmptyConnections,
WcConnectionsPreviewData.fullState,
),
)

View file

@ -0,0 +1,141 @@
package com.tangem.features.walletconnect.connections.ui.preview
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo
import com.tangem.features.walletconnect.connections.entity.WcConnectionsState
import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig
import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM
import com.tangem.features.walletconnect.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber
internal object WcConnectionsPreviewData {
private const val BASE_URL = "https://raw.githubusercontent.com/TaranVH/LOGOS/refs/heads/master/Crypto"
private val connections = persistentListOf(
WcConnectionsUM(
userWalletId = "wallet_id_1",
walletName = "Wallet 2.0",
connectedApps = persistentListOf(
WcConnectedAppInfo(
name = "Moralis",
iconUrl = "$BASE_URL/satoshi.png",
isVerified = true,
onClick = { Timber.d("Moralis clicked") },
),
WcConnectedAppInfo(
name = "website.com",
iconUrl = "$BASE_URL/doge_again_lol.jpg",
onClick = { Timber.d("website.com clicked") },
),
),
),
WcConnectionsUM(
userWalletId = "wallet_id_2",
walletName = "Bitcoin",
connectedApps = persistentListOf(
WcConnectedAppInfo(
name = "React app",
iconUrl = "$BASE_URL/LiteCoin.png",
isVerified = true,
onClick = { Timber.d("React app clicked") },
),
),
),
WcConnectionsUM(
userWalletId = "wallet_id_3",
walletName = "Wallet 2.0",
connectedApps = persistentListOf(
WcConnectedAppInfo(
name = "Moralis",
iconUrl = "$BASE_URL/satoshi.png",
isVerified = true,
onClick = { Timber.d("Moralis clicked") },
),
WcConnectedAppInfo(
name = "website.com",
iconUrl = "$BASE_URL/doge_again_lol.jpg",
onClick = { Timber.d("website.com clicked") },
),
),
),
WcConnectionsUM(
userWalletId = "wallet_id_4",
walletName = "Bitcoin",
connectedApps = persistentListOf(
WcConnectedAppInfo(
name = "React app",
iconUrl = "$BASE_URL/LiteCoin.png",
isVerified = true,
onClick = { Timber.d("React app clicked") },
),
),
),
WcConnectionsUM(
userWalletId = "wallet_id_5",
walletName = "Wallet 2.0",
connectedApps = persistentListOf(
WcConnectedAppInfo(
name = "Moralis",
iconUrl = "$BASE_URL/satoshi.png",
isVerified = true,
onClick = { Timber.d("Moralis clicked") },
),
WcConnectedAppInfo(
name = "website.com",
iconUrl = "$BASE_URL/doge_again_lol.jpg",
onClick = { Timber.d("website.com clicked") },
),
),
),
WcConnectionsUM(
userWalletId = "wallet_id_6",
walletName = "Bitcoin",
connectedApps = persistentListOf(
WcConnectedAppInfo(
name = "React app",
iconUrl = "$BASE_URL/LiteCoin.png",
isVerified = true,
onClick = { Timber.d("React app clicked") },
),
),
),
)
val stateWithEmptyConnections = WcConnectionsState(
topAppBarConfig = WcConnectionsTopAppBarConfig(
startButtonUM = TopAppBarButtonUM(
iconRes = R.drawable.ic_back_24,
onIconClicked = {},
enabled = true,
),
endActions = persistentListOf(
TangemDropdownMenuItem(
title = resourceReference(R.string.wc_disconnect_all),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
),
),
connections = persistentListOf(),
onNewConnectionClick = {},
)
val fullState = stateWithEmptyConnections.copy(connections = connections)
fun stateForModel(
onBackClicked: () -> Unit,
openQrScanScreen: () -> Unit,
disconnectAll: () -> Unit,
): WcConnectionsState {
return fullState.copy(
topAppBarConfig = fullState.topAppBarConfig.copy(
startButtonUM = fullState.topAppBarConfig.startButtonUM.copy(onIconClicked = onBackClicked),
endActions = fullState.topAppBarConfig.endActions
.map { it.copy(onClick = disconnectAll) }
.toImmutableList(),
),
onNewConnectionClick = openQrScanScreen,
)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.walletconnect.di
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.walletconnect.connections.components.DefaultWalletConnectEntryComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface WalletConnectFeatureModule {
@Binds
@Singleton
fun bindWalletConnectEntryComponentFactory(
factory: DefaultWalletConnectEntryComponent.Factory,
): WalletConnectEntryComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.walletconnect.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.walletconnect.connections.model.WcConnectionsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface WalletConnectModelModule {
@Binds
@IntoMap
@ClassKey(WcConnectionsModel::class)
fun bindWcConnectionsModel(model: WcConnectionsModel): Model
}

View file

@ -235,6 +235,9 @@ include(":features:biometry:impl")
include(":features:nft:api")
include(":features:nft:impl")
include(":features:walletconnect:api")
include(":features:walletconnect:impl")
// endregion Feature modules
// region Domain modules