Updated on 2026-08-14
This commit is contained in:
parent
d06051845b
commit
5c74278e3d
30 changed files with 83 additions and 666 deletions
|
|
@ -66,7 +66,6 @@ internal class WcPairComponent(
|
|||
else -> model.stackNavigation.pop()
|
||||
}
|
||||
is WcAppInfoRoutes.SelectNetworks,
|
||||
is WcAppInfoRoutes.SelectWallet,
|
||||
is WcAppInfoRoutes.PortfolioSelector,
|
||||
-> model.stackNavigation.pop()
|
||||
}
|
||||
|
|
@ -102,14 +101,6 @@ internal class WcPairComponent(
|
|||
callback = model,
|
||||
),
|
||||
)
|
||||
is WcAppInfoRoutes.SelectWallet -> WcSelectWalletComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = WcSelectWalletComponent.WcSelectWalletParams(
|
||||
selectedWalletId = config.selectedWalletId,
|
||||
onDismiss = ::dismiss,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
WcAppInfoRoutes.PortfolioSelector -> portfolioSelectorComponentFactory.create(
|
||||
context = appComponentContext,
|
||||
params = PortfolioSelectorComponent.Params(
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
package com.tangem.features.walletconnect.connections.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.walletconnect.connections.model.WcSelectWalletModel
|
||||
import com.tangem.features.walletconnect.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class WcSelectWalletComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: WcSelectWalletParams,
|
||||
) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent {
|
||||
|
||||
private val model: WcSelectWalletModel = getOrCreateModel(params = params)
|
||||
|
||||
override fun dismiss() {
|
||||
params.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
WcSelectWalletModalBS(
|
||||
wallets = state.wallets,
|
||||
selectedWalletId = state.selectedUserWalletId,
|
||||
onBack = router::pop,
|
||||
onDismiss = ::dismiss,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallback {
|
||||
fun onWalletSelected(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
data class WcSelectWalletParams(
|
||||
val selectedWalletId: UserWalletId,
|
||||
val callback: ModelCallback,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WcSelectWalletModalBS(
|
||||
wallets: ImmutableList<UserWalletItemUM>,
|
||||
selectedWalletId: UserWalletId,
|
||||
onBack: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (wallets.isEmpty()) return
|
||||
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onDismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
onBack = onBack,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(R.string.common_choose_wallet),
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onBack,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
WcSelectWalletContent(
|
||||
modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
wallets = wallets,
|
||||
selectedWalletId = selectedWalletId,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WcSelectWalletContent(
|
||||
wallets: ImmutableList<UserWalletItemUM>,
|
||||
selectedWalletId: UserWalletId,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
wallets.fastForEach { state ->
|
||||
key(state.id) {
|
||||
val baseModifier = Modifier
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.clickable(onClick = state.onClick)
|
||||
val itemModifier = if (state.id == selectedWalletId.stringValue) {
|
||||
baseModifier.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors.text.accent,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
)
|
||||
} else {
|
||||
baseModifier
|
||||
}
|
||||
UserWalletItem(
|
||||
modifier = itemModifier,
|
||||
state = state,
|
||||
blockColors = TangemBlockCardColors.copy(
|
||||
containerColor = Color.Unspecified,
|
||||
disabledContainerColor = Color.Unspecified,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun WcSelectWalletContent_Preview() {
|
||||
val wallets = persistentListOf(
|
||||
UserWalletItemUM(
|
||||
id = "user_wallet_1",
|
||||
name = stringReference("Tangem 2.0"),
|
||||
information = getInformation(42),
|
||||
balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false),
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = "user_wallet_2",
|
||||
name = stringReference("Tangem White"),
|
||||
information = getInformation(24),
|
||||
balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false),
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = "user_wallet_3",
|
||||
name = stringReference("Bitcoin"),
|
||||
information = getInformation(1),
|
||||
balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false),
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = "user_wallet_4",
|
||||
name = stringReference("Tangem 1.0"),
|
||||
information = getInformation(21),
|
||||
balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false),
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = "user_wallet_4",
|
||||
name = stringReference("Tangem 1.0"),
|
||||
information = UserWalletItemUM.Information.Loading,
|
||||
balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false),
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = "user_wallet_4",
|
||||
name = stringReference("Tangem 1.0"),
|
||||
information = UserWalletItemUM.Information.Failed,
|
||||
balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false),
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
TangemThemePreview {
|
||||
WcSelectWalletModalBS(
|
||||
wallets = wallets,
|
||||
selectedWalletId = UserWalletId(wallets.first().id.encodeToByteArray()),
|
||||
onBack = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInformation(tokenCount: Int): UserWalletItemUM.Information.Loaded {
|
||||
val text = TextReference.PluralRes(
|
||||
id = R.plurals.card_label_token_count,
|
||||
count = tokenCount,
|
||||
formatArgs = wrappedList(tokenCount),
|
||||
)
|
||||
return UserWalletItemUM.Information.Loaded(text)
|
||||
}
|
||||
|
|
@ -27,9 +27,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent {
|
|||
val verifiedDAppState: VerifiedDAppState,
|
||||
val appSubtitle: String,
|
||||
val notification: WcAppInfoSecurityNotification?,
|
||||
val portfolioSelectRow: PortfolioSelectUM?,
|
||||
val walletName: String,
|
||||
val onWalletClick: (() -> Unit)?,
|
||||
val portfolioSelectRow: PortfolioSelectUM,
|
||||
val networksInfo: WcNetworksInfo,
|
||||
val onNetworksClick: () -> Unit,
|
||||
override val connectButtonConfig: WcPrimaryButtonConfig,
|
||||
|
|
|
|||
|
|
@ -21,16 +21,11 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.walletconnect.WcAnalyticEvents
|
||||
import com.tangem.domain.walletconnect.model.WcPairError
|
||||
import com.tangem.domain.walletconnect.model.WcPairError.Unknown
|
||||
|
|
@ -40,16 +35,17 @@ import com.tangem.domain.walletconnect.model.WcSessionProposal
|
|||
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
|
||||
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
|
||||
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.account.PortfolioSelectorComponent
|
||||
import com.tangem.features.account.PortfolioSelectorController
|
||||
import com.tangem.features.walletconnect.connections.components.WcPairComponent
|
||||
import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent
|
||||
import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent
|
||||
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
|
||||
import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig
|
||||
import com.tangem.features.walletconnect.connections.model.transformers.*
|
||||
import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer
|
||||
import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer
|
||||
import com.tangem.features.walletconnect.connections.model.transformers.WcDAppVerifiedStateConverter
|
||||
import com.tangem.features.walletconnect.connections.model.transformers.WcNetworksSelectedTransformer
|
||||
import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes
|
||||
import com.tangem.features.walletconnect.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -61,11 +57,8 @@ import kotlin.properties.Delegates
|
|||
import com.tangem.utils.transformer.update as transformerUpdate
|
||||
|
||||
internal interface WcPairComponentCallback :
|
||||
WcSelectWalletComponent.ModelCallback,
|
||||
WcSelectNetworksComponent.ModelCallback
|
||||
|
||||
private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
|
|
@ -75,11 +68,9 @@ internal class WcPairModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
val selectorController: PortfolioSelectorController,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
portfolioFetcherFactory: PortfolioFetcher.Factory,
|
||||
wcPairUseCaseFactory: WcPairUseCase.Factory,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(), WcPairComponentCallback {
|
||||
|
||||
|
|
@ -102,9 +93,6 @@ internal class WcPairModel @Inject constructor(
|
|||
override val onBack: () -> Unit = { stackNavigation.pop() }
|
||||
}
|
||||
|
||||
private val selectedUserWalletFlow: MutableStateFlow<UserWallet> by lazy {
|
||||
MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId })
|
||||
}
|
||||
private val selectedPortfolio = MutableSharedFlow<Pair<UserWallet, AccountStatus.CryptoPortfolio>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
|
|
@ -119,14 +107,15 @@ internal class WcPairModel @Inject constructor(
|
|||
|
||||
init {
|
||||
modelScope.launch {
|
||||
val params = SingleAccountListProducer.Params(params.userWalletId)
|
||||
val accountList = singleAccountListSupplier.getSyncOrNull(params)
|
||||
if (accountList == null) {
|
||||
val portfolioBalance = portfolioFetcher.data.first().balances
|
||||
.firstNotNullOfOrNull { (walletId, balance) ->
|
||||
if (params.userWalletId == walletId) balance else null
|
||||
}
|
||||
if (portfolioBalance == null) {
|
||||
router.pop()
|
||||
return@launch
|
||||
}
|
||||
val firstAccount = accountList.accounts.first()
|
||||
selectorController.selectAccount(firstAccount.accountId)
|
||||
selectorController.selectAccount(portfolioBalance.accountsBalance.mainAccount.accountId)
|
||||
combineFlows(portfolioFetcher)
|
||||
}
|
||||
}
|
||||
|
|
@ -135,10 +124,12 @@ internal class WcPairModel @Inject constructor(
|
|||
combine(
|
||||
flow = portfolioFetcher.data,
|
||||
flow2 = selectorController.selectedAccountWithData(portfolioFetcher)
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach { selectedPortfolio.tryEmit(it) }
|
||||
.onEach { stackNavigation.pop() },
|
||||
.runningReduce { _, new ->
|
||||
stackNavigation.pop()
|
||||
new
|
||||
},
|
||||
flow3 = wcPairUseCase(),
|
||||
flow4 = isAccountsModeEnabledUseCase(),
|
||||
transform = { portfolios, selected, pairState, isAccountMode ->
|
||||
|
|
@ -155,9 +146,9 @@ internal class WcPairModel @Inject constructor(
|
|||
|
||||
private suspend fun handlePairState(
|
||||
pairState: WcPairState,
|
||||
portfolios: PortfolioFetcher.Data? = null,
|
||||
selected: Pair<UserWallet, AccountStatus.CryptoPortfolio>? = null,
|
||||
isAccountMode: Boolean? = null,
|
||||
portfolios: PortfolioFetcher.Data,
|
||||
selected: Pair<UserWallet, AccountStatus.CryptoPortfolio>,
|
||||
isAccountMode: Boolean,
|
||||
) {
|
||||
when (pairState) {
|
||||
is WcPairState.Approving.Loading -> appInfoUiState.transformerUpdate(
|
||||
|
|
@ -184,36 +175,32 @@ internal class WcPairModel @Inject constructor(
|
|||
pairState = pairState,
|
||||
portfolios = portfolios,
|
||||
selected = selected,
|
||||
isAccountMode = isAccountMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleProposalState(
|
||||
pairState: WcPairState.Proposal,
|
||||
portfolios: PortfolioFetcher.Data? = null,
|
||||
selected: Pair<UserWallet, AccountStatus.CryptoPortfolio>? = null,
|
||||
portfolios: PortfolioFetcher.Data,
|
||||
selected: Pair<UserWallet, AccountStatus.CryptoPortfolio>,
|
||||
isAccountMode: Boolean,
|
||||
) {
|
||||
val availableWallets = pairState.dAppSession.proposalNetwork.keys
|
||||
.filter { !it.isLocked && it.isMultiCurrency }
|
||||
sessionProposal = pairState.dAppSession
|
||||
val selectedUserWalletFlow = this.selectedUserWalletFlow
|
||||
val portfolioWallet = selected?.first
|
||||
val portfolioAccount = selected?.second
|
||||
val portfolioAccountId = portfolioAccount?.account?.accountId
|
||||
val portfolioAccount = selected.second
|
||||
val portfolioAccountId = portfolioAccount.account.accountId
|
||||
val proposalAccountNetwork = sessionProposal.proposalAccountNetwork
|
||||
val foundNetwork = if (portfolioAccountId != null) {
|
||||
requireNotNull(proposalAccountNetwork)[portfolioAccountId]
|
||||
} else {
|
||||
sessionProposal.proposalNetwork[selectedUserWalletFlow.value]
|
||||
}
|
||||
val foundNetwork = proposalAccountNetwork[portfolioAccountId]
|
||||
if (foundNetwork == null) {
|
||||
processError(Unknown("Selected wallet not found"))
|
||||
} else {
|
||||
val portfolioSelectRow = tryToCreatePortfolioSelectRow(selected, portfolios)
|
||||
if (proposalAccountNetwork != null) {
|
||||
selectorController.isEnabled.value = { _, account ->
|
||||
proposalAccountNetwork.contains(account.account.accountId)
|
||||
}
|
||||
val portfolioSelectRow = createPortfolioSelectRow(
|
||||
selectedPortfolio = selected,
|
||||
portfolios = portfolios,
|
||||
isAccountMode = isAccountMode,
|
||||
)
|
||||
selectorController.isEnabled.value = { _, account ->
|
||||
proposalAccountNetwork.contains(account.account.accountId)
|
||||
}
|
||||
proposalNetwork = foundNetwork
|
||||
additionallyEnabledNetworks = proposalNetwork.available
|
||||
|
|
@ -224,13 +211,6 @@ internal class WcPairModel @Inject constructor(
|
|||
onDismiss = ::rejectPairing,
|
||||
onConnect = ::onConnect,
|
||||
portfolioSelectRow = portfolioSelectRow,
|
||||
onWalletClick = {
|
||||
stackNavigation.pushNew(
|
||||
WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId),
|
||||
)
|
||||
}.takeIf {
|
||||
portfolioSelectRow == null && availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT
|
||||
},
|
||||
onNetworksClick = {
|
||||
stackNavigation.pushNew(
|
||||
WcAppInfoRoutes.SelectNetworks(
|
||||
|
|
@ -242,7 +222,6 @@ internal class WcPairModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
},
|
||||
userWallet = portfolioWallet ?: selectedUserWalletFlow.value,
|
||||
proposalNetwork = proposalNetwork,
|
||||
additionallyEnabledNetworks = additionallyEnabledNetworks,
|
||||
),
|
||||
|
|
@ -250,17 +229,15 @@ internal class WcPairModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun tryToCreatePortfolioSelectRow(
|
||||
selectedPortfolio: Pair<UserWallet, AccountStatus.CryptoPortfolio>?,
|
||||
portfolios: PortfolioFetcher.Data?,
|
||||
): PortfolioSelectUM? {
|
||||
selectedPortfolio ?: return null
|
||||
portfolios ?: return null
|
||||
private suspend fun createPortfolioSelectRow(
|
||||
selectedPortfolio: Pair<UserWallet, AccountStatus.CryptoPortfolio>,
|
||||
portfolios: PortfolioFetcher.Data,
|
||||
isAccountMode: Boolean,
|
||||
): PortfolioSelectUM {
|
||||
val (wallet, portfolioAccount) = selectedPortfolio
|
||||
val account = when (val account = portfolioAccount.account) {
|
||||
is Account.CryptoPortfolio -> account
|
||||
}
|
||||
val isAccountMode = selectorController.isAccountMode.first()
|
||||
val icon: AccountIconUM.CryptoPortfolio?
|
||||
val name: TextReference
|
||||
if (isAccountMode) {
|
||||
|
|
@ -303,15 +280,16 @@ internal class WcPairModel @Inject constructor(
|
|||
private fun connect() {
|
||||
val enabledAvailableNetworks =
|
||||
proposalNetwork.available.filter { network -> network in additionallyEnabledNetworks }
|
||||
val selectedPortfolio = selectedPortfolio.replayCache.firstOrNull()
|
||||
val wallet = selectedPortfolio?.first ?: selectedUserWalletFlow.value
|
||||
val account = selectedPortfolio?.second?.account
|
||||
val selectedPortfolio = selectedPortfolio.replayCache
|
||||
.firstOrNull()
|
||||
?: return
|
||||
val (wallet, account) = selectedPortfolio
|
||||
|
||||
modelScope.launch {
|
||||
analytics.send(
|
||||
WcAnalyticEvents.PairButtonConnect(
|
||||
dAppName = sessionProposal.dAppMetaData.name,
|
||||
accountDerivation = account?.derivationIndex?.value,
|
||||
accountDerivation = account.account.derivationIndex.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -319,7 +297,7 @@ internal class WcPairModel @Inject constructor(
|
|||
WcSessionApprove(
|
||||
wallet = wallet,
|
||||
network = enabledAvailableNetworks + proposalNetwork.required,
|
||||
account = account,
|
||||
account = account.account,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -375,20 +353,6 @@ internal class WcPairModel @Inject constructor(
|
|||
alert?.let { stackNavigation.pushNew(it) }
|
||||
}
|
||||
|
||||
override fun onWalletSelected(userWalletId: UserWalletId) {
|
||||
val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId }
|
||||
proposalNetwork = sessionProposal.proposalNetwork[selectedUserWallet] ?: return
|
||||
selectedUserWalletFlow.update { selectedUserWallet }
|
||||
additionallyEnabledNetworks = proposalNetwork.available
|
||||
appInfoUiState.transformerUpdate(
|
||||
WcAppInfoWalletChangedTransformer(
|
||||
selectedUserWallet = selectedUserWallet,
|
||||
proposalNetwork = proposalNetwork,
|
||||
additionallyEnabledNetworks = additionallyEnabledNetworks,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNetworksSelected(selectedNetworks: Set<Network>) {
|
||||
additionallyEnabledNetworks = selectedNetworks
|
||||
appInfoUiState.transformerUpdate(
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
package com.tangem.features.walletconnect.connections.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.wallet.utils.UserWalletsFetcher
|
||||
import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.WcSelectWalletParams
|
||||
import com.tangem.features.walletconnect.connections.entity.WcAppInfoWalletUM
|
||||
import com.tangem.features.walletconnect.connections.utils.WcUserWalletsFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class WcSelectWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
messageSender: UiMessageSender,
|
||||
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<WcSelectWalletParams>()
|
||||
|
||||
internal val state: StateFlow<WcAppInfoWalletUM>
|
||||
field = MutableStateFlow<WcAppInfoWalletUM>(
|
||||
WcAppInfoWalletUM(
|
||||
wallets = persistentListOf(),
|
||||
selectedUserWalletId = params.selectedWalletId,
|
||||
),
|
||||
)
|
||||
|
||||
private val userWalletsFetcher = WcUserWalletsFetcher(
|
||||
userWalletsFetcherFactory = userWalletsFetcherFactory,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
messageSender = messageSender,
|
||||
onWalletSelected = ::onWalletSelected,
|
||||
)
|
||||
|
||||
init {
|
||||
userWalletsFetcher
|
||||
.userWallets
|
||||
.onEach { state.update { state -> state.copy(wallets = it) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onWalletSelected(userWalletId: UserWalletId) {
|
||||
params.callback.onWalletSelected(userWalletId)
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.walletconnect.connections.model.transformers
|
|||
import com.domain.blockaid.models.dapp.CheckDAppResult
|
||||
import com.tangem.common.ui.account.PortfolioSelectUM
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.walletconnect.model.WcSessionProposal
|
||||
import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification
|
||||
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
|
||||
|
|
@ -16,10 +15,8 @@ internal class WcAppInfoTransformer(
|
|||
private val dAppVerifiedStateConverter: WcDAppVerifiedStateConverter,
|
||||
private val onDismiss: () -> Unit,
|
||||
private val onConnect: (securityStatus: CheckDAppResult) -> Unit,
|
||||
private val portfolioSelectRow: PortfolioSelectUM?,
|
||||
private val onWalletClick: (() -> Unit)?,
|
||||
private val portfolioSelectRow: PortfolioSelectUM,
|
||||
private val onNetworksClick: () -> Unit,
|
||||
private val userWallet: UserWallet,
|
||||
private val proposalNetwork: WcSessionProposal.ProposalNetwork,
|
||||
private val additionallyEnabledNetworks: Set<Network>,
|
||||
) : Transformer<WcAppInfoUM> {
|
||||
|
|
@ -33,8 +30,6 @@ internal class WcAppInfoTransformer(
|
|||
),
|
||||
appSubtitle = WcAppSubtitleConverter.convert(dAppSession.dAppMetaData),
|
||||
notification = createNotification(dAppSession.securityStatus),
|
||||
walletName = userWallet.name,
|
||||
onWalletClick = onWalletClick,
|
||||
portfolioSelectRow = portfolioSelectRow,
|
||||
networksInfo = WcNetworksInfoConverter.convert(
|
||||
value = WcNetworksInfoConverter.Input(
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package com.tangem.features.walletconnect.connections.model.transformers
|
||||
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.walletconnect.model.WcSessionProposal
|
||||
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class WcAppInfoWalletChangedTransformer(
|
||||
private val selectedUserWallet: UserWallet,
|
||||
private val proposalNetwork: WcSessionProposal.ProposalNetwork,
|
||||
private val additionallyEnabledNetworks: Set<Network>,
|
||||
) : Transformer<WcAppInfoUM> {
|
||||
override fun transform(prevState: WcAppInfoUM): WcAppInfoUM {
|
||||
val contentState = prevState as? WcAppInfoUM.Content ?: return prevState
|
||||
return contentState.copy(
|
||||
walletName = selectedUserWallet.name,
|
||||
networksInfo = WcNetworksInfoConverter.convert(
|
||||
WcNetworksInfoConverter.Input(
|
||||
missingNetworks = proposalNetwork.missingRequired,
|
||||
requiredNetworks = proposalNetwork.required,
|
||||
availableNetworks = proposalNetwork.available,
|
||||
notAddedNetworks = proposalNetwork.notAdded,
|
||||
additionallyEnabledNetworks = additionallyEnabledNetworks,
|
||||
),
|
||||
),
|
||||
connectButtonConfig = prevState.connectButtonConfig.copy(
|
||||
enabled = WcConnectButtonAvailabilityConverter.convert(
|
||||
WcConnectButtonAvailabilityConverter.Input(
|
||||
missingNetworks = proposalNetwork.missingRequired,
|
||||
requiredNetworks = proposalNetwork.required,
|
||||
availableNetworks = proposalNetwork.available,
|
||||
selectedNetworks = additionallyEnabledNetworks,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ internal class WcSessionsAccountModeTransformer(
|
|||
items.add(walletHeader)
|
||||
|
||||
accountList.accounts.filterIsInstance<Account.CryptoPortfolio>().forEach accountsForEach@{ account ->
|
||||
val accountSessions = sessions.filter { it.account?.accountId == account.accountId }
|
||||
val accountSessions = sessions.filter { it.account.accountId == account.accountId }
|
||||
if (accountSessions.isEmpty()) return@accountsForEach
|
||||
val connectedApps = accountSessions.map { dappSession ->
|
||||
with(dappSession.sdkModel) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.walletconnect.connections.routes
|
|||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
@ -15,9 +14,6 @@ internal sealed class WcAppInfoRoutes : Route {
|
|||
@Serializable
|
||||
data object PortfolioSelector : WcAppInfoRoutes()
|
||||
|
||||
@Serializable
|
||||
data class SelectWallet(val selectedWalletId: UserWalletId) : WcAppInfoRoutes()
|
||||
|
||||
@Serializable
|
||||
data class SelectNetworks(
|
||||
val missingRequiredNetworks: Set<Network>,
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import androidx.compose.ui.graphics.painter.Painter
|
|||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -273,19 +271,7 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier
|
|||
val itemsModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12)
|
||||
if (state.portfolioSelectRow != null) {
|
||||
PortfolioRowItem(portfolioSelectRow = state.portfolioSelectRow)
|
||||
} else {
|
||||
WalletRowItem(
|
||||
modifier = if (state.onWalletClick != null) {
|
||||
Modifier.clickableSingle(onClick = state.onWalletClick)
|
||||
} else {
|
||||
Modifier
|
||||
}.then(itemsModifier),
|
||||
walletName = state.walletName,
|
||||
showEndIcon = state.onWalletClick != null,
|
||||
)
|
||||
}
|
||||
PortfolioRowItem(portfolioSelectRow = state.portfolioSelectRow)
|
||||
HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary)
|
||||
SelectNetworksBlock(
|
||||
modifier = Modifier
|
||||
|
|
@ -341,56 +327,6 @@ private fun PortfolioRowItem(portfolioSelectRow: PortfolioSelectUM, modifier: Mo
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.testTag(WalletConnectBottomSheetTestTags.WALLET_ICON),
|
||||
painter = painterResource(R.drawable.ic_wallet_new_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing4)
|
||||
.testTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE),
|
||||
text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing16)
|
||||
.testTag(WalletConnectBottomSheetTestTags.WALLET_NAME),
|
||||
text = walletName,
|
||||
textAlign = TextAlign.End,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (showEndIcon) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12)
|
||||
.size(width = 18.dp, height = 24.dp),
|
||||
painter = painterResource(R.drawable.ic_select_18_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
|
|
@ -673,9 +609,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
|
|||
verifiedDAppState = VerifiedDAppState.Verified {},
|
||||
appSubtitle = "react-app.walletconnect.com",
|
||||
notification = WcAppInfoSecurityNotification.SecurityRisk,
|
||||
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
|
||||
onWalletClick = null,
|
||||
portfolioSelectRow = null,
|
||||
portfolioSelectRow = PortfolioSelectRowPreviewData.account,
|
||||
networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks(
|
||||
items = persistentListOf(
|
||||
WcNetworkInfoItem.Required(
|
||||
|
|
@ -720,9 +654,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
|
|||
verifiedDAppState = VerifiedDAppState.Unknown,
|
||||
appSubtitle = "react-app.walletconnect.com",
|
||||
notification = WcAppInfoSecurityNotification.UnknownDomain,
|
||||
portfolioSelectRow = null,
|
||||
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
|
||||
onWalletClick = {},
|
||||
portfolioSelectRow = PortfolioSelectRowPreviewData.wallet,
|
||||
networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(networks = "Solana"),
|
||||
onNetworksClick = {},
|
||||
onDismiss = {},
|
||||
|
|
@ -735,8 +667,6 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
|
|||
appSubtitle = "react-app.walletconnect.com",
|
||||
notification = WcAppInfoSecurityNotification.UnknownDomain,
|
||||
portfolioSelectRow = PortfolioSelectRowPreviewData.account,
|
||||
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
|
||||
onWalletClick = {},
|
||||
networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(networks = "Solana"),
|
||||
onNetworksClick = {},
|
||||
onDismiss = {},
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
package com.tangem.features.walletconnect.connections.utils
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.wallet.utils.UserWalletsFetcher
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@ModelScoped
|
||||
internal class WcUserWalletsFetcher(
|
||||
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
|
||||
messageSender: UiMessageSender,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val onWalletSelected: (UserWalletId) -> Unit,
|
||||
) {
|
||||
|
||||
private val userWalletsFetcher = userWalletsFetcherFactory.create(
|
||||
messageSender = messageSender,
|
||||
onlyMultiCurrency = true,
|
||||
isAuthMode = false,
|
||||
isClickableIfLocked = false,
|
||||
onWalletClick = { onWalletSelected(it) },
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = userWalletsFetcher.userWallets
|
||||
.flatMapLatest { listOfWalletItem ->
|
||||
val flows = listOfWalletItem.map(::getTokenListFlow)
|
||||
combine(flows) { it.toList().toImmutableList() }
|
||||
}
|
||||
|
||||
private fun getTokenListFlow(walletItem: UserWalletItemUM): Flow<UserWalletItemUM> {
|
||||
return singleAccountStatusListSupplier(userWalletId = UserWalletId(walletItem.id)).map { accountStatusList ->
|
||||
val information = when (accountStatusList.totalFiatBalance) {
|
||||
is TotalFiatBalance.Failed -> UserWalletItemUM.Information.Failed
|
||||
is TotalFiatBalance.Loading -> UserWalletItemUM.Information.Loading
|
||||
is TotalFiatBalance.Loaded -> tokenCountInfo(accountStatusList.flattenCurrencies().size)
|
||||
}
|
||||
|
||||
walletItem.copy(information = information)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tokenCountInfo(count: Int): UserWalletItemUM.Information.Loaded {
|
||||
val text = TextReference.PluralRes(
|
||||
id = R.plurals.card_label_token_count,
|
||||
count = count,
|
||||
formatArgs = wrappedList(count),
|
||||
)
|
||||
return UserWalletItemUM.Information.Loaded(text)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,11 +28,6 @@ internal interface WalletConnectModelModule {
|
|||
@ClassKey(WcPairModel::class)
|
||||
fun bindWcPairModel(model: WcPairModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(WcSelectWalletModel::class)
|
||||
fun bindWcSelectWalletModel(model: WcSelectWalletModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(WcSelectNetworksModel::class)
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ internal class WcAddNetworkModel @Inject constructor(
|
|||
network = useCase.network,
|
||||
emulationStatus = null,
|
||||
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
|
||||
accountDerivation = useCase.session.account?.derivationIndex?.value,
|
||||
accountDerivation = useCase.session.account.derivationIndex.value,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ internal class WcSendTransactionModel @Inject constructor(
|
|||
rawRequest = useCase.rawSdkRequest,
|
||||
network = useCase.network,
|
||||
securityStatus = securityStatusState.value.toCheckDAppResult(),
|
||||
accountDerivation = useCase.session.account?.derivationIndex?.value,
|
||||
accountDerivation = useCase.session.account.derivationIndex.value,
|
||||
)
|
||||
analytics.send(event)
|
||||
showSuccessSignMessage()
|
||||
|
|
@ -466,7 +466,7 @@ internal class WcSendTransactionModel @Inject constructor(
|
|||
network = useCase.network,
|
||||
emulationStatus = emulationStatus,
|
||||
securityStatus = securityCheck.toCheckDAppResult(),
|
||||
accountDerivation = useCase.session.account?.derivationIndex?.value,
|
||||
accountDerivation = useCase.session.account.derivationIndex.value,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ internal class WcSignTransactionModel @Inject constructor(
|
|||
rawRequest = useCase.rawSdkRequest,
|
||||
network = useCase.network,
|
||||
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
|
||||
accountDerivation = useCase.session.account?.derivationIndex?.value,
|
||||
accountDerivation = useCase.session.account.derivationIndex.value,
|
||||
)
|
||||
analytics.send(event)
|
||||
showSuccessSignMessage()
|
||||
|
|
@ -172,7 +172,7 @@ internal class WcSignTransactionModel @Inject constructor(
|
|||
network = useCase.network,
|
||||
emulationStatus = null,
|
||||
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
|
||||
accountDerivation = useCase.session.account?.derivationIndex?.value,
|
||||
accountDerivation = useCase.session.account.derivationIndex.value,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue