Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-21 17:09:31 +05:00
parent b9e992c533
commit 369f449c09
10 changed files with 263 additions and 106 deletions

View file

@ -0,0 +1,99 @@
package com.tangem.core.ui.components.bottomsheets
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
/**
* Generic options bottom sheet component
*
* @param config Bottom sheet configuration containing OptionsBottomSheetContent
* @param title Title text for the bottom sheet
* @param containerColor Background color of the bottom sheet
*/
@Composable
fun OptionsBottomSheet(
config: TangemBottomSheetConfig,
title: TextReference,
containerColor: androidx.compose.ui.graphics.Color = TangemTheme.colors.background.tertiary,
) {
TangemBottomSheet<OptionsBottomSheetContent>(
config = config,
titleText = title,
containerColor = containerColor,
content = { content ->
OptionsBottomSheetContent(content = content)
},
)
}
@Composable
private fun OptionsBottomSheetContent(content: OptionsBottomSheetContent) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
content.options.forEachIndexed { index, option ->
InputRowDefault(
text = option.label,
showDivider = index < content.options.size - 1,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = content.options.size - 1,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(option.key) },
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OptionsBottomSheetPreview() {
TangemThemePreview {
OptionsBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = OptionsBottomSheetContent(
options = persistentListOf(
BottomSheetOption(
key = "option1",
label = TextReference.Str("First Option"),
),
BottomSheetOption(
key = "option2",
label = TextReference.Str("Second Option"),
),
BottomSheetOption(
key = "option3",
label = TextReference.Str("Third Option"),
),
),
onOptionClick = {},
),
),
title = TextReference.Str("Select Option"),
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.core.ui.components.bottomsheets
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* @param key Unique identifier for the option
* @param label Display text for the option
*/
data class BottomSheetOption(
val key: String,
val label: TextReference,
)
/**
* @param options List of options to display
* @param onOptionClick Callback when an option is clicked, receives the option key
*/
data class OptionsBottomSheetContent(
val options: ImmutableList<BottomSheetOption> = persistentListOf(),
val onOptionClick: (String) -> Unit = {},
) : TangemBottomSheetConfigContent

View file

@ -18,6 +18,7 @@ dependencies {
implementation(projects.features.wallet.api)
implementation(projects.features.disclaimer.api)
implementation(projects.features.tester.api)
implementation(projects.features.createWalletSelection.api)
/* Project - Core */
implementation(projects.core.decompose)

View file

@ -2,6 +2,7 @@ package com.tangem.features.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@ -11,4 +12,5 @@ internal data class UserWalletListUM(
val isWalletSavingInProgress: Boolean,
val addNewWalletText: TextReference,
val onAddNewWalletClick: () -> Unit,
val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
)

View file

@ -6,12 +6,17 @@ 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.navigation.url.UrlOpener
import com.tangem.core.ui.R.*
import com.tangem.core.ui.components.bottomsheets.BottomSheetOption
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
@ -20,16 +25,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class UserWalletListModel @Inject constructor(
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
private val userWalletSaver: UserWalletSaver,
override val dispatchers: CoroutineDispatcherProvider,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
) : Model() {
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
@ -45,7 +53,8 @@ internal class UserWalletListModel @Inject constructor(
userWallets = persistentListOf(),
isWalletSavingInProgress = false,
addNewWalletText = TextReference.EMPTY,
onAddNewWalletClick = ::addUserWallet,
onAddNewWalletClick = ::showAddWalletBottomSheet,
addWalletBottomSheet = TangemBottomSheetConfig.Empty,
),
)
@ -54,8 +63,9 @@ internal class UserWalletListModel @Inject constructor(
flow = userWalletsFetcher.userWallets,
flow2 = shouldSaveUserWalletsUseCase(),
flow3 = isWalletSavingInProgress,
transform = ::updateState,
).launchIn(modelScope)
) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress ->
updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress)
}.launchIn(modelScope)
}
private fun updateState(
@ -74,7 +84,58 @@ internal class UserWalletListModel @Inject constructor(
)
}
private fun addUserWallet() = withProgress(isWalletSavingInProgress) {
userWalletSaver.scanAndSaveUserWallet(modelScope)
private fun showAddWalletBottomSheet() {
state.update { currentState ->
currentState.copy(
addWalletBottomSheet = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismissAddWalletBottomSheet,
content = createAddWalletBottomSheetContent(),
),
)
}
}
private fun dismissAddWalletBottomSheet() {
state.update { currentState ->
currentState.copy(
addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false),
)
}
}
private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent {
return OptionsBottomSheetContent(
options = persistentListOf(
BottomSheetOption(
key = ADD_WALLET_KEY_CREATE,
label = resourceReference(string.home_button_create_new_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_ADD,
label = resourceReference(string.home_button_add_existing_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_BUY,
label = resourceReference(string.details_buy_wallet),
),
),
onOptionClick = { optionKey ->
dismissAddWalletBottomSheet()
when (optionKey) {
ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection)
ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet)
ADD_WALLET_KEY_BUY -> modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
},
)
}
companion object {
private const val ADD_WALLET_KEY_CREATE = "create"
private const val ADD_WALLET_KEY_ADD = "add"
private const val ADD_WALLET_KEY_BUY = "buy"
}
}

