Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-14 15:39:54 +03:00
parent 70d2f5ee06
commit 93abd29e1a
32 changed files with 609 additions and 118 deletions

View file

@ -29,4 +29,5 @@ dependencies {
implementation(deps.material)
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(deps.zxing.qrCore)
}

View file

@ -0,0 +1,29 @@
package com.tangem.core.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.extensions.toQrCode
import com.tangem.core.ui.res.TangemTheme
@Composable
fun rememberQrPainters(
content: List<String>,
size: Dp = TangemTheme.dimens.size248,
padding: Dp = TangemTheme.dimens.spacing0,
): List<BitmapPainter> {
val density = LocalDensity.current
return remember(content) {
content.map { code ->
BitmapPainter(
code.toQrCode(
sizePx = with(density) { size.roundToPx() },
paddingPx = with(density) { padding.roundToPx() },
).asImageBitmap(),
)
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import com.tangem.core.ui.res.TangemTheme
/**
* Tangem bottom sheet with custom draggable header and config
*
* @param config data model containing logic and ui models
* @param content custom bottom sheet content
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TangemBottomSheet(
config: TangemBottomSheetConfig,
content: @Composable ColumnScope.(TangemBottomSheetConfigContent) -> Unit,
) {
ModalBottomSheet(
onDismissRequest = config.onDismissRequest,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheetLarge,
dragHandle = { TangemBottomSheetDraggableHeader() },
) {
content(config.content)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.ui.components.bottomsheets
/**
* Tangem bottom sheet config
*
* @property isShow flag that determine if bottom sheet is shown
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*/
data class TangemBottomSheetConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: TangemBottomSheetConfigContent,
)

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.components.bottomsheets
/**
* General interface for bottom sheet config model
*/
interface TangemBottomSheetConfigContent

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TangemBottomSheetDraggableHeader() {
Surface(
modifier = Modifier
.height(TangemTheme.dimens.size20),
color = TangemTheme.colors.background.primary,
) {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.size(
width = TangemTheme.dimens.size32,
height = TangemTheme.dimens.size4,
)
.background(
color = TangemTheme.colors.icon.inactive,
shape = TangemTheme.shapes.roundedCornersSmall,
),
)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
data class AddressModel(
val value: String,
val type: Type = Type.Default,
) {
enum class Type {
Legacy, Default
}
}

View file

@ -0,0 +1,192 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.R
import com.tangem.core.ui.components.MiddleEllipsisText
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rememberQrPainters
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) {
if (config.content is TokenReceiveBottomSheetConfig && config.isShow) {
TangemBottomSheet(config) { content ->
TokenReceiveBottomSheetContent(
content = content as TokenReceiveBottomSheetConfig,
)
}
}
}
@Composable
private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) {
var selectedAddress by remember { mutableStateOf(content.addresses.first()) }
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing24,
top = TangemTheme.dimens.spacing24,
end = TangemTheme.dimens.spacing24,
bottom = TangemTheme.dimens.spacing16,
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24),
) {
QrCodeContent(
content = content,
onAddressChange = { selectedAddress = it },
)
Text(
text = stringResource(R.string.receive_bottom_sheet_warning_message_full, content.name),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(selectedAddress.value))
},
)
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(selectedAddress.value)
},
)
}
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) {
val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value))
val pagerState = rememberPagerState()
val pageCount = content.addresses.count()
LaunchedEffect(key1 = pagerState.currentPage) {
onAddressChange.invoke(content.addresses[pagerState.currentPage])
}
HorizontalPager(
pageCount = pageCount,
state = pagerState,
) { currentPage ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
) {
Text(
text = stringResource(
R.string.receive_bottom_sheet_warning_message,
getName(content = content, index = pagerState.currentPage),
content.symbol,
content.network,
),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h3,
)
Image(
painter = qrCodes[currentPage],
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(TangemTheme.dimens.size248),
)
MiddleEllipsisText(
text = content.addresses[currentPage].value,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.subtitle1,
)
}
}
if (pageCount > 1) {
val indicatorState = rememberLazyListState()
val selectedColor = TangemTheme.colors.icon.primary1
val unselectedColor = TangemTheme.colors.icon.informative
LazyRow(
modifier = Modifier
.height(TangemTheme.dimens.size20),
state = indicatorState,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(pageCount) { iteration ->
item(key = iteration) {
val color = if (pagerState.currentPage == iteration) {
selectedColor
} else {
unselectedColor
}
Box(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing4,
top = TangemTheme.dimens.spacing6,
end = TangemTheme.dimens.spacing4,
bottom = TangemTheme.dimens.spacing6,
)
.background(color, CircleShape)
.size(TangemTheme.dimens.size7),
)
}
}
}
}
}
@Composable
private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String {
return if (content.addresses.size < 2) {
content.name
} else {
"${
stringResource(
id = when (content.addresses[index].type) {
AddressModel.Type.Default -> R.string.address_type_default
AddressModel.Type.Legacy -> R.string.address_type_legacy
},
)
} ${content.name}"
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
class TokenReceiveBottomSheetConfig(
val name: String,
val symbol: String,
val network: String,
val addresses: List<AddressModel>,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.extensions
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
fun Context.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
ContextCompat.startActivity(this, shareIntent, null)
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.extensions
import android.graphics.Bitmap
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.util.Hashtable
@Suppress("MagicNumber")
fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap {
val hintMap = Hashtable<EncodeHintType, Any>()
hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage
hintMap[EncodeHintType.MARGIN] = paddingPx
val qrCodeWriter = QRCodeWriter()
val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, sizePx, sizePx, hintMap)
val width = bitMatrix.width
val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until width) {
bmp.setPixel(
y,
x,
if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE,
)
}
}
return bmp
}

