Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 11:51:52 +03:00
commit 4076059447
1222 changed files with 26275 additions and 12629 deletions

View file

@ -21,4 +21,5 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.account)
}

View file

@ -1,24 +0,0 @@
package com.tangem.features.account
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.account.Account
import kotlinx.coroutines.flow.StateFlow
interface AccountSelectorComponent : ComposableBottomSheetComponent {
data class Params(
val onDismiss: () -> Unit,
val accountsBalanceFetcher: AccountsBalanceFetcher,
val controller: AccountSelectorController,
)
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): AccountSelectorComponent
}
}
interface AccountSelectorController {
val selectedAccount: StateFlow<Account?>
fun selectAccount(account: Account?)
}

View file

@ -1,9 +1,9 @@
package com.tangem.features.account
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListError
@ -11,7 +11,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface AccountsBalanceFetcher {
interface PortfolioFetcher {
val data: Flow<Data>
@ -21,12 +21,15 @@ interface AccountsBalanceFetcher {
data class Data(
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val balances: Map<UserWallet, Map<Account, AccountBalance>>,
val balances: Map<UserWallet, PortfolioBalance>,
)
data class AccountBalance(
val balance: Lce<TokenListError, TotalFiatBalance>,
)
data class PortfolioBalance(
val walletBalance: Lce<TokenListError, TotalFiatBalance>,
val accountsBalance: AccountStatusList,
) {
val userWallet: UserWallet get() = accountsBalance.userWallet
}
sealed interface Mode {
data class All(val onlyMultiCurrency: Boolean) : Mode
@ -34,6 +37,6 @@ interface AccountsBalanceFetcher {
}
interface Factory {
fun create(mode: Mode, scope: CoroutineScope): AccountsBalanceFetcher
fun create(mode: Mode, scope: CoroutineScope): PortfolioFetcher
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.account
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface PortfolioSelectorComponent : ComposableBottomSheetComponent, ComposableContentComponent {
val title: StateFlow<TextReference>
data class Params(
val onDismiss: () -> Unit,
val portfolioFetcher: PortfolioFetcher,
val controller: PortfolioSelectorController,
)
interface Factory : ComponentFactory<Params, PortfolioSelectorComponent>
}
/**
* if [isAccountMode] is false it's mean [selectedAccount] emit [AccountId] for Main account
*/
interface PortfolioSelectorController {
val isAccountMode: Flow<Boolean>
val selectedAccount: StateFlow<AccountId?>
fun selectAccount(accountId: AccountId?)
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?>
}

View file

@ -14,6 +14,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.account.api)
implementation(projects.features.wallet.api)
/** Core modules */
implementation(projects.core.analytics)
@ -29,6 +30,7 @@ dependencies {
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.core)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
@ -39,6 +41,9 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** Tangem libraries */
implementation(tangemDeps.card.core)
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)

View file

@ -1,8 +1,8 @@
package com.tangem.features.account.createedit
import androidx.annotation.StringRes
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.toDomain
import androidx.annotation.StringRes
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
@ -13,11 +13,11 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.res.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
@ -60,7 +60,7 @@ internal class AccountCreateEditModel @Inject constructor(
private val umBuilder = AccountCreateEditUMBuilder(params)
val uiState: StateFlow<AccountCreateEditUM>
field = MutableStateFlow(value = getInitialState())
field = MutableStateFlow(value = getInitialState())
init {
if (params is AccountCreateEditComponent.Params.Create) {
@ -75,13 +75,17 @@ internal class AccountCreateEditModel @Inject constructor(
)
val firstAction = EventMessageAction(
title = resourceReference(R.string.account_unsaved_dialog_action_second),
warning = true,
isWarning = true,
onClick = { router.pop() },
)
val messageRes = when (params) {
is AccountCreateEditComponent.Params.Create -> R.string.account_unsaved_dialog_message_create
is AccountCreateEditComponent.Params.Edit -> R.string.account_unsaved_dialog_message_edit
}
messageSender.send(
DialogMessage(
title = resourceReference(R.string.account_unsaved_dialog_title),
message = resourceReference(R.string.account_unsaved_dialog_message_create),
message = resourceReference(messageRes),
firstActionBuilder = { firstAction },
secondActionBuilder = { secondAction },
),
@ -110,20 +114,34 @@ internal class AccountCreateEditModel @Inject constructor(
derivationIndex = derivationIndex,
)
uiState.value = uiState.value.toggleProgress(showProgress = false)
result
.onLeft { showMessage(it.toString()) }
.onLeft(::handleAddAccountError)
.onRight {
showMessage(R.string.account_create_success_message)
router.pop()
}
}
private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) {
val duplicateAccountNames = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
?.cause is AccountList.Error.DuplicateAccountNames
when {
duplicateAccountNames -> showAccountNameExist()
else -> {
showSomethingWrong()
logError(error = AccountFeatureError.CreateAccount.FailedToCreateAccount(cause = error))
}
}
}
private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) {
val state = uiState.value
val name = state.account.name.toDomain().getOrNull() ?: return
val icon = state.account.portfolioIcon.toDomain()
val isNewName = name != params.account.accountName
val isNewIcon = icon != params.account.portfolioIcon
uiState.value = uiState.value.toggleProgress(showProgress = true)
val result = updateCryptoPortfolioUseCase(
icon = if (isNewIcon) icon else null,
@ -131,25 +149,37 @@ internal class AccountCreateEditModel @Inject constructor(
accountId = params.account.accountId,
)
uiState.value = uiState.value.toggleProgress(showProgress = false)
result
.onLeft { showMessage(it.toString()) }
.onLeft(::handleEditAccountError)
.onRight {
showMessage(R.string.account_edit_success_message)
router.pop()
}
}
private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) {
val duplicateAccountNames = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
?.cause is AccountList.Error.DuplicateAccountNames
when {
duplicateAccountNames -> showAccountNameExist()
else -> {
showSomethingWrong()
logError(error = AccountFeatureError.EditAccount.FailedToEditAccount(cause = error))
}
}
}
private fun showMessage(@StringRes id: Int) {
val message = resourceReference(id)
messageSender.send(ToastMessage(message = message))
}
private fun showMessage(text: String) {
messageSender.send(ToastMessage(message = stringReference(text)))
private fun onCloseClick() {
val showConfirmDialog = uiState.value.buttonState.isButtonEnabled
if (showConfirmDialog) unsaveChangeDialog() else router.pop()
}
private fun onCloseClick() = unsaveChangeDialog()
private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) {
uiState.value = uiState.value
.updateIconSelect(icon)
@ -203,33 +233,46 @@ internal class AccountCreateEditModel @Inject constructor(
}
}
.onLeft { cause ->
handleError(
error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex,
message = cause.toString(),
val error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex(cause)
logError(
error = error,
params = mapOf(
"userWalletId" to userWalletId.stringValue,
"cause" to cause.toString(),
),
)
messageSender.showErrorDialog(universalError = error, onDismiss = router::pop)
return@launch
}
}
}
private fun handleError(
error: AccountFeatureError,
message: String? = null,
params: Map<String, String> = mapOf(),
) {
val exception = IllegalStateException("$error. Cause: $message")
private fun logError(error: AccountFeatureError, params: Map<String, String> = mapOf()) {
val exception = IllegalStateException(error.toString())
Timber.e(exception)
analyticsExceptionHandler.sendException(
event = ExceptionAnalyticsEvent(exception = exception, params = params),
)
}
messageSender.showErrorDialog(universalError = error, onDismiss = router::pop)
private fun showSomethingWrong() {
val dialogMessage = DialogMessage(
title = resourceReference(R.string.common_something_went_wrong),
message = resourceReference(R.string.account_could_not_create),
)
messageSender.send(dialogMessage)
}
private fun showAccountNameExist() {
val dialogMessage = DialogMessage(
title = resourceReference(R.string.common_something_went_wrong),
message = resourceReference(R.string.account_form_name_already_exist_error_description),
)
messageSender.send(dialogMessage)
}
}

View file

@ -1,6 +1,10 @@
package com.tangem.features.account.createedit.error
import com.tangem.core.error.UniversalError
import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
sealed interface AccountFeatureError : UniversalError {
@ -14,16 +18,29 @@ sealed interface AccountFeatureError : UniversalError {
override val subsystemCode: String get() = "001"
data object UnableToGetDerivationIndex : CreateAccount {
data class UnableToGetDerivationIndex(val cause: GetUnoccupiedAccountIndexUseCase.Error) : CreateAccount {
override val specificErrorCode: String = "001"
}
data class FailedToCreateAccount(val cause: AddCryptoPortfolioUseCase.Error) : CreateAccount {
override val specificErrorCode: String = "002"
}
}
sealed interface EditAccount : AccountFeatureError {
override val subsystemCode: String get() = "002"
data object RequiredCryptoPortfolio : EditAccount {
data class FailedToEditAccount(val cause: UpdateCryptoPortfolioUseCase.Error) : EditAccount {
override val specificErrorCode: String = "001"
}
}
sealed interface ArchivedAccountList : AccountFeatureError {
override val subsystemCode: String get() = "003"
data class FailedToRecoverAccount(val cause: RecoverCryptoPortfolioUseCase.Error) : ArchivedAccountList {
override val specificErrorCode: String = "001"
}
}

View file

@ -19,6 +19,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -30,8 +32,8 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.fields.AutoSizeTextField
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
@ -43,9 +45,11 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modifier = Modifier) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.tertiary)
.background(color = TangemTheme.colors.background.secondary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),
@ -76,6 +80,10 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi
color = TangemTheme.colors.text.tertiary,
)
}
if (state.buttonState.showProgress) {
focusManager.clearFocus()
keyboardController?.hide()
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
@ -161,6 +169,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
contentAlignment = Alignment.Center,
modifier = Modifier
.wrapContentSize()
.clip(CircleShape)
.clickable(onClick = { colorsState.onColorSelect(color) })
.size(48.dp),
) {
@ -208,6 +217,7 @@ private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) {
contentAlignment = Alignment.Center,
modifier = Modifier
.wrapContentSize()
.clip(CircleShape)
.clickable(onClick = { iconsState.onIconSelect(icon) })
.size(52.dp),
) {

View file

@ -57,7 +57,7 @@ internal class AccountDetailsModel @Inject constructor(
)
val firstAction = EventMessageAction(
title = resourceReference(R.string.account_details_archive_action),
warning = true,
isWarning = true,
onClick = ::archiveCryptoPortfolio,
)
messageSender.send(

View file

@ -0,0 +1,28 @@
package com.tangem.features.account.di
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.account.fetcher.DefaultPortfolioFetcher
import com.tangem.features.account.selector.DefaultPortfolioSelectorComponent
import com.tangem.features.account.selector.DefaultPortfolioSelectorController
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface AccountFeatureModule {
@Binds
fun bindPortfolioFetcherFactory(impl: DefaultPortfolioFetcher.Factory): PortfolioFetcher.Factory
@Binds
fun bindPortfolioSelectorController(impl: DefaultPortfolioSelectorController): PortfolioSelectorController
@Binds
fun bindPortfolioSelectorComponentFactory(
impl: DefaultPortfolioSelectorComponent.Factory,
): PortfolioSelectorComponent.Factory
}

View file

@ -1,17 +1,16 @@
package com.tangem.features.account.fetcher
import arrow.core.getOrElse
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.account.AccountsBalanceFetcher
import com.tangem.features.account.AccountsBalanceFetcher.*
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioFetcher.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -20,14 +19,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
@Suppress("LongParameterList")
internal class DefaultPortfolioFetcher @AssistedInject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
dispatchers: CoroutineDispatcherProvider,
@Assisted mode: Mode,
@Assisted private val scope: CoroutineScope,
) : AccountsBalanceFetcher {
) : PortfolioFetcher {
private val _mode = MutableStateFlow(mode)
private val _data = MutableSharedFlow<Data>(
@ -56,8 +58,8 @@ internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
.map { it.filterWallets(mode) }
.distinctUntilChanged()
.flatMapLatest { wallets -> balancesForWallets(wallets) },
flow2 = appCurrencyFlow(),
flow3 = balanceHidingFlow(),
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { balances, appCurrency, isBalanceHiding ->
Data(
appCurrency = appCurrency,
@ -73,48 +75,25 @@ internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
}
}
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWallet, Map<Account, AccountBalance>>> =
wallets.asFlow()
.map { walletAccountsBalancesFlow(it) }
.mapLatest { accountsBalances -> combine(accountsBalances) { pairs -> pairs.toMap() } }
.flattenConcat()
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, Map<Account, AccountBalance>>> =
walletAccounts(wallet)
.distinctUntilChanged()
.map { list -> list.map(::accountBalanceFlow) }
.mapLatest { balanceFlows -> combine(balanceFlows) { balances -> balances.toMap() } }
.flattenConcat()
.map { accountBalance -> wallet to accountBalance }
private fun walletAccounts(wallet: UserWallet): Flow<List<Account>> = flow {
// todo account load accounts
val accounts: List<Account> = Account.CryptoPortfolio
.createMainAccount(wallet.walletId)
.let(::listOf)
emit(accounts)
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWallet, PortfolioBalance>> {
val balanceFlows = wallets.map { walletAccountsBalancesFlow(it) }
return combine(balanceFlows) { pairs -> pairs.toMap() }
}
private fun accountBalanceFlow(account: Account): Flow<Pair<Account, AccountBalance>> = flow {
// todo account load balance
val balance = AccountBalance(balance = Lce.Content(TotalFiatBalance.Loading))
emit(account to balance)
}
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, PortfolioBalance>> = combine(
flow = accountStatusListFlow(wallet),
flow2 = getWalletTotalBalanceUseCase(wallet.walletId),
transform = { accountStatusList, walletBalance ->
val portfolioBalance = PortfolioBalance(walletBalance, accountStatusList)
wallet to portfolioBalance
},
)
private fun appCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { it.getOrElse { AppCurrency.Default } }
.distinctUntilChanged()
}
private fun balanceHidingFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
private fun accountStatusListFlow(wallet: UserWallet): Flow<AccountStatusList> =
singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(wallet.walletId))
@AssistedFactory
interface Factory : AccountsBalanceFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultAccountsBalanceFetcher
interface Factory : PortfolioFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultPortfolioFetcher
}
}

View file

@ -1,87 +0,0 @@
package com.tangem.features.account.selector
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
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.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.features.account.AccountSelectorComponent
import com.tangem.features.account.AccountsBalanceFetcher
import com.tangem.features.account.selector.entity.AccountSelectorItemUM
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@ModelScoped
internal class AccountSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<AccountSelectorComponent.Params>()
private val balanceFetcher get() = params.accountsBalanceFetcher
private val selectorController get() = params.controller
internal val state: StateFlow<AccountSelectorUM>
field = MutableStateFlow<AccountSelectorUM>(emptyState())
init {
balanceFetcher.data
.map { data ->
AccountSelectorUM(
isSingleWallet = balanceFetcher.mode.value is AccountsBalanceFetcher.Mode.Wallet,
items = buildUiList(data).toImmutableList(),
)
}
.launchIn(modelScope)
}
private fun buildUiList(data: AccountsBalanceFetcher.Data) = buildList {
fun Account.accountItemState(balance: Lce<TokenListError, TotalFiatBalance>): TokenItemState {
val totalFiatBalance = balance.getOrElse(
ifError = { TotalFiatBalance.Failed },
ifLoading = { TotalFiatBalance.Loading },
)
return when (this) {
is Account.CryptoPortfolio -> AccountCryptoPortfolioItemStateConverter(
appCurrency = data.appCurrency,
account = this,
onItemClick = { selectorController.selectAccount(it) },
).convert(totalFiatBalance)
}
}
data.balances.forEach { wallet, accounts ->
if (wallet.isLocked) return@forEach
AccountSelectorItemUM.Wallet(
id = wallet.walletId.stringValue,
name = stringReference(wallet.name),
).let(::add)
accounts.forEach { account, balance ->
AccountSelectorItemUM.Account(
account = account.accountItemState(balance.balance),
isBalanceHidden = data.isBalanceHidden,
).let(::add)
}
}
}
private fun emptyState() = AccountSelectorUM(
items = persistentListOf(),
balanceFetcher.mode.value is AccountsBalanceFetcher.Mode.Wallet,
)
}

View file

@ -1,69 +0,0 @@
package com.tangem.features.account.selector
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
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.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.account.AccountSelectorComponent
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.ui.AccountSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAccountSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: AccountSelectorComponent.Params,
) : AppComponentContext by appComponentContext, AccountSelectorComponent {
private val model: AccountSelectorModel = getOrCreateModel(params)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = ::dismiss,
containerColor = TangemTheme.colors.background.primary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_choose_wallet),
startIconRes = R.drawable.ic_back_24,
onStartClick = ::dismiss,
)
},
content = {
AccountSelectorContent(
state = state,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
)
},
)
}
@AssistedFactory
interface Factory : AccountSelectorComponent.Factory {
override fun create(
appComponentContext: AppComponentContext,
params: AccountSelectorComponent.Params,
): DefaultAccountSelectorComponent
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.features.account.selector
import com.tangem.domain.models.account.Account
import com.tangem.features.account.AccountSelectorController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
internal class DefaultAccountSelectorController @Inject constructor() : AccountSelectorController {
private val _selectedAccount: MutableStateFlow<Account?> = MutableStateFlow(null)
override val selectedAccount: StateFlow<Account?> get() = _selectedAccount
override fun selectAccount(account: Account?) {
_selectedAccount.update { account }
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.account.selector
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.selector.ui.PortfolioSelectorBS
import com.tangem.features.account.selector.ui.PortfolioSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
internal class DefaultPortfolioSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: PortfolioSelectorComponent.Params,
) : AppComponentContext by appComponentContext, PortfolioSelectorComponent {
private val model: PortfolioSelectorModel = getOrCreateModel(params)
override val title: StateFlow<TextReference>
get() = model.state
.map { it.title }
.stateIn(componentScope, SharingStarted.Lazily, model.state.value.title)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
PortfolioSelectorBS(state, onDismiss = ::dismiss)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
PortfolioSelectorContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : PortfolioSelectorComponent.Factory {
override fun create(
appComponentContext: AppComponentContext,
params: PortfolioSelectorComponent.Params,
): DefaultPortfolioSelectorComponent
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.account.selector
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.account.PortfolioFetcher
import kotlinx.coroutines.flow.*
import javax.inject.Inject
internal class DefaultPortfolioSelectorController @Inject constructor(
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : PortfolioSelectorController {
private val _selectedAccount: MutableStateFlow<AccountId?> = MutableStateFlow(null)
override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() }
override val selectedAccount: StateFlow<AccountId?> get() = _selectedAccount
override fun selectAccount(accountId: AccountId?) {
_selectedAccount.update { accountId }
}
override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?> =
combine(
flow = _selectedAccount,
flow2 = portfolioFetcher.data,
transform = { accountId, data ->
accountId ?: return@combine null
var result: Pair<UserWallet, AccountStatus>? = null
data.balances.forEach { wallet, balance ->
val accountStatuses = balance.accountsBalance.accountStatuses
.find { accountId == it.account.accountId }
if (accountStatuses != null) result = wallet to accountStatuses
}
return@combine result
},
)
}

View file

@ -0,0 +1,186 @@
package com.tangem.features.account.selector
import com.tangem.common.ui.account.AccountPortfolioItemUMConverter
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
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.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.PortfolioSelectorItemUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class PortfolioSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val walletImageFetcher: UserWalletImageFetcher,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : Model() {
private val params = paramsContainer.require<PortfolioSelectorComponent.Params>()
private val balanceFetcher get() = params.portfolioFetcher
private val selectorController get() = params.controller
internal val state: StateFlow<PortfolioSelectorUM>
field = MutableStateFlow<PortfolioSelectorUM>(emptyState())
init {
combine(
flow = isAccountsModeEnabledUseCase(),
flow2 = loadBalanceWithArtwork(),
transform = { isAccountsMode, (portfolioData, artworks) ->
val uiList = buildUiList(isAccountsMode, portfolioData, artworks)
val title = when (isAccountsMode) {
true -> resourceReference(R.string.common_choose_account)
false -> resourceReference(R.string.common_choose_wallet)
}
state.value = PortfolioSelectorUM(
title = title,
items = uiList.toImmutableList(),
)
},
)
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun buildUiList(
isAccountsMode: Boolean,
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): List<PortfolioSelectorItemUM> = when (isAccountsMode) {
true -> buildAccountsList(portfolioData, artworks)
false -> buildWalletList(portfolioData, artworks)
}
private fun buildWalletList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
portfolioData.balances.forEach { wallet, portfolio ->
val balance = portfolio.walletBalance.getOrNull()
val walletItemUM = UserWalletItemUMConverter(
onClick = {
// todo account
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
},
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
artwork = artworks[wallet.walletId],
isAuthMode = false,
).convert(wallet)
if (walletItemUM.isEnabled) {
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
} else {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
}
}
if (lockedWallets.isNotEmpty()) {
val lockedWalletsTitle = PortfolioSelectorItemUM.GroupTitle(
id = "lockedWalletsTitleId",
name = resourceReference(R.string.common_locked_wallets),
)
add(lockedWalletsTitle)
addAll(lockedWallets)
}
}
private fun buildAccountsList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
portfolioData.balances.forEach { wallet, portfolio ->
val balance = portfolio.walletBalance.getOrNull()
val walletItemUM = UserWalletItemUMConverter(
onClick = {
// todo account
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
},
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
artwork = artworks[wallet.walletId],
isAuthMode = false,
).convert(wallet)
if (!walletItemUM.isEnabled) {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
return@forEach
}
val walletTitle = PortfolioSelectorItemUM.GroupTitle(
id = "GroupTitle ${wallet.walletId.stringValue}",
name = stringReference(wallet.name),
)
add(walletTitle)
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
portfolio.accountsBalance.accountStatuses.forEach { accountStatus ->
val account = accountStatus.account
val accountBalance = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
}
val accountItemUM = AccountPortfolioItemUMConverter(
onClick = { selectorController.selectAccount(account.accountId) },
appCurrency = appCurrency,
accountBalance = accountBalance,
isBalanceHidden = isBalanceHidden,
).convert(account)
add(PortfolioSelectorItemUM.Portfolio(accountItemUM))
}
}
if (lockedWallets.isNotEmpty()) {
val lockedWalletsTitle = PortfolioSelectorItemUM.GroupTitle(
id = "lockedWalletsTitleId",
name = resourceReference(R.string.common_locked_wallets),
)
add(lockedWalletsTitle)
addAll(lockedWallets)
}
}
private fun loadBalanceWithArtwork():
Flow<Pair<PortfolioFetcher.Data, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
val wallets = Channel<Set<UserWallet>>()
val portfolioFlow = balanceFetcher.data
.onEach { wallets.trySend(it.balances.keys) }
val artworksFlow = wallets.receiveAsFlow()
.distinctUntilChanged()
.flatMapLatest { walletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
return combine(
flow = portfolioFlow,
flow2 = artworksFlow,
) { portfolioData, artworks -> portfolioData to artworks }
}
private fun emptyState() = PortfolioSelectorUM(
items = persistentListOf(),
title = TextReference.EMPTY,
)
}

View file

@ -1,28 +0,0 @@
package com.tangem.features.account.selector.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class AccountSelectorUM(
val items: ImmutableList<AccountSelectorItemUM>,
val isSingleWallet: Boolean,
)
@Immutable
sealed interface AccountSelectorItemUM {
val id: String
data class Wallet(
override val id: String,
val name: TextReference,
) : AccountSelectorItemUM
data class Account(
val account: TokenItemState,
val isBalanceHidden: Boolean,
) : AccountSelectorItemUM {
override val id: String = account.id
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.account.selector.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class PortfolioSelectorUM(
val title: TextReference,
val items: ImmutableList<PortfolioSelectorItemUM>,
)
@Immutable
sealed interface PortfolioSelectorItemUM {
val id: String
data class GroupTitle(
override val id: String,
val name: TextReference,
) : PortfolioSelectorItemUM
data class Portfolio(
val item: UserWalletItemUM,
) : PortfolioSelectorItemUM {
override val id: String = item.id.stringValue
}
}

View file

@ -1,146 +0,0 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.token.AccountItemPreviewData
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.account.selector.entity.AccountSelectorItemUM
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.features.account.selector.ui.AccountSelectorPreviewData.firstList
import kotlinx.collections.immutable.toImmutableList
import java.util.UUID
@Composable
internal fun AccountSelectorContent(
state: AccountSelectorUM,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
LazyColumn(
modifier = modifier,
contentPadding = contentPadding,
) {
val items = state.items
itemsIndexed(
items = items,
key = { _, item -> item.id },
) { index, item ->
val previewItem = items.getOrNull(index.dec())
val offsetModifier = when {
previewItem == null -> Modifier
state.isSingleWallet -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
item is AccountSelectorItemUM.Wallet -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
else -> Modifier.padding(
top = TangemTheme.dimens.spacing8,
)
}
when (item) {
is AccountSelectorItemUM.Account -> TokenItem(
state = item.account,
isBalanceHidden = item.isBalanceHidden,
modifier = offsetModifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.primary),
)
is AccountSelectorItemUM.Wallet -> WalletNameRow(
model = item,
modifier = offsetModifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
}
}
@Composable
private fun WalletNameRow(model: AccountSelectorItemUM.Wallet, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = model.name.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AccountSelectorContentPreview(
@PreviewParameter(AccountSelectorPreviewStateProvider::class) params: AccountSelectorUM,
) {
TangemThemePreview {
AccountSelectorContent(
state = params,
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
internal object AccountSelectorPreviewData {
val firstList
get() = buildList {
AccountSelectorItemUM.Wallet(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem 2.0"),
).let(::add)
AccountItemPreviewData.accountItem
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon)
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
AccountSelectorItemUM.Wallet(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon)
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
}
}
internal class AccountSelectorPreviewStateProvider : CollectionPreviewParameterProvider<AccountSelectorUM>(
buildList {
val secondList = listOf(
AccountItemPreviewData.accountItem,
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon),
).map { AccountSelectorItemUM.Account(it, false) }
val first = AccountSelectorUM(
items = firstList.toImmutableList(),
isSingleWallet = false,
)
val second = AccountSelectorUM(
items = secondList.toImmutableList(),
isSingleWallet = true,
)
add(first)
add(second)
},
)

View file

@ -13,14 +13,13 @@ 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.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
@Composable
internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
internal fun PortfolioSelectorBS(state: PortfolioSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
@ -32,13 +31,13 @@ internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit,
containerColor = TangemTheme.colors.background.secondary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_choose_account),
title = state.title,
startIconRes = R.drawable.ic_back_24,
onStartClick = onDismiss,
)
},
content = {
AccountSelectorContent(
PortfolioSelectorContent(
state = state,
contentPadding = PaddingValues(bottom = 16.dp),
modifier = modifier.padding(horizontal = 16.dp),
@ -50,9 +49,9 @@ internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit,
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(AccountSelectorPreviewStateProvider::class) params: AccountSelectorUM) {
private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::class) params: PortfolioSelectorUM) {
TangemThemePreview {
AccountSelectorBS(
PortfolioSelectorBS(
state = params,
onDismiss = {},
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),

View file

@ -0,0 +1,219 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
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.account.impl.R
import com.tangem.features.account.selector.entity.PortfolioSelectorItemUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.firstList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.lockedWalletList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.secondList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.walletList
import kotlinx.collections.immutable.toImmutableList
import java.util.UUID
private const val DISABLED_WALLET_ALPHA = 0.5f
@Composable
internal fun PortfolioSelectorContent(
state: PortfolioSelectorUM,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
LazyColumn(
modifier = modifier,
contentPadding = contentPadding,
) {
val items = state.items
itemsIndexed(
items = items,
key = { _, item -> item.id },
) { index, item ->
val previewItem = items.getOrNull(index.dec())
val offsetModifier = when {
previewItem == null -> Modifier
item is PortfolioSelectorItemUM.GroupTitle -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
else -> Modifier.padding(
top = TangemTheme.dimens.spacing8,
)
}
when (item) {
is PortfolioSelectorItemUM.Portfolio -> UserWalletItem(
state = item.item,
modifier = offsetModifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.primary)
.let { if (!item.item.isEnabled) it.alpha(DISABLED_WALLET_ALPHA) else it },
)
is PortfolioSelectorItemUM.GroupTitle -> WalletNameRow(
model = item,
modifier = offsetModifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
}
}
@Composable
private fun WalletNameRow(model: PortfolioSelectorItemUM.GroupTitle, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = model.name.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::class) params: PortfolioSelectorUM) {
TangemThemePreview {
PortfolioSelectorContent(
state = params,
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
internal object PortfolioSelectorPreviewData {
val accountName get() = stringReference(value = "Portfolio")
val walletName get() = stringReference(value = "Tangem 2.0")
private val accountItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
name = accountName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
isEnabled = true,
onClick = { },
imageState = ImageState.Account(
name = accountName,
icon = AccountIconPreviewData.randomAccountIcon(),
),
label = null,
)
private val walletItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
name = walletName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
isEnabled = true,
onClick = { },
imageState = ImageState.MobileWallet,
label = null,
)
private val lockedWalletItem: UserWalletItemUM
get() = walletItem.copy(isEnabled = false)
val firstList
get() = buildList {
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem 2.0"),
).let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
accountItem.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
}
val secondList
get() = firstList + buildList {
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
).let(::add)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
}
val walletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem))
}
val lockedWalletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
val title = PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
)
add(title)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
}
}
internal class PortfolioSelectorPreviewStateProvider : CollectionPreviewParameterProvider<PortfolioSelectorUM>(
buildList {
val first = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_account),
items = firstList.toImmutableList(),
)
val second = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_account),
items = secondList.toImmutableList(),
)
val walletListUM = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_wallet),
items = walletList.toImmutableList(),
)
val lockedWalletListUM =
PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_wallet),
items = lockedWalletList.toImmutableList(),
)
add(first)
add(second)
add(walletListUM)
add(lockedWalletListUM)
},
)