View file

@ -15,9 +15,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.R.*
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.details.component.UserWalletListComponent
@ -44,6 +48,8 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
onClick = state.onAddNewWalletClick,
)
}
AddWalletBottomSheet(state.addWalletBottomSheet)
}
@Composable
@ -94,6 +100,15 @@ private fun AddWalletButton(
}
}
@Composable
private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
OptionsBottomSheet(
config = config,
title = resourceReference(string.auth_info_add_wallet_title),
containerColor = TangemTheme.colors.background.tertiary,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)

View file

@ -18,6 +18,7 @@ dependencies {
/** Core */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.analytics)
implementation(projects.common.routing)

View file

@ -6,8 +6,12 @@ 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.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.BottomSheetOption
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
@ -15,11 +19,10 @@ import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.features.welcome.impl.R
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.*
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -46,6 +49,8 @@ internal class WelcomeModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase,
private val walletsRepository: WalletsRepository,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
) : Model() {
val uiState: StateFlow<WelcomeUM>
@ -149,8 +154,27 @@ internal class WelcomeModel @Inject constructor(
currentState.copy(
addWalletBottomSheet = TangemBottomSheetConfig(
isShown = true,
content = AddWalletBottomSheetContentUM(
onOptionClick = ::onAddWalletOptionClick,
content = OptionsBottomSheetContent(
options = persistentListOf(
BottomSheetOption(
key = ADD_WALLET_KEY_CREATE,
label = resourceReference(R.string.home_button_create_new_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_ADD,
label = resourceReference(R.string.home_button_add_existing_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_BUY,
label = resourceReference(R.string.details_buy_wallet),
),
),
onOptionClick = { optionKey ->
updateSelectState {
it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false))
}
onAddWalletOptionClick(optionKey)
},
),
onDismissRequest = {
updateSelectState {
@ -162,11 +186,13 @@ internal class WelcomeModel @Inject constructor(
}
}
private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) {
when (option) {
Create -> router.push(AppRoute.CreateWalletSelection)
Add -> router.push(AppRoute.AddExistingWallet)
Buy -> Unit // TODO
private fun onAddWalletOptionClick(optionKey: String) {
when (optionKey) {
ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection)
ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet)
ADD_WALLET_KEY_BUY -> modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
}
@ -246,4 +272,10 @@ internal class WelcomeModel @Inject constructor(
}
}
}
companion object {
private const val ADD_WALLET_KEY_CREATE = "create"
private const val ADD_WALLET_KEY_ADD = "add"
private const val ADD_WALLET_KEY_BUY = "buy"
}
}

View file

@ -1,90 +0,0 @@
package com.tangem.features.welcome.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.welcome.impl.R
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM
@Composable
fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<AddWalletBottomSheetContentUM>(
config = config,
titleText = resourceReference(R.string.auth_info_add_wallet_title),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: AddWalletBottomSheetContentUM) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
InputRowDefault(
text = resourceReference(R.string.home_button_create_new_wallet),
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = 3,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) },
)
InputRowDefault(
text = resourceReference(R.string.home_button_add_existing_wallet),
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 1,
lastIndex = 2,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) },
)
InputRowDefault(
text = resourceReference(R.string.details_buy_wallet),
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 2,
lastIndex = 2,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Buy) },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SecurityScoreBottomSheetPreview() {
TangemThemePreview {
AddWalletBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = AddWalletBottomSheetContentUM(),
),
)
}
}

View file

@ -19,9 +19,13 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.R.*
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.welcome.impl.R
@ -170,4 +174,13 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) {
color = TangemTheme.colors.text.secondary,
)
}
}
@Composable
fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
OptionsBottomSheet(
config = config,
title = resourceReference(string.auth_info_add_wallet_title),
containerColor = TangemTheme.colors.background.tertiary,
)
}