View file

@ -81,6 +81,7 @@ data class TangemDimens internal constructor(
val size158: Dp = 158.dp,
val size164: Dp = 164.dp,
val size200: Dp = 200.dp,
val size248: Dp = 248.dp,
// endregion Size
// region Spacing
val spacing0: Dp = 0.dp,

View file

@ -13,6 +13,7 @@ data class TangemShapes internal constructor(
val roundedCornersXMedium: Shape,
val roundedCornersLarge: Shape,
val bottomSheet: Shape,
val bottomSheetLarge: Shape,
) {
constructor(dimens: TangemDimens) : this(
roundedCornersSmall = RoundedCornerShape(size = dimens.radius2),
@ -25,5 +26,9 @@ data class TangemShapes internal constructor(
topStart = dimens.radius16,
topEnd = dimens.radius16,
),
bottomSheetLarge = RoundedCornerShape(
topStart = dimens.radius24,
topEnd = dimens.radius24,
),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.walletmanager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.extensions.Result
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.config.ConfigManager
@ -202,6 +203,21 @@ class DefaultWalletManagersFacade(
return walletManager
}
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address> {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
return getOrCreateWalletManager(
userWallet = userWallet,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
?.wallet
?.addresses
?.sortedBy { it.type }
.orEmpty()
}
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
if (tokens.isEmpty()) return

View file

@ -2,6 +2,7 @@ package com.tangem.domain.walletmanager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.models.PaginationWrapper
@ -70,4 +71,12 @@ interface WalletManagersFacade {
blockchain: Blockchain,
derivationPath: String?,
): WalletManager?
/**
* Returns ordered list of addresses for selected wallet for given currency
*
* @param userWalletId selected wallet id
* @param network network of currency
*/
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address>
}

View file

@ -17,6 +17,7 @@ dependencies {
// region Domain modules
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
// endregion

View file

@ -90,5 +90,6 @@ internal object TokenDetailsPreviewData {
dialogConfig = null,
pendingTxs = persistentListOf(),
pullToRefreshConfig = pullToRefreshConfig,
bottomSheetConfig = null,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -16,4 +17,5 @@ internal data class TokenDetailsState(
val txHistoryState: TxHistoryState,
val dialogConfig: TokenDetailsDialogConfig?,
val pullToRefreshConfig: TokenDetailsPullToRefreshConfig,
val bottomSheetConfig: TangemBottomSheetConfig?,
)

View file

@ -51,6 +51,7 @@ internal class TokenDetailsSkeletonStateConverter(
),
dialogConfig = null,
pullToRefreshConfig = createPullToRefresh(),
bottomSheetConfig = null,
)
}

View file

@ -2,7 +2,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import androidx.paging.PagingData
import arrow.core.Either
import com.tangem.blockchain.common.address.Address
import com.tangem.common.Provider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -131,4 +135,31 @@ internal class TokenDetailsStateFactory(
fun getRefreshedState(): TokenDetailsState {
return refreshStateConverter.convert(false)
}
fun getStateWithReceiveBottomSheet(currency: CryptoCurrency, addresses: List<Address>): TokenDetailsState {
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
content = TokenReceiveBottomSheetConfig(
name = currency.name,
symbol = currency.symbol,
network = currency.network.name,
addresses = addresses.map {
AddressModel(
value = it.value,
type = AddressModel.Type.valueOf(it.type.name),
)
},
),
),
)
}
fun getStateWithClosedBottomSheet(): TokenDetailsState {
val state = currentStateProvider()
return state.copy(
bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false),
)
}
}

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.Transaction
@ -95,6 +96,12 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
}
TokenDetailsDialogs(state = state)
state.bottomSheetConfig?.let { config ->
TokenReceiveBottomSheet(
config = config,
)
}
}
}