View file

@ -7,7 +7,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
interface AskBiometryComponent : ComposableContentComponent, ComposableBottomSheetComponent {
data class Params(
val bottomSheetVariant: Boolean,
val isBottomSheetVariant: Boolean,
val modelCallbacks: ModelCallbacks,
)

View file

@ -13,7 +13,7 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.SetAskBiometryShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
@ -55,7 +55,7 @@ internal class AskBiometryModel @Inject constructor(
private val _uiState = MutableStateFlow(
AskBiometryUM(
bottomSheetVariant = params.bottomSheetVariant,
bottomSheetVariant = params.isBottomSheetVariant,
onAllowClick = ::onAllowClick,
onDontAllowClick = ::dontAllow,
onDismiss = ::dismiss,
@ -160,7 +160,7 @@ internal class AskBiometryModel @Inject constructor(
title = resourceReference(R.string.common_cancel),
onClick = {},
),
dismissOnFirstAction = true,
shouldDismissOnFirstAction = true,
),
)
}

View file

@ -23,8 +23,8 @@ import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.analytics.Shop
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
@ -61,14 +61,14 @@ internal class CreateWalletSelectionModel @Inject constructor(
) : Model() {
internal val uiState: StateFlow<CreateWalletSelectionUM>
field = MutableStateFlow(
CreateWalletSelectionUM(
onBackClick = { router.pop() },
onMobileWalletClick = ::onMobileWalletClick,
onHardwareWalletClick = ::onHardwareWalletClick,
onScanClick = ::onScanClick,
),
)
field = MutableStateFlow(
CreateWalletSelectionUM(
onBackClick = { router.pop() },
onMobileWalletClick = ::onMobileWalletClick,
onHardwareWalletClick = ::onHardwareWalletClick,
onScanClick = ::onScanClick,
),
)
init {
showAlreadyHaveWalletWithDelay()

View file

@ -140,11 +140,11 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
@Composable
private fun WalletBlock(
modifier: Modifier = Modifier,
title: String,
description: String,
badge: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
badge: @Composable () -> Unit,
) {
Column(
modifier = modifier

View file

@ -18,11 +18,11 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
import com.tangem.features.details.impl.R

View file

@ -31,6 +31,7 @@ dependencies {
implementation(projects.core.navigation)
implementation(projects.core.decompose)
implementation(projects.common.routing)
implementation(projects.common.ui)
/** Domain modules */
implementation(projects.domain.models)

View file

@ -1,6 +1,7 @@
package com.tangem.features.disclaimer.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.notifications.NotificationId
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -49,6 +50,10 @@ internal class DisclaimerModel @Inject constructor(
cardRepository.acceptTangemTOS()
val shouldAskPushPermission = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
if (shouldAskPushPermission) {
notificationsRepository.setShouldShowNotifications(
key = NotificationId.EnablePushesReminderNotification.key,
value = false,
)
router.push(AppRoute.PushNotification(AppRoute.PushNotification.Source.Stories))
} else {
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)

View file

@ -26,8 +26,8 @@ import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.analytics.Shop
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.repositories.SettingsRepository
@ -46,14 +46,7 @@ import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale

View file

@ -95,11 +95,11 @@ fun StoriesTextAnimation(
@Composable
fun StoriesBottomImageAnimation(
firstStepDuration: Int,
totalDuration: Int,
initialScale: Float = 2.5f,
secondStageScale: Float = SCALE_SWITCH_BARRIER,
targetScale: Float = 1.0f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit,
) {
val secondStepDuration = totalDuration - firstStepDuration

View file

@ -25,10 +25,7 @@ internal class AccessCodeComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
if (!state.isConfirmMode) {
DisableScreenshotsDisposableEffect()
}
DisableScreenshotsDisposableEffect()
AccessCode(
modifier = modifier,
state = state,

View file

@ -6,10 +6,11 @@ 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.ui.UiMessageSender
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.CanUseBiometryUseCase
@ -56,10 +57,11 @@ internal class AccessCodeModel @Inject constructor(
private val params = paramsContainer.require<AccessCodeComponent.Params>()
internal val uiState: StateFlow<AccessCodeUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
private fun getInitialState() = AccessCodeUM(
accessCode = "",
accessCodeColor = PinTextColor.Primary,
onAccessCodeChange = ::onAccessCodeChange,
isConfirmMode = params.accessCodeToConfirm != null,
buttonEnabled = false,
@ -76,6 +78,12 @@ internal class AccessCodeModel @Inject constructor(
} else {
value.length == uiState.value.accessCodeLength
},
accessCodeColor = when {
params.accessCodeToConfirm == null -> PinTextColor.Primary
value.length != uiState.value.accessCodeLength -> PinTextColor.Primary
value == params.accessCodeToConfirm -> PinTextColor.Success
else -> PinTextColor.WrongCode
},
)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.hotwallet.accesscode.entity
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
internal data class AccessCodeUM(
val accessCode: String,
val accessCodeColor: PinTextColor,
val onAccessCodeChange: (String) -> Unit,
val isConfirmMode: Boolean,
val buttonEnabled: Boolean,

View file

@ -76,7 +76,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
length = state.accessCodeLength,
isPasswordVisual = state.isConfirmMode,
value = state.accessCode,
pinTextColor = PinTextColor.Primary,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
}
@ -109,6 +109,7 @@ private fun PreviewSet() {
AccessCode(
state = AccessCodeUM(
accessCode = "",
accessCodeColor = PinTextColor.Primary,
onAccessCodeChange = {},
isConfirmMode = false,
buttonEnabled = false,
@ -127,6 +128,7 @@ private fun PreviewConfirm() {
AccessCode(
state = AccessCodeUM(
accessCode = "123456",
accessCodeColor = PinTextColor.Success,
onAccessCodeChange = {},
isConfirmMode = true,
buttonEnabled = true,

View file

@ -5,7 +5,7 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.FullScreen
import com.tangem.core.ui.components.DialogFullScreen
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.ui.HotAccessCodeRequestFullScreenContent
@ -47,7 +47,7 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor(
var isShownIfProxy by remember { mutableStateOf(state.isShown) }
if (isShownIfProxy) {
FullScreen(focusable = true, onBackClick = state.onDismiss) {
DialogFullScreen(onDismissRequest = state.onDismiss) {
HotAccessCodeRequestFullScreenContent(
state = state.copy(isShown = isShownProxy),
modifier = modifier,

View file

@ -6,10 +6,11 @@ import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
@ -43,7 +44,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
)
val uiState: StateFlow<HotAccessCodeRequestUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) {
if (userWalletExists(attemptRequest.hotWalletId).not()) {
@ -82,6 +83,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
it.copy(
accessCodeColor = PinTextColor.WrongCode,
onAccessCodeChange = {},
useBiometricVisible = currentRequest.hasBiometry,
)
}
delay(timeMillis = 500) // Delay to show the wrong access code state
@ -121,7 +123,10 @@ internal class HotAccessCodeRequestModel @Inject constructor(
if (accessCode.length == ACCESS_CODE_LENGTH) {
uiState.update {
it.copy(onAccessCodeChange = {})
it.copy(
onAccessCodeChange = {},
useBiometricVisible = false,
)
}
result.value = HotWalletPasswordRequester.Result.EnteredPassword(HotAuth.Password(accessCode.toCharArray()))
@ -143,7 +148,24 @@ internal class HotAccessCodeRequestModel @Inject constructor(
suspend fun collectAttempts(attempts: Attempts) {
when (attempts) {
is Attempts.FastForward -> {
/** ignore */
if (attempts.count > 0) {
uiState.update {
it.copy(
wrongAccessCodeText = resourceReference(
R.string.access_code_check_warining_lock,
wrappedList(MAX_FAST_FORWARD_ATTEMPTS - attempts.count),
),
onAccessCodeChange = ::onAccessCodeChange,
)
}
} else {
uiState.update {
it.copy(
wrongAccessCodeText = null,
onAccessCodeChange = ::onAccessCodeChange,
)
}
}
}
is Attempts.WithDelay -> {
uiState.update {

View file

@ -37,105 +37,110 @@ import com.tangem.features.hotwallet.impl.R
@Suppress("MagicNumber", "LongMethod")
@Composable
internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM, modifier: Modifier = Modifier) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),
) {
Column(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
horizontalAlignment = Alignment.CenterHorizontally,
Box(Modifier.fillMaxSize()) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),
) {
TangemTopAppBar(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onDismiss),
)
SpacerH(68.dp)
Column(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 24.dp),
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
text = stringResourceSafe(R.string.access_code_check_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onDismiss),
)
SpacerH24()
SpacerH(68.dp)
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
Column(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
text = stringResourceSafe(R.string.access_code_check_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
SpacerH(20.dp)
SpacerH24()
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
SpacerH(20.dp)
AnimatedVisibility(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
visible = state.wrongAccessCodeText != null,
enter = fadeIn(),
exit = fadeOut(),
) {
val wrongAccessCodeText =
state.wrongAccessCodeText ?: return@AnimatedVisibility
Text(
text = wrongAccessCodeText.resolveReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2.copy(
lineBreak = LineBreak.Heading,
),
color = TangemTheme.colors.text.warning,
)
}
}
AnimatedVisibility(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
visible = state.wrongAccessCodeText != null,
visible = state.useBiometricVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
val wrongAccessCodeText =
state.wrongAccessCodeText ?: return@AnimatedVisibility
Text(
text = wrongAccessCodeText.resolveReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2.copy(
lineBreak = LineBreak.Heading,
SecondaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.navigationBarsPadding()
.imePadding(),
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResourceSafe(R.string.common_biometrics),
),
color = TangemTheme.colors.text.warning,
onClick = state.useBiometricClick,
)
}
}
if (state.useBiometricVisible) {
SecondaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.navigationBarsPadding()
.imePadding(),
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResourceSafe(R.string.common_biometrics),
),
onClick = state.useBiometricClick,
)
}
}
}

View file

@ -15,7 +15,6 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
@ -36,7 +35,6 @@ internal class AddExistingWalletModel @Inject constructor(
) : Model() {
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks()
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
@ -44,13 +42,12 @@ internal class AddExistingWalletModel @Inject constructor(
val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks()
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
val startRoute = AddExistingWalletRoute.Start
val startRoute = AddExistingWalletRoute.Import
val currentRoute: MutableStateFlow<AddExistingWalletRoute> = MutableStateFlow(startRoute)
fun onChildBack() {
when (currentRoute.value) {
is AddExistingWalletRoute.Start -> router.pop()
is AddExistingWalletRoute.Import -> stackNavigation.pop()
is AddExistingWalletRoute.Import -> router.pop()
is AddExistingWalletRoute.BackupCompleted -> Unit
is AddExistingWalletRoute.SetAccessCode -> Unit
is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop()
@ -87,7 +84,7 @@ internal class AddExistingWalletModel @Inject constructor(
title = resourceReference(R.string.access_code_alert_skip_ok),
onClick = { navigateToPushNotificationsOrNext() },
),
dismissOnFirstAction = true,
shouldDismissOnFirstAction = true,
),
)
}
@ -102,16 +99,6 @@ internal class AddExistingWalletModel @Inject constructor(
}
}
inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks {
override fun onBackClick() {
router.pop()
}
override fun onImportPhraseClick() {
stackNavigation.push(AddExistingWalletRoute.Import)
}
}
inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks {
override fun onWalletImported(userWalletId: UserWalletId) {
stackNavigation.replaceAll(AddExistingWalletRoute.BackupCompleted(userWalletId))

View file

@ -9,8 +9,6 @@ internal class AddExistingWalletStepperStateManager {
fun getStepperState(route: AddExistingWalletRoute): HotWalletStepperComponent.StepperUM? {
return when (route) {
is AddExistingWalletRoute.Start -> null
is AddExistingWalletRoute.Import -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_IMPORT,
steps = STEPS_COUNT,

View file

@ -5,7 +5,6 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
@ -24,12 +23,6 @@ internal class AddExistingWalletChildFactory @Inject constructor(
model: AddExistingWalletModel,
): ComposableContentComponent {
return when (route) {
is AddExistingWalletRoute.Start -> AddExistingWalletStartComponent(
context = childContext,
params = AddExistingWalletStartComponent.Params(
callbacks = model.addExistingWalletStartModelCallbacks,
),
)
is AddExistingWalletRoute.Import -> AddExistingWalletImportComponent(
context = childContext,
params = AddExistingWalletImportComponent.Params(

View file

@ -6,9 +6,6 @@ import kotlinx.serialization.Serializable
internal sealed class AddExistingWalletRoute : Route {
@Serializable
object Start : AddExistingWalletRoute()
@Serializable
object Import : AddExistingWalletRoute()

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent
@ -22,6 +23,7 @@ internal class AddExistingWalletImportComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
DisableScreenshotsDisposableEffect()
AddExistingWalletImportContent(
state = state,
modifier = modifier,

View file

@ -6,16 +6,12 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.MnemonicRepository
@ -80,7 +76,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
}
internal val uiState: StateFlow<AddExistingWalletImportUM>
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
@Suppress("UnusedPrivateMember")
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {

View file

@ -67,13 +67,17 @@ internal class ImportSeedPhraseUiStateBuilder(
val text = st.words.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val newWords = wordsFromText.dropLast(1) + word
val newWordsText = newWords.joinToString(" ")
st.copy(
words = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
),
val newWordsText = newWords.joinToString(" ").plus(" ")
val newWordsState = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
)
st.copy(
words = newWordsState,
).also {
launchInterceptWords(wordsField = newWordsState)
suggestNextWord(newWordsState)
}
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.addexistingwallet.start.ui.AddExistingWalletStartContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class AddExistingWalletStartComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: AddExistingWalletStartModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AddExistingWalletStartContent(
state = state,
modifier = modifier,
)
}
interface ModelCallbacks {
fun onBackClick()
fun onImportPhraseClick()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -1,206 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic.SignedIn
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
import com.tangem.core.decompose.di.GlobalUiMessageSender
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.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.analytics.Shop
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@Suppress("LongParameterList")
@ModelScoped
internal class AddExistingWalletStartModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val saveWalletUseCase: SaveWalletUseCase,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val scanCardProcessor: ScanCardProcessor,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val urlOpener: UrlOpener,
private val userWalletsListRepository: UserWalletsListRepository,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
private val params: AddExistingWalletStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<AddExistingWalletStartUM>
field = MutableStateFlow(
AddExistingWalletStartUM(
showWantToPurchaseBlock = false,
isScanInProgress = false,
onBackClick = params.callbacks::onBackClick,
onImportPhraseClick = params.callbacks::onImportPhraseClick,
onScanCardClick = ::onScanClick,
onBuyCardClick = ::onShopClick,
),
)
init {
showWantToPurchaseBlockWithDelay()
}
private fun showWantToPurchaseBlockWithDelay() {
modelScope.launch {
delay(SHOW_WANT_TO_PURCHASE_BLOCK_DELAY)
uiState.update { it.copy(showWantToPurchaseBlock = true) }
}
}
private fun onShopClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
analyticsEventHandler.send(Shop.ScreenOpened)
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
scanCard()
}
private fun scanCard() {
modelScope.launch {
setLoading(true)
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
val analyticsSource = AnalyticsParam.ScreensSources.Intro
scanCardProcessor.scan(
analyticsSource = analyticsSource,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
} else {
setLoading(true)
}
},
onFailure = { error ->
handleScanError(error)
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
},
)
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
setLoading(false)
return
}
saveWalletUseCase(userWallet).fold(
ifLeft = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (it) {
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
is SaveWalletError.WalletAlreadySaved -> {
userWalletsListRepository.unlock(
userWalletId = userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
).onRight {
appRouter.replaceAll(AppRoute.Wallet)
}
}
}
},
ifRight = {
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse)
appRouter.replaceAll(AppRoute.Wallet)
},
)
}
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private fun setLoading(isLoading: Boolean) {
uiState.update { it.copy(isScanInProgress = isLoading) }
}
fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
}
companion object {
private const val SHOW_WANT_TO_PURCHASE_BLOCK_DELAY = 3000L
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface AddExistingWalletStartModule {
@Binds
@IntoMap
@ClassKey(AddExistingWalletStartModel::class)
fun bindAddExistingWalletStartModel(model: AddExistingWalletStartModel): Model
}

View file

@ -1,10 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start.entity
internal data class AddExistingWalletStartUM(
val showWantToPurchaseBlock: Boolean,
val isScanInProgress: Boolean,
val onBackClick: () -> Unit,
val onImportPhraseClick: () -> Unit,
val onScanCardClick: () -> Unit,
val onBuyCardClick: () -> Unit,
)

View file

@ -1,191 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM
import com.tangem.features.hotwallet.common.ui.OptionBlock
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
TangemTopAppBar(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onBackClick),
title = "Add existing wallet",
)
Column(
modifier = Modifier
.weight(1f)
.padding(
start = 16.dp,
top = 24.dp,
end = 16.dp,
),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = stringResourceSafe(R.string.wallet_import_seed_navtitle),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
OptionBlock(
modifier = Modifier
.padding(top = 32.dp),
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_seed_title),
description = stringResourceSafe(R.string.wallet_import_seed_description),
badge = null,
onClick = state.onImportPhraseClick,
enabled = true,
)
OptionBlock(
modifier = Modifier
.padding(top = 8.dp),
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_scan_title),
description = stringResourceSafe(R.string.wallet_import_scan_description),
badge = {
if (state.isScanInProgress) {
CircularProgressIndicator(
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
.padding(2.dp),
color = TangemTheme.colors.text.primary1,
strokeWidth = TangemTheme.dimens.size2,
)
} else {
Icon(
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp),
painter = painterResource(R.drawable.ic_tangem_24),
contentDescription = null,
tint = TangemTheme.colors.icon.secondary,
)
}
},
onClick = state.onScanCardClick,
enabled = true,
)
OptionBlock(
modifier = Modifier
.padding(top = 8.dp),
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_google_drive_title),
description = stringResourceSafe(R.string.wallet_import_google_drive_description),
badge = {
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.background(
color = TangemTheme.colors.field.focused,
shape = TangemTheme.shapes.roundedCorners8,
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = stringResourceSafe(R.string.common_coming_soon),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
}
},
onClick = null,
enabled = false,
)
}
AnimatedVisibility(state.showWantToPurchaseBlock) {
BuyTangemWalletBlock(
onScanClick = state.onBuyCardClick,
)
}
}
}
@Composable
private fun BuyTangemWalletBlock(onScanClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.background(
color = TangemTheme.colors.field.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = 20.dp,
vertical = 16.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.weight(1f)
.padding(end = 16.dp),
text = stringResourceSafe(R.string.wallet_import_buy_question),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
PrimaryButton(
modifier = Modifier
.wrapContentWidth(),
text = stringResourceSafe(R.string.wallet_import_buy_title),
onClick = onScanClick,
size = TangemButtonSize.RoundedAction,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = true,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewCreateWalletContent() {
TangemThemePreview {
AddExistingWalletStartContent(
state = AddExistingWalletStartUM(
showWantToPurchaseBlock = true,
isScanInProgress = true,
onBackClick = {},
onImportPhraseClick = {},
onScanCardClick = {},
onBuyCardClick = {},
),
)
}
}

View file

@ -28,13 +28,18 @@ internal class CreateMobileWalletModel @Inject constructor(
) : Model() {
internal val uiState: StateFlow<CreateMobileWalletUM>
field = MutableStateFlow(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
field = MutableStateFlow(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onImportClick = ::onImportClick,
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
private fun onImportClick() {
router.push(AppRoute.AddExistingWallet)
}
private fun onCreateClick() {
modelScope.launch {

View file

@ -3,5 +3,6 @@ package com.tangem.features.hotwallet.createmobilewallet.entity
internal data class CreateMobileWalletUM(
val createButtonLoading: Boolean,
val onBackClick: () -> Unit,
val onImportClick: () -> Unit,
val onCreateClick: () -> Unit,
)

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
@ -80,13 +81,23 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
iconRes = R.drawable.ic_settings_24,
)
}
SecondaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
top = 16.dp,
end = 16.dp,
),
text = stringResourceSafe(R.string.hw_import_existing_wallet),
onClick = state.onImportClick,
)
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = stringResourceSafe(R.string.common_create),
text = stringResourceSafe(R.string.onboarding_create_wallet_button_create_wallet),
showProgress = state.createButtonLoading,
enabled = true,
onClick = state.onCreateClick,
)
}
@ -134,6 +145,7 @@ private fun PreviewCreateWalletContent() {
state = CreateMobileWalletUM(
onBackClick = {},
createButtonLoading = false,
onImportClick = {},
onCreateClick = {},
),
)

View file

@ -14,7 +14,6 @@ import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckCompone
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@ -28,7 +27,6 @@ internal class CreateWalletBackupModel @Inject constructor(
val params = paramsContainer.require<CreateWalletBackupComponent.Params>()
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks()
val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks()
val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks()
@ -63,14 +61,6 @@ internal class CreateWalletBackupModel @Inject constructor(
router.pop()
}
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
override fun onBackClick() {
onBack()
}
override fun onSkipClick() = Unit
}
inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks {
override fun onContinueClick() {
onManualBackupStarted()

View file

@ -1,56 +0,0 @@
package com.tangem.features.hotwallet.createwalletbackup
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import javax.inject.Inject
internal class CreateWalletBackupStepperStateManager @Inject constructor() {
fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? {
return when (route) {
is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_START,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_PHRASE,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_CONFIRM,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_COMPLETED,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_done),
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,
)
}
}
companion object {
private const val STEPS_COUNT = 4
private const val STEP_START = 1
private const val STEP_PHRASE = 2
private const val STEP_CONFIRM = 3
private const val STEP_COMPLETED = 4
}
}

View file

@ -13,9 +13,8 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -24,9 +23,7 @@ import kotlinx.coroutines.launch
internal class DefaultCreateWalletBackupComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: CreateWalletBackupComponent.Params,
private val stepperStateManager: CreateWalletBackupStepperStateManager,
createWalletBackupChildFactory: CreateWalletBackupChildFactory,
stepperComponentFactory: DefaultHotWalletStepperComponent.Factory,
) : CreateWalletBackupComponent, AppComponentContext by appComponentContext {
private val model: CreateWalletBackupModel = getOrCreateModel(params)
@ -46,14 +43,6 @@ internal class DefaultCreateWalletBackupComponent @AssistedInject constructor(
},
)
private val stepperComponent = stepperComponentFactory.create(
context = this,
params = HotWalletStepperComponent.Params(
initState = HotWalletStepperComponent.StepperUM.initialState(),
callback = model.hotWalletStepperComponentModelCallback,
),
)
init {
innerStack.subscribe(
lifecycle = lifecycle,
@ -72,13 +61,11 @@ internal class DefaultCreateWalletBackupComponent @AssistedInject constructor(
BackHandler(onBack = model::onBack)
val stepperState = stepperStateManager.getStepperState(currentRoute)
stepperState?.let { stepperComponent.updateState(it) }
CreateWalletBackupContent(
stackState = stackState,
stepperComponent = stepperComponent.takeIf { stepperState != null },
modifier = modifier,
showTopBar = currentRoute !is CreateWalletBackupRoute.BackupCompleted,
onBackClick = model::onBack,
)
}

View file

@ -3,11 +3,9 @@ package com.tangem.features.hotwallet.createwalletbackup.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager
import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
@ -16,7 +14,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface CreateWalletBackupModuleBinds {
internal interface CreateWalletBackupModule {
@Binds
@Singleton
@ -28,15 +26,4 @@ internal interface CreateWalletBackupModuleBinds {
@IntoMap
@ClassKey(CreateWalletBackupModel::class)
fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model
}
@Module
@InstallIn(SingletonComponent::class)
internal object CreateWalletBackupModule {
@Provides
@Singleton
fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager {
return CreateWalletBackupStepperStateManager()
}
}

View file

@ -11,15 +11,19 @@ import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.features.hotwallet.impl.R
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
@Composable
internal fun CreateWalletBackupContent(
stackState: ChildStack<CreateWalletBackupRoute, ComposableContentComponent>,
stepperComponent: HotWalletStepperComponent?,
showTopBar: Boolean,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -29,7 +33,15 @@ internal fun CreateWalletBackupContent(
.imePadding()
.systemBarsPadding(),
) {
stepperComponent?.Content(Modifier)
if (showTopBar) {
TangemTopAppBar(
modifier = Modifier,
startButton = TopAppBarButtonUM.Back(
onBackClicked = onBackClick,
),
title = stringResourceSafe(id = R.string.common_backup),
)
}
Children(
stack = stackState,

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel
import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent
@ -22,6 +23,7 @@ internal class ManualBackupCheckComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
DisableScreenshotsDisposableEffect()
ManualBackupCheckContent(
state = state,
modifier = modifier,

View file

@ -44,7 +44,7 @@ internal class ManualBackupCheckModel @Inject constructor(
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupCheckUM>
field = MutableStateFlow(getInitialUIState())
field = MutableStateFlow(getInitialUIState())
init {
modelScope.launch {

View file

@ -18,9 +18,9 @@ internal class ManualBackupCompletedModel @Inject constructor(
private val params: ManualBackupCompletedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<ManualBackupCompletedUM>
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
),
)
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
),
)
}

View file

@ -35,11 +35,11 @@ internal class ManualBackupPhraseModel @Inject constructor(
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupPhraseUM>
field = MutableStateFlow(
ManualBackupPhraseUM(
onContinueClick = callbacks::onContinueClick,
),
)
field = MutableStateFlow(
ManualBackupPhraseUM(
onContinueClick = callbacks::onContinueClick,
),
)
init {
modelScope.launch {

View file

@ -18,9 +18,9 @@ internal class ManualBackupStartModel @Inject constructor(
private val params: ManualBackupStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<ManualBackupStartUM>
field = MutableStateFlow(
ManualBackupStartUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
field = MutableStateFlow(
ManualBackupStartUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -18,9 +18,9 @@ internal class MobileWalletSetupFinishedModel @Inject constructor(
private val params: MobileWalletSetupFinishedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<MobileWalletSetupFinishedUM>
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -20,7 +20,7 @@ internal class HotWalletStepperModel @Inject constructor(
val params = paramsContainer.require<HotWalletStepperComponent.Params>()
val uiState: StateFlow<HotWalletStepperComponent.StepperUM>
field = MutableStateFlow(params.initState)
field = MutableStateFlow(params.initState)
fun updateState(newState: HotWalletStepperComponent.StepperUM) {
uiState.value = newState

View file

@ -35,11 +35,11 @@ internal class ViewPhraseModel @Inject constructor(
private val params = paramsContainer.require<ViewPhraseComponent.Params>()
internal val uiState: StateFlow<ViewPhraseUM>
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
init {
loadSeedPhrase()

View file

@ -63,7 +63,7 @@ internal class WalletActivationModel @Inject constructor(
is WalletActivationRoute.ManualBackupCheck -> stackNavigation.pop()
is WalletActivationRoute.ManualBackupCompleted -> Unit
is WalletActivationRoute.SetAccessCode -> Unit
is WalletActivationRoute.ConfirmAccessCode -> Unit
is WalletActivationRoute.ConfirmAccessCode -> stackNavigation.pop()
is WalletActivationRoute.PushNotifications -> Unit
is WalletActivationRoute.SetupFinished -> Unit
}
@ -97,7 +97,7 @@ internal class WalletActivationModel @Inject constructor(
title = resourceReference(R.string.access_code_alert_skip_ok),
onClick = { navigateToPushNotificationsOrNext() },
),
dismissOnFirstAction = true,
shouldDismissOnFirstAction = true,
),
)
}

View file

@ -24,7 +24,7 @@ import javax.inject.Inject
@ModelScoped
internal class WalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
private val getWalletUseCase: GetUserWalletUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
@ -33,35 +33,37 @@ internal class WalletBackupModel @Inject constructor(
private val params: WalletBackupComponent.Params = paramsContainer.require()
val uiState: StateFlow<WalletBackupUM>
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
backedUp = false,
),
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
backedUp = false,
),
)
)
init {
getWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = {
updateBackupStatuses(it)
},
)
getUserWalletUseCase.invokeFlow(params.userWalletId)
.onEach { either ->
either.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = {
updateBackupStatuses(it)
},
)
}.launchIn(modelScope)
}
private fun updateBackupStatuses(userWallet: UserWallet) {
@ -95,7 +97,7 @@ internal class WalletBackupModel @Inject constructor(
private fun onRecoveryPhraseClick() {
if (uiState.value.backedUp) {
getWalletUseCase.invoke(params.userWalletId)
getUserWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")

View file

@ -1,12 +1,11 @@
package com.tangem.features.kyc
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface KycComponent {
interface KycComponent : ComposableContentComponent {
fun launch()
data object Params
interface Factory {
fun create(appComponentContext: AppComponentContext): KycComponent
}
interface Factory : ComponentFactory<Params, KycComponent>
}

View file

@ -1,45 +1,62 @@
package com.tangem.features.kyc
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.sumsub.sns.core.SNSMobileSDK
import com.sumsub.sns.core.data.listener.TokenExpirationHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.pay.KycStartInfo
import com.tangem.features.kyc.theme.TangemSNSIconHandler
import com.tangem.features.kyc.theme.TangemSNSTheme
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import java.util.Locale
class DefaultKycComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: KycComponent.Params,
) : KycComponent, AppComponentContext by appComponentContext {
private val model: DefaultKycModel = getOrCreateModel()
private val model: DefaultKycModel = getOrCreateModel(params)
override fun launch() {
init {
componentScope.launch {
model.uiState.collect {
it?.let { startInfo ->
val tokenExpirationHandler = object : TokenExpirationHandler {
override fun onTokenExpired() = ""
}
val snsSdk = SNSMobileSDK.Builder(activity)
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
.withTheme(TangemSNSTheme.theme(activity))
.withIconHandler(TangemSNSIconHandler())
.withLocale(Locale(startInfo.locale))
.build()
snsSdk.launch()
}
model.uiState.drop(1).collectLatest { startInfo ->
startInfo?.let { launchSdk(startInfo) }
router.pop()
}
}
model.getKycToken()
}
private fun launchSdk(startInfo: KycStartInfo) {
val tokenExpirationHandler = object : TokenExpirationHandler {
/**
* We don't refresh this token for f&f release. Assume it lives long enough to finish KYC
* [REDACTED_TODO_COMMENT]
*/
override fun onTokenExpired() = ""
}
val snsSdk = SNSMobileSDK.Builder(activity)
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
.withTheme(TangemSNSTheme.theme(activity))
.withIconHandler(TangemSNSIconHandler())
.withLocale(Locale(startInfo.locale))
.build()
snsSdk.launch()
}
@Composable
override fun Content(modifier: Modifier) {
KycLoadingScreen(router::pop, modifier)
}
@AssistedFactory
interface Factory : KycComponent.Factory {
override fun create(appComponentContext: AppComponentContext): DefaultKycComponent
override fun create(context: AppComponentContext, params: KycComponent.Params): DefaultKycComponent
}
}

View file

@ -21,7 +21,7 @@ class DefaultKycModel @Inject constructor(
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
val uiState = _uiState.asStateFlow()
fun getKycToken() {
init {
modelScope.launch {
kycRepository.getKycStartInfo().getOrNull()?.let { _uiState.emit(it) }
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.kyc
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.R
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun KycLoadingScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
topBar = {
AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(),
onBackClick = onBack,
iconRes = R.drawable.ic_back_24,
)
},
content = { paddingValues ->
Box(
modifier = modifier
.padding(paddingValues)
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier,
color = TangemTheme.colors.icon.primary1,
)
}
},
)
}

View file

@ -15,6 +15,8 @@ dependencies {
implementation(projects.features.kyc.api)
implementation(projects.core.decompose)
implementation(deps.compose.ui)
implementation(projects.core.ui)
/** DI */
implementation(deps.hilt.android)

View file

@ -1,5 +1,7 @@
package com.tangem.features.kyc
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -7,17 +9,19 @@ import dagger.assisted.AssistedInject
/**
* Mocking it for release/external builds to exclude SumSub dependency
* This will never be called if the FT [isTangemPayEnabled] is off
*/
@Suppress("UnusedPrivateProperty")
internal class MockKycComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: KycComponent.Params,
) : KycComponent, AppComponentContext by appComponentContext {
override fun launch() {
/* no op */
}
@Composable
override fun Content(modifier: Modifier) { /* no op */ }
@AssistedFactory
interface Factory : KycComponent.Factory {
override fun create(appComponentContext: AppComponentContext): MockKycComponent
override fun create(context: AppComponentContext, params: KycComponent.Params): MockKycComponent
}
}

View file

@ -12,7 +12,7 @@ interface ChooseManagedTokensComponent : ComposableContentComponent {
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val source: Source,
val showSendViaSwapNotification: Boolean,
val shouldShowSendViaSwapNotification: Boolean,
val callback: ModelCallback? = null,
val analyticsCategoryName: String,
)

View file

@ -8,7 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
sealed class CommonManageTokensAnalyticEvents(
category: String,
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = category, event = event, params = params) {
data class TokenSearchClicked(
@ -26,8 +26,8 @@ sealed class CommonManageTokensAnalyticEvents(
event = "Token Searched",
params = buildMap {
put(CHOSEN_TOKEN, if (isTokenChosen) "Yes" else "No")
token?.let { put(TOKEN_PARAM, token) }
blockchain?.let { put(BLOCKCHAIN, blockchain) }
if (token != null) { put(TOKEN_PARAM, token) }
if (blockchain != null) { put(BLOCKCHAIN, blockchain) }
},
)
}

View file

@ -81,7 +81,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<ChooseManageTokensBottomSheetConfig> = SlotNavigation()
val uiState: StateFlow<ChooseManagedTokenUM>
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
init {
manageTokensListManager.uiItems
@ -124,7 +124,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
}
private fun getNotification(): NotificationUM? {
return if (params.source == Source.SendViaSwap && params.showSendViaSwapNotification) {
return if (params.source == Source.SendViaSwap && params.shouldShowSendViaSwapNotification) {
ChooseManagedTokensNotificationUM.SendViaSwap(onCloseClick = ::removeNotification)
} else {
null

View file

@ -153,7 +153,7 @@ private fun LazyListScope.contentItems(items: ImmutableList<CurrencyItemUM>) {
name = name,
type = symbol,
icon = icon,
showCustom = false,
shouldShowCustom = false,
)
},
modifier = Modifier

View file

@ -128,7 +128,7 @@ internal class PreviewManageTokensComponent(
background = Color.Black,
topBadgeIconResId = R.drawable.img_eth_22,
isGrayscale = false,
showCustomBadge = true,
shouldShowCustomBadge = true,
),
onRemoveClick = {},
)
@ -141,7 +141,7 @@ internal class PreviewManageTokensComponent(
url = null,
fallbackResId = R.drawable.img_btc_22,
isGrayscale = false,
showCustomBadge = false,
shouldShowCustomBadge = false,
),
networks = if (index == 2) {
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index))

View file

@ -66,7 +66,7 @@ internal class PreviewOnboardingManageTokensComponent(
background = Color.Black,
topBadgeIconResId = R.drawable.img_eth_22,
isGrayscale = false,
showCustomBadge = true,
shouldShowCustomBadge = true,
),
onRemoveClick = {},
)
@ -79,7 +79,7 @@ internal class PreviewOnboardingManageTokensComponent(
url = null,
fallbackResId = R.drawable.img_btc_22,
isGrayscale = false,
showCustomBadge = false,
shouldShowCustomBadge = false,
),
networks = if (index == 2) {
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks())

View file

@ -163,9 +163,9 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier)
url = null,
fallbackResId = model.iconResId,
isGrayscale = false,
showCustomBadge = false,
shouldShowCustomBadge = false,
),
showCustom = false,
shouldShowCustom = false,
)
},
action = {

View file

@ -290,7 +290,7 @@ private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier =
name = name,
type = symbol,
icon = icon,
showCustom = true,
shouldShowCustom = true,
)
},
action = {
@ -316,7 +316,7 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, isEditable: Boolean, m
name = name,
type = symbol,
icon = icon,
showCustom = false,
shouldShowCustom = false,
)
},
action = {
@ -442,7 +442,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
showTangemIcon = true,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("wallet_id"),
userWalletId = UserWalletId("0x"),
),
),
PreviewManageTokensComponent(
@ -455,7 +455,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
showTangemIcon = false,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("wallet_id"),
userWalletId = UserWalletId("0x"),
),
),
)

View file

@ -27,7 +27,7 @@ internal fun CustomDerivationInputDialog(model: CustomDerivationInputUM, onDismi
fieldValue = value,
confirmButton = DialogButtonUM(
title = stringResourceSafe(id = R.string.common_ok),
enabled = model.isConfirmEnabled,
isEnabled = model.isConfirmEnabled,
onClick = model.onConfirm,
),
dismissButton = DialogButtonUM(

View file

@ -83,6 +83,11 @@ internal class ManageTokensListManager @AssistedInject constructor(
.distinctUntilChanged()
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
/**
* Launch pagination flow to get currencies
*
* @param isCollapsed set initial display state of networks. !!! WARNING !!! Use `false` flag with cation
*/
suspend fun launchPagination(isCollapsed: Boolean) = coroutineScope {
val loadUserTokensFromRemote = when (mode) {
is ManageTokensMode.Wallet -> source == ManageTokensSource.ONBOARDING

View file

@ -77,7 +77,7 @@ internal class ManageTokensWarningDelegate @AssistedInject constructor(
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.token_details_hide_alert_hide),
warning = true,
isWarning = true,
onClick = onConfirm,
)
},

View file

@ -32,7 +32,7 @@ private fun ManagedCryptoCurrency.Custom.toUiModel(
url = iconUrl,
fallbackResId = network.id.getIconRes(isColored = true),
isGrayscale = false,
showCustomBadge = true,
shouldShowCustomBadge = true,
)
}
is ManagedCryptoCurrency.Custom.Token -> {
@ -44,7 +44,7 @@ private fun ManagedCryptoCurrency.Custom.toUiModel(
fallbackTint = getTintForTokenIcon(background),
topBadgeIconResId = network.id.getIconRes(isColored = true),
isGrayscale = false,
showCustomBadge = true,
shouldShowCustomBadge = true,
)
}
},
@ -68,7 +68,7 @@ private fun ManagedCryptoCurrency.Token.toUiModel(
url = iconUrl,
topBadgeIconResId = null,
isGrayscale = if (isEditable) !isAdded else false,
showCustomBadge = false,
shouldShowCustomBadge = false,
fallbackTint = getTintForTokenIcon(background),
fallbackBackground = background,
),

View file

@ -19,7 +19,7 @@ interface MarketsTokenDetailsComponent : ComposableContentComponent {
data class Params(
val token: TokenMarketParams,
val appCurrency: AppCurrency,
val showPortfolio: Boolean,
val shouldShowPortfolio: Boolean,
val analyticsParams: AnalyticsParams?,
)

View file

@ -62,7 +62,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc
imageUrl = getTokenIconUrlFromDefaultHost(rawTokenId),
),
appCurrency = appCurrency,
showPortfolio = true,
shouldShowPortfolio = true,
analyticsParams = null,
),
)

View file

@ -46,7 +46,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.showPortfolio) {
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) {
portfolioComponentFactory.create(
context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(

View file

@ -27,7 +27,7 @@ internal object ExchangeItemStateConverter : Converter<TokenMarketExchange, Toke
url = value.imageUrl,
fallbackResId = R.drawable.ic_alert_24,
isGrayscale = false,
showCustomBadge = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)),
fiatAmountState = TokenItemState.FiatAmountState.Content(

View file

@ -58,8 +58,8 @@ internal fun MarketsTokenDetailsContent(
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
backButtonEnabled: Boolean,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
Content(
modifier = modifier,
@ -88,8 +88,8 @@ private fun Content(
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
backButtonEnabled: Boolean,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }

View file

@ -170,7 +170,7 @@ private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterPr
url = null,
fallbackResId = R.drawable.ic_facebook_24,
isGrayscale = false,
showCustomBadge = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"),

View file

@ -65,7 +65,7 @@ private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier =
subtitle = TextReference.Res(id = R.string.information_generated_with_ai),
iconResId = R.drawable.ic_magic_28,
onClick = onClick,
showArrowIcon = false,
shouldShowArrowIcon = false,
),
modifier = modifier,
subtitleColor = TangemTheme.colors.text.primary1,

View file

@ -79,7 +79,7 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
params = MarketsTokenDetailsComponent.Params(
token = token,
appCurrency = appCurrency,
showPortfolio = true,
shouldShowPortfolio = true,
analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = null,
source = "Market",

View file

@ -38,6 +38,7 @@ internal class PortfolioAnalyticsEvent(
TokenActionsBSContentUM.Action.Buy -> "Button - Buy"
TokenActionsBSContentUM.Action.Receive -> "Button - Receive"
TokenActionsBSContentUM.Action.Exchange -> "Button - Swap"
TokenActionsBSContentUM.Action.Stake -> "Button - Stake"
else -> "error"
},
params = buildMap {

View file

@ -1,8 +1,6 @@
package com.tangem.features.markets.portfolio.impl.loader
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
@ -15,7 +13,6 @@ import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
/**
@ -41,8 +38,8 @@ internal class PortfolioDataLoader @Inject constructor(
fun load(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
return combine(
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyFlow(),
flow3 = getBalanceHidingSettingsFlow(),
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { walletsWithCurrencies, appCurrency, isBalanceHidden ->
PortfolioData(
walletsWithCurrencies = walletsWithCurrencies,
@ -113,23 +110,6 @@ internal class PortfolioDataLoader @Inject constructor(
.distinctUntilChanged()
}
private fun getSelectedAppCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map {
it.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
}
.distinctUntilChanged()
}
private fun getBalanceHidingSettingsFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
private fun getWalletsWithTotalBalanceFlow(
ids: List<UserWalletId>,
): Flow<Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>> {

View file

@ -23,7 +23,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.utils.Provider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -42,7 +41,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender,
private val shareManager: ShareManager,
private val sendFeatureToggles: SendFeatureToggles,
) {
private val disabledActionsInDemoMode = buildSet {
@ -159,17 +157,10 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val route = if (sendFeatureToggles.isSendWithSwapEnabled) {
AppRoute.SendEntryPoint(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
)
} else {
AppRoute.Send(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
)
}
val route = AppRoute.SendEntryPoint(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
)
router.push(route)
}

View file

@ -106,7 +106,7 @@ private fun AddButton(state: AddButtonState, onClick: () -> Unit) {
text = resourceReference(R.string.markets_add_token),
icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24),
onClick = onClick,
enabled = state == AddButtonState.Available,
isEnabled = state == AddButtonState.Available,
),
)
}

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -100,9 +101,9 @@ private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider<Port
tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy(
fiatAmountState = contentFiatAmount.copy(
icons = persistentListOf(
TokenItemState.FiatAmountState.Content.IconUM(
TokenFiatAmountState.Content.IconUM(
iconRes = R.drawable.ic_staking_24,
useAccentColor = true,
tint = IconTint.Accent,
),
),
),

View file

@ -44,7 +44,7 @@ internal class TokenMarketBlockModel @Inject constructor(
private val params = paramsContainer.require<TokenMarketBlockComponent.Params>()
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false)
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false)
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
@ -152,7 +152,7 @@ internal class TokenMarketBlockModel @Inject constructor(
AppRoute.MarketsTokenDetails(
token = tokenParam,
appCurrency = currentAppCurrency.value,
showPortfolio = false,
shouldShowPortfolio = false,
analyticsParams = AppRoute.MarketsTokenDetails.AnalyticsParams(
blockchain = params.cryptoCurrency.network.name,
source = "Token",

Some files were not shown because too many files have changed in this diff Show more