View file

@ -25,4 +25,6 @@ interface TokenDetailsClickIntents {
fun onReloadClick()
fun onExploreClick()
fun onDismissBottomSheet()
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
@ -48,6 +49,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val reduxStateHolder: ReduxStateHolder,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
@ -209,7 +211,17 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onReceiveClick() {
// TODO: [REDACTED_JIRA]
viewModelScope.launch(dispatchers.io) {
val addresses = walletManagersFacade.getAddress(
userWalletId = wallet.walletId,
network = cryptoCurrency.network,
)
uiState = stateFactory.getStateWithReceiveBottomSheet(
currency = cryptoCurrency,
addresses = addresses,
)
}
}
override fun onSellClick() {
@ -274,4 +286,8 @@ internal class TokenDetailsViewModel @Inject constructor(
uiState = stateFactory.getRefreshedState()
}.saveIn(refreshStateJobHolder)
}
override fun onDismissBottomSheet() {
uiState = stateFactory.getStateWithClosedBottomSheet()
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common
import androidx.paging.PagingData
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.components.transactions.state.TransactionState
@ -290,10 +291,10 @@ internal object WalletPreviewData {
}
val bottomSheet by lazy {
WalletBottomSheetConfig(
TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets(
content = WalletBottomSheetConfig.UnlockWallets(
onUnlockClick = {},
onScanClick = {},
),

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -20,7 +21,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() {
override val walletsListConfig: WalletsListConfig,
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val notifications: ImmutableList<WalletNotification>,
override val bottomSheetConfig: WalletBottomSheetConfig?,
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val tokensListState: WalletTokensListState,
val tokenActionsBottomSheet: ActionsBottomSheetConfig?,
val onManageTokensClick: () -> Unit,
@ -42,10 +43,10 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() {
WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick),
)
override val bottomSheetConfig = WalletBottomSheetConfig(
override val bottomSheetConfig = TangemBottomSheetConfig(
isShow = isBottomSheetShow,
onDismissRequest = onBottomSheetDismiss,
content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets(
content = WalletBottomSheetConfig.UnlockWallets(
onUnlockClick = onUnlockClick,
onScanClick = onScanClick,
),

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state
import androidx.compose.runtime.Immutable
import androidx.paging.PagingData
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -31,7 +32,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
override val walletsListConfig: WalletsListConfig,
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val notifications: ImmutableList<WalletNotification>,
override val bottomSheetConfig: WalletBottomSheetConfig?,
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val buttons: PersistentList<WalletManageButton>,
override val txHistoryState: TxHistoryState,
val marketPriceBlockState: MarketPriceBlockState,
@ -55,10 +56,10 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick),
)
override val bottomSheetConfig = WalletBottomSheetConfig(
override val bottomSheetConfig = TangemBottomSheetConfig(
isShow = isBottomSheetShow,
onDismissRequest = onBottomSheetDismiss,
content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets(
content = WalletBottomSheetConfig.UnlockWallets(
onUnlockClick = onUnlockClick,
onScanClick = onScanClick,
),

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import kotlinx.collections.immutable.ImmutableList
@ -29,7 +30,7 @@ internal sealed class WalletState {
abstract val notifications: ImmutableList<WalletNotification>
/** Bottom sheet config */
abstract val bottomSheetConfig: WalletBottomSheetConfig?
abstract val bottomSheetConfig: TangemBottomSheetConfig?
/**
* Util function that allow to make a copy

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.components
import androidx.annotation.DrawableRes
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.res.TangemColorPalette
@ -10,90 +11,79 @@ import com.tangem.feature.wallet.impl.R
/**
* Wallet bottom sheet config
*
* @property isShow flag that determine if bottom sheet is shown
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*
[REDACTED_AUTHOR]
*/
// TODO: Finalize notification strings [REDACTED_JIRA]
internal data class WalletBottomSheetConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: BottomSheetContentConfig,
) {
sealed class WalletBottomSheetConfig(
open val title: TextReference,
open val subtitle: TextReference,
@DrawableRes open val iconResId: Int,
open val tint: Color? = null,
val primaryButtonConfig: ButtonConfig,
val secondaryButtonConfig: ButtonConfig,
) : TangemBottomSheetConfigContent {
sealed class BottomSheetContentConfig(
open val title: TextReference,
open val subtitle: TextReference,
@DrawableRes open val iconResId: Int,
open val tint: Color? = null,
val primaryButtonConfig: ButtonConfig,
val secondaryButtonConfig: ButtonConfig,
) {
data class ButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
@DrawableRes val iconResId: Int? = null,
)
data class ButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
@DrawableRes val iconResId: Int? = null,
)
data class UnlockWallets(
val onUnlockClick: () -> Unit,
val onScanClick: () -> Unit,
) : WalletBottomSheetConfig(
title = TextReference.Str(value = "Unlock needed"),
subtitle = TextReference.Str(
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
"incididunt ut labore et dolore magna aliqua.",
),
iconResId = R.drawable.ic_locked_24,
tint = TangemColorPalette.Black,
primaryButtonConfig = ButtonConfig(text = TextReference.Str(value = "Unlock"), onClick = onUnlockClick),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Scan card"),
onClick = onScanClick,
iconResId = R.drawable.ic_tangem_24,
),
)
data class UnlockWallets(
val onUnlockClick: () -> Unit,
val onScanClick: () -> Unit,
) : BottomSheetContentConfig(
title = TextReference.Str(value = "Unlock needed"),
subtitle = TextReference.Str(
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
"incididunt ut labore et dolore magna aliqua.",
),
iconResId = R.drawable.ic_locked_24,
tint = TangemColorPalette.Black,
primaryButtonConfig = ButtonConfig(text = TextReference.Str(value = "Unlock"), onClick = onUnlockClick),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Scan card"),
onClick = onScanClick,
iconResId = R.drawable.ic_tangem_24,
),
)
data class LikeTangemApp(
val onRateTheAppClick: () -> Unit,
val onShareClick: () -> Unit,
) : WalletBottomSheetConfig(
title = TextReference.Str(value = "Like Tangem App?"),
subtitle = TextReference.Str(value = "How was your experience with our app? Let us know:"),
iconResId = R.drawable.ic_star_24,
tint = TangemColorPalette.Tangerine,
primaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Rate the app"),
onClick = onRateTheAppClick,
),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Share feedback"),
onClick = onShareClick,
),
)
data class LikeTangemApp(
val onRateTheAppClick: () -> Unit,
val onShareClick: () -> Unit,
) : BottomSheetContentConfig(
title = TextReference.Str(value = "Like Tangem App?"),
subtitle = TextReference.Str(value = "How was your experience with our app? Let us know:"),
iconResId = R.drawable.ic_star_24,
tint = TangemColorPalette.Tangerine,
primaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Rate the app"),
onClick = onRateTheAppClick,
),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Share feedback"),
onClick = onShareClick,
),
)
data class CriticalWarningAlreadySignedHashes(
val onOkClick: () -> Unit,
val onCancelClick: () -> Unit,
) : BottomSheetContentConfig(
title = TextReference.Res(
id = R.string.warning_important_security_info,
formatArgs = WrappedList(listOf("\u26A0")),
),
subtitle = TextReference.Res(id = R.string.alert_signed_hashes_message),
iconResId = R.drawable.img_attention_20,
tint = null,
primaryButtonConfig = ButtonConfig(
text = TextReference.Res(id = R.string.common_ok),
onClick = onOkClick,
),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Res(id = R.string.common_cancel),
onClick = onCancelClick,
),
)
}
data class CriticalWarningAlreadySignedHashes(
val onOkClick: () -> Unit,
val onCancelClick: () -> Unit,
) : WalletBottomSheetConfig(
title = TextReference.Res(
id = R.string.warning_important_security_info,
formatArgs = WrappedList(listOf("\u26A0")),
),
subtitle = TextReference.Res(id = R.string.alert_signed_hashes_message),
iconResId = R.drawable.img_attention_20,
tint = null,
primaryButtonConfig = ButtonConfig(
text = TextReference.Res(id = R.string.common_ok),
onClick = onOkClick,
),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Res(id = R.string.common_cancel),
onClick = onCancelClick,
),
)
}

View file

@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory
import androidx.paging.PagingData
import arrow.core.Either
import com.tangem.common.Provider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.tokens.error.CurrencyStatusError
@ -18,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetCon
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter
@ -161,10 +162,10 @@ internal class WalletStateFactory(
fun getRefreshedState(): WalletState = refreshStateConverter.convert(value = false)
fun getStateWithOpenWalletBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState {
fun getStateWithOpenWalletBottomSheet(content: TangemBottomSheetConfigContent): WalletState {
return when (val state = currentStateProvider() as WalletState.ContentState) {
is WalletMultiCurrencyState.Content -> state.copy(
bottomSheetConfig = WalletBottomSheetConfig(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
content = content,
@ -175,7 +176,7 @@ internal class WalletStateFactory(
onBottomSheetDismiss = clickIntents::onDismissBottomSheet,
)
is WalletSingleCurrencyState.Content -> state.copy(
bottomSheetConfig = WalletBottomSheetConfig(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
content = content,

View file

@ -18,6 +18,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
@ -25,6 +27,7 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
@ -167,7 +170,15 @@ private fun ManageTokensButton(onManageTokensClick: () -> Unit) {
private fun WalletBottomSheets(state: WalletState) {
val bottomSheetConfig = (state as? WalletState.ContentState)?.bottomSheetConfig
if (bottomSheetConfig != null && bottomSheetConfig.isShow) {
WalletBottomSheet(config = bottomSheetConfig)
when (bottomSheetConfig.content) {
is WalletBottomSheetConfig -> {
WalletBottomSheet(config = bottomSheetConfig)
}
is TokenReceiveBottomSheetConfig -> {
TokenReceiveBottomSheet(config = bottomSheetConfig)
}
}
}
(state as? WalletMultiCurrencyState.Content)?.let { multiCurrencyState ->

View file

@ -15,6 +15,8 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
@ -27,21 +29,17 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBott
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun WalletBottomSheet(config: WalletBottomSheetConfig) {
ModalBottomSheet(
onDismissRequest = config.onDismissRequest,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = TangemTheme.colors.background.primary,
dragHandle = { BottomSheetDefaults.DragHandle() },
) {
BottomSheetContent(config = config.content)
internal fun WalletBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(config) { content ->
BottomSheetContent(
config = content as WalletBottomSheetConfig,
)
}
}
@Composable
private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetContentConfig) {
private fun BottomSheetContent(config: WalletBottomSheetConfig) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
@ -90,10 +88,7 @@ private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetConten
}
@Composable
private fun PrimaryButton(
config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig,
modifier: Modifier = Modifier,
) {
private fun PrimaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) {
if (config.iconResId == null) {
PrimaryButton(
text = config.text.resolveReference(),
@ -111,10 +106,7 @@ private fun PrimaryButton(
}
@Composable
private fun SecondaryButton(
config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig,
modifier: Modifier = Modifier,
) {
private fun SecondaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) {
if (config.iconResId == null) {
SecondaryButton(
text = config.text.resolveReference(),
@ -139,7 +131,7 @@ private fun WalletBottomSheetContent_Light(
) {
TangemTheme(isDark = false) {
// Use preview of content because ModalBottomSheet isn't supported in Preview mode
BottomSheetContent(config = config.content)
BottomSheetContent(config = config)
}
}
@ -151,10 +143,10 @@ private fun WalletBottomSheetContent_Dark(
) {
TangemTheme(isDark = false) {
// Use preview of content because ModalBottomSheet isn't supported in Preview mode
BottomSheetContent(config = config.content)
BottomSheetContent(config = config)
}
}
private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider<WalletBottomSheetConfig>(
private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider<TangemBottomSheetConfig>(
collection = listOf(WalletPreviewData.bottomSheet),
)

View file

@ -7,6 +7,8 @@ import com.tangem.common.Provider
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -27,6 +29,7 @@ import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
@ -86,6 +89,7 @@ internal class WalletViewModel @Inject constructor(
private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val reduxStateHolder: ReduxStateHolder,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents {
@ -271,7 +275,7 @@ internal class WalletViewModel @Inject constructor(
override fun onCriticalWarningAlreadySignedHashesClick() {
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
content = WalletBottomSheetConfig.BottomSheetContentConfig.CriticalWarningAlreadySignedHashes(
content = WalletBottomSheetConfig.CriticalWarningAlreadySignedHashes(
onOkClick = {},
onCancelClick = {},
),
@ -284,7 +288,7 @@ internal class WalletViewModel @Inject constructor(
override fun onLikeTangemAppClick() {
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
content = WalletBottomSheetConfig.LikeTangemApp(
onRateTheAppClick = ::onRateTheAppClick,
onShareClick = ::onShareClick,
),
@ -428,7 +432,31 @@ internal class WalletViewModel @Inject constructor(
}
override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
// TODO: [REDACTED_JIRA]
val state = uiState as? WalletState.ContentState ?: return
viewModelScope.launch(dispatchers.io) {
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
val addresses = walletManagersFacade.getAddress(
userWalletId = userWallet.walletId,
network = cryptoCurrencyStatus.currency.network,
)
val currency = cryptoCurrencyStatus.currency
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
content = TokenReceiveBottomSheetConfig(
name = currency.name,
symbol = currency.symbol,
network = currency.network.name,
addresses = addresses.map {
AddressModel(
value = it.value,
type = AddressModel.Type.valueOf(it.type.name),
)
},
),
)
}
}
override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {