Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-10 16:15:30 +03:00
commit 2f62d482ca
635 changed files with 18802 additions and 9118 deletions

View file

@ -5,9 +5,8 @@ import com.tangem.core.decompose.model.ParamsContainer
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.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase
@ -48,8 +47,11 @@ internal class ArchivedAccountListModel @Inject constructor(
)
messageSender.send(
DialogMessage(
title = stringReference(account.accountName.value),
message = TextReference.EMPTY,
title = resourceReference(R.string.account_archived_recover_dialog_title),
message = resourceReference(
id = R.string.account_archived_recover_dialog_description,
formatArgs = wrappedList(account.accountName.value),
),
firstActionBuilder = { firstAction },
secondActionBuilder = { secondAction },
),

View file

@ -1,8 +1,8 @@
package com.tangem.features.account.createedit
import com.tangem.common.ui.account.toDomain
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.common.ui.account.toDomain
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -170,10 +170,14 @@ internal class AccountCreateEditModel @Inject constructor(
it.updateDerivationIndex(derivationIndex = derivationIndex.value)
}
}
.onLeft {
.onLeft { cause ->
handleError(
error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex,
params = mapOf("userWalletId" to userWalletId.stringValue),
message = cause.toString(),
params = mapOf(
"userWalletId" to userWalletId.stringValue,
"cause" to cause.toString(),
),
)
return@launch
@ -181,8 +185,12 @@ internal class AccountCreateEditModel @Inject constructor(
}
}
private fun handleError(error: AccountFeatureError, params: Map<String, String> = mapOf()) {
val exception = IllegalStateException(error.toString())
private fun handleError(
error: AccountFeatureError,
message: String? = null,
params: Map<String, String> = mapOf(),
) {
val exception = IllegalStateException("$error. Cause: $message")
Timber.e(exception)

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import kotlinx.collections.immutable.ImmutableList
data class AccountCreateEditUM(
internal data class AccountCreateEditUM(
val title: TextReference,
val account: Account,
val colorsState: Colors,

View file

@ -2,7 +2,7 @@ package com.tangem.features.account.details.entity
import com.tangem.common.ui.account.CryptoPortfolioIconUM
data class AccountDetailsUM(
internal data class AccountDetailsUM(
val accountName: String,
val accountIcon: CryptoPortfolioIconUM,
val onCloseClick: () -> Unit,

View file

@ -15,7 +15,7 @@ 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.models.wallet.UserWallet
import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase
import com.tangem.domain.settings.SetAskBiometryShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
@ -39,7 +39,7 @@ import javax.inject.Inject
internal class AskBiometryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase,
private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase,
private val settingsRepository: SettingsRepository,
private val tangemSdkManager: TangemSdkManager,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
@ -66,7 +66,7 @@ internal class AskBiometryModel @Inject constructor(
init {
modelScope.launch {
setSaveWalletScreenShownUseCase()
setAskBiometryShownUseCase()
}
}
@ -111,7 +111,6 @@ internal class AskBiometryModel @Inject constructor(
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
walletsRepository.saveShouldSaveUserWallets(item = true)
settingsRepository.setShouldSaveAccessCodes(value = true)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
walletsRepository.setUseBiometricAuthentication(value = true)
@ -120,6 +119,7 @@ internal class AskBiometryModel @Inject constructor(
isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(),
)
} else {
settingsRepository.setShouldSaveAccessCodes(value = true)
if (userWallet is UserWallet.Cold) {
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,

View file

@ -23,11 +23,11 @@ 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.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM
@ -56,7 +56,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
private val saveWalletUseCase: SaveWalletUseCase,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
@ -146,7 +146,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
)
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
@ -154,7 +154,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = userWalletsListManager.walletsCount.toString(),
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)

View file

@ -149,7 +149,7 @@ private fun WalletBlock(
.padding(top = 8.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
color = TangemTheme.colors.background.secondary,
color = TangemTheme.colors.field.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.clickable(onClick = onClick)

View file

@ -11,16 +11,15 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.component.DetailsComponent
@ -56,7 +55,7 @@ internal class DetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val appStateHolder: ReduxStateHolder,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val feedbackFeatureToggles: FeedbackFeatureToggles,
@ -121,20 +120,15 @@ internal class DetailsModel @Inject constructor(
val selectedUserWallet = getSelectedWalletSyncUseCase().getOrNull()
?: error("Selected wallet is null")
if (selectedUserWallet is UserWallet.Hot) {
return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Send feedback
}
val scanResponse = selectedUserWallet.requireColdWallet().scanResponse
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
val metaInfo = getWalletMetaInfoUseCase(selectedUserWallet.walletId).getOrNull() ?: return@launch
val feedbackType = when {
userWallets.all { it is UserWallet.Cold && it.scanResponse.card.isVisa } ->
FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
FeedbackEmailType.Visa.DirectUserRequest(metaInfo)
userWallets.all { it !is UserWallet.Cold || it.scanResponse.card.isVisa.not() } ->
FeedbackEmailType.DirectUserRequest(cardInfo)
FeedbackEmailType.DirectUserRequest(metaInfo)
else -> {
showFeedbackEmailTypeOptionBS(cardInfo)
showFeedbackEmailTypeOptionBS(metaInfo)
return@launch
}
}
@ -144,18 +138,14 @@ internal class DetailsModel @Inject constructor(
}
private fun openUseDesk() {
val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null")
if (userWallet is UserWallet.Hot) {
return // TODO [REDACTED_TASK_KEY] [Hot Wallet] UseDesk
modelScope.launch {
val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null")
val metaInfo = getWalletMetaInfoUseCase.invoke(userWallet.walletId).getOrNull() ?: return@launch
router.push(AppRoute.Usedesk(metaInfo))
}
val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return
router.push(AppRoute.Usedesk(cardInfo))
}
private fun showFeedbackEmailTypeOptionBS(selectedCardInfo: CardInfo) {
private fun showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo: WalletMetaInfo) {
state.update {
it.copy(
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig(
@ -171,7 +161,7 @@ internal class DetailsModel @Inject constructor(
content = SelectEmailFeedbackTypeBS(
onOptionClick = { option ->
onEmailFeedbackTypeOptionSelected(
selectedCardInfo = selectedCardInfo,
selectedWalletMetaInfo = selectedWalletMetaInfo,
option = option,
)
@ -189,32 +179,33 @@ internal class DetailsModel @Inject constructor(
}
private fun onEmailFeedbackTypeOptionSelected(
selectedCardInfo: CardInfo,
selectedWalletMetaInfo: WalletMetaInfo,
option: SelectEmailFeedbackTypeBS.Option,
) {
modelScope.launch {
val feedbackType = when (option) {
SelectEmailFeedbackTypeBS.Option.General -> {
if (selectedCardInfo.isVisa.not()) {
FeedbackEmailType.DirectUserRequest(selectedCardInfo)
if (selectedWalletMetaInfo.isVisa == false) {
FeedbackEmailType.DirectUserRequest(selectedWalletMetaInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
.firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa.not() }
?.requireColdWallet()?.scanResponse ?: return@launch
val userWallet = getWalletsUseCase.invokeSync()
.firstOrNull {
it is UserWallet.Hot || it is UserWallet.Cold && it.scanResponse.card.isVisa.not()
} ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.DirectUserRequest(cardInfo)
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch
FeedbackEmailType.DirectUserRequest(metaInfo)
}
}
SelectEmailFeedbackTypeBS.Option.Visa -> {
if (selectedCardInfo.isVisa) {
FeedbackEmailType.Visa.DirectUserRequest(selectedCardInfo)
if (selectedWalletMetaInfo.isVisa == true) {
FeedbackEmailType.Visa.DirectUserRequest(selectedWalletMetaInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
val userWallet = getWalletsUseCase.invokeSync()
.firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa }
?.requireColdWallet()?.scanResponse ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
?: return@launch
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch
FeedbackEmailType.Visa.DirectUserRequest(metaInfo)
}
}
}

View file

@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor(
private val userWalletsFetcher = userWalletsFetcherFactory.create(
messageSender = messageSender,
onlyMultiCurrency = false,
authMode = false,
isAuthMode = false,
onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) },
)

View file

@ -26,6 +26,7 @@ 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.redux.ReduxStateHolder
@ -41,6 +42,7 @@ import com.tangem.features.home.api.HomeComponent
import com.tangem.features.home.impl.ui.state.HomeUM
import com.tangem.features.home.impl.ui.state.Stories
import com.tangem.features.home.impl.ui.state.getRestrictedStories
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@ -76,6 +78,8 @@ internal class HomeModel @Inject constructor(
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val userWalletsListManager: UserWalletsListManager,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
private val reduxStateHolder: ReduxStateHolder,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
@ -216,7 +220,7 @@ internal class HomeModel @Inject constructor(
)
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
@ -224,13 +228,21 @@ internal class HomeModel @Inject constructor(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = userWalletsListManager.walletsCount.toString(),
walletsCount = getWalletsCount().toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private suspend fun getWalletsCount(): Int {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWalletsSync().size
} else {
userWalletsListManager.walletsCount
}
}
private fun setLoading(isLoading: Boolean) {
_uiState.update { it.copy(scanInProgress = isLoading) }
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface UpgradeWalletComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, UpgradeWalletComponent>
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface ViewPhraseComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, ViewPhraseComponent>
}

View file

@ -34,12 +34,11 @@ internal class AccessCodeComponent @AssistedInject constructor(
}
interface ModelCallbacks {
fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String)
fun onAccessCodeConfirmed(userWalletId: UserWalletId)
fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String)
fun onAccessCodeUpdated(userWalletId: UserWalletId)
}
data class Params(
val isConfirmMode: Boolean,
val accessCodeToConfirm: String? = null,
val userWalletId: UserWalletId,
val callbacks: ModelCallbacks,

View file

@ -5,43 +5,64 @@ import arrow.core.getOrElse
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.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.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.SetAskBiometryShownUseCase
import com.tangem.domain.settings.ShouldShowAskBiometryUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase
import com.tangem.domain.wallets.usecase.GetHotWalletContextualUnlockUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
import com.tangem.features.hotwallet.impl.R
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.UnlockHotWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class AccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getHotWalletContextualUnlockUseCase: GetHotWalletContextualUnlockUseCase,
private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletsRepository: WalletsRepository,
private val tangemHotSdk: TangemHotSdk,
private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase,
private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<AccessCodeComponent.Params>()
private var hotWalletId: HotWalletId? = null
internal val uiState: StateFlow<AccessCodeUM>
field = MutableStateFlow(getInitialState())
private fun getInitialState() = AccessCodeUM(
accessCode = "",
onAccessCodeChange = ::onAccessCodeChange,
isConfirmMode = params.isConfirmMode,
isConfirmMode = params.accessCodeToConfirm != null,
buttonEnabled = false,
buttonInProgress = false,
onButtonClick = ::onButtonClick,
@ -51,7 +72,7 @@ internal class AccessCodeModel @Inject constructor(
uiState.update {
it.copy(
accessCode = value,
buttonEnabled = if (params.isConfirmMode) {
buttonEnabled = if (params.accessCodeToConfirm != null) {
value == params.accessCodeToConfirm
} else {
value.length == uiState.value.accessCodeLength
@ -61,12 +82,10 @@ internal class AccessCodeModel @Inject constructor(
}
private fun onButtonClick() {
if (!params.isConfirmMode) {
params.callbacks.onAccessCodeSet(params.userWalletId, uiState.value.accessCode)
if (params.accessCodeToConfirm == null) {
params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode)
} else {
params.accessCodeToConfirm?.let {
setCode(params.userWalletId, it)
}
setCode(params.userWalletId, params.accessCodeToConfirm)
}
}
@ -81,12 +100,16 @@ internal class AccessCodeModel @Inject constructor(
.getOrElse { error("User wallet with id $userWalletId not found") }
if (userWallet !is UserWallet.Hot) return@launch
val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId)
.getOrNull()
?: UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
var updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Password(accessCode.toCharArray()),
)
tryToAskForBiometry()
if (walletsRepository.requireAccessCode().not()) {
updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
@ -117,7 +140,7 @@ internal class AccessCodeModel @Inject constructor(
)
}
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
params.callbacks.onAccessCodeUpdated(params.userWalletId)
}.onFailure {
Timber.e(it)
@ -127,4 +150,54 @@ internal class AccessCodeModel @Inject constructor(
}
}
}
private suspend fun tryToAskForBiometry() {
if (!shouldAskForBiometry()) return
suspendCancellableCoroutine { continuation ->
uiMessageSender.send(
DialogMessage(
title = resourceReference(R.string.common_attention),
message = resourceReference(R.string.hot_access_code_set_biometric_ask),
firstAction = EventMessageAction(
title = resourceReference(R.string.common_allow),
onClick = {
modelScope.launch {
setAskBiometryShownUseCase()
walletsRepository.setUseBiometricAuthentication(true)
walletsRepository.setRequireAccessCode(false)
continuation.resumeWith(Result.success(Unit))
}
},
),
secondAction = EventMessageAction(
title = resourceReference(R.string.save_user_wallet_agreement_dont_allow),
onClick = {
modelScope.launch {
setAskBiometryShownUseCase()
continuation.resumeWith(Result.success(Unit))
}
},
),
onDismissRequest = {
modelScope.launch {
continuation.resumeWith(Result.success(Unit))
}
},
),
)
}
}
private suspend fun shouldAskForBiometry(): Boolean {
val canUseBiometry = canUseBiometryUseCase()
val shouldShowAskBiometry = shouldShowAskBiometryUseCase()
return canUseBiometry && shouldShowAskBiometry
}
override fun onDestroy() {
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) }
super.onDestroy()
}
}

View file

@ -74,7 +74,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
) {
PinTextField(
length = state.accessCodeLength,
isPasswordVisual = !state.isConfirmMode,
isPasswordVisual = state.isConfirmMode,
value = state.accessCode,
pinTextColor = PinTextColor.Primary,
onValueChange = state.onAccessCodeChange,

View file

@ -2,9 +2,15 @@ package com.tangem.features.hotwallet.addexistingwallet.entry
import com.arkivanov.decompose.router.stack.*
import com.tangem.common.routing.AppRoute
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.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
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.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
@ -26,6 +32,7 @@ internal class AddExistingWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
@ -69,13 +76,31 @@ internal class AddExistingWalletModel @Inject constructor(
stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished)
}
private fun showSkipAccessCodeWarningDialog() {
uiMessageSender.send(
DialogMessage(
message = resourceReference(R.string.access_code_alert_skip_description),
title = resourceReference(R.string.access_code_alert_skip_title),
firstAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {},
),
secondAction = EventMessageAction(
title = resourceReference(R.string.access_code_alert_skip_ok),
onClick = { navigateToPushNotificationsOrNext() },
),
dismissOnFirstAction = true,
),
)
}
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
override fun onBackClick() {
onChildBack()
}
override fun onSkipClick() {
navigateToPushNotificationsOrNext()
showSkipAccessCodeWarningDialog()
}
}
@ -102,11 +127,11 @@ internal class AddExistingWalletModel @Inject constructor(
}
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) {
stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode))
}
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
navigateToPushNotificationsOrNext()
}
}

View file

@ -46,7 +46,6 @@ internal class AddExistingWalletChildFactory @Inject constructor(
is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create(
context = childContext,
params = AccessCodeComponent.Params(
isConfirmMode = false,
userWalletId = route.userWalletId,
callbacks = model.accessCodeModelCallbacks,
),
@ -54,7 +53,6 @@ internal class AddExistingWalletChildFactory @Inject constructor(
is AddExistingWalletRoute.ConfirmAccessCode -> accessCodeComponentFactory.create(
context = childContext,
params = AccessCodeComponent.Params(
isConfirmMode = true,
accessCodeToConfirm = route.accessCode,
userWalletId = route.userWalletId,
callbacks = model.accessCodeModelCallbacks,

View file

@ -23,11 +23,11 @@ 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.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM
@ -56,7 +56,7 @@ internal class AddExistingWalletStartModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val urlOpener: UrlOpener,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
@ -145,7 +145,7 @@ internal class AddExistingWalletStartModel @Inject constructor(
)
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
@ -153,7 +153,7 @@ internal class AddExistingWalletStartModel @Inject constructor(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = userWalletsListManager.walletsCount.toString(),
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)

View file

@ -62,7 +62,7 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
)
OptionBlock(
modifier = Modifier
.padding(top = 24.dp),
.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),
@ -71,6 +71,8 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
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),
@ -99,6 +101,8 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
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),

View file

@ -1,6 +1,5 @@
package com.tangem.features.hotwallet.common.ui
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -8,8 +7,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@ -30,38 +29,16 @@ internal fun OptionBlock(
backgroundColor: Color,
modifier: Modifier = Modifier,
) {
val backgroundColor by animateColorAsState(
targetValue = if (enabled) {
backgroundColor
} else {
backgroundColor.copy(alpha = DISABLED_COLORS_ALPHA)
},
)
val titleColor by animateColorAsState(
targetValue = if (enabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.primary1.copy(alpha = DISABLED_COLORS_ALPHA)
},
)
val descriptionColor by animateColorAsState(
targetValue = if (enabled) {
TangemTheme.colors.text.tertiary
} else {
TangemTheme.colors.text.tertiary.copy(alpha = DISABLED_COLORS_ALPHA)
},
)
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 8.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.alpha(if (enabled) 1f else DISABLED_COLORS_ALPHA)
.background(
color = backgroundColor,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.conditional(onClick != null) {
.conditional(onClick != null && enabled) {
onClick?.let { clickableSingle(onClick = it) } ?: Modifier
}
.padding(16.dp),
@ -73,7 +50,7 @@ internal fun OptionBlock(
.padding(end = 4.dp),
text = title,
style = TangemTheme.typography.subtitle1,
color = titleColor,
color = TangemTheme.colors.text.primary1,
)
badge?.invoke()
}
@ -82,7 +59,7 @@ internal fun OptionBlock(
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = descriptionColor,
color = TangemTheme.colors.text.tertiary,
)
}
}

View file

@ -1,14 +1,10 @@
package com.tangem.features.hotwallet.manualbackup.phrase.entity
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class ManualBackupPhraseUM(
val onContinueClick: () -> Unit,
val words: ImmutableList<MnemonicGridItem> = persistentListOf(),
) {
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,
)
}
val words: ImmutableList<EnumeratedTwoColumnGridItem> = persistentListOf(),
)

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
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.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
@ -51,7 +52,7 @@ internal class ManualBackupPhraseModel @Inject constructor(
uiState.update {
it.copy(
words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s ->
ManualBackupPhraseUM.MnemonicGridItem(index + 1, s)
EnumeratedTwoColumnGridItem(index + 1, s)
}.toImmutableList(),
)
}

View file

@ -7,20 +7,18 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
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.impl.R
import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
@ -41,8 +39,8 @@ internal fun ManualBackupPhraseContent(state: ManualBackupPhraseUM, modifier: Mo
modifier = Modifier.padding(top = 20.dp),
)
SeedPhraseGridBlock(
mnemonicGridItems = state.words,
EnumeratedTwoColumnGrid(
items = state.words,
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 32.dp),
@ -97,70 +95,6 @@ private fun TitleBlock(state: ManualBackupPhraseUM, modifier: Modifier = Modifie
}
}
@Composable
private fun SeedPhraseGridBlock(
mnemonicGridItems: ImmutableList<ManualBackupPhraseUM.MnemonicGridItem>,
modifier: Modifier = Modifier,
) {
VerticalGrid(
modifier = modifier,
items = mnemonicGridItems,
) { item ->
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
verticalAlignment = Alignment.CenterVertically,
) {
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
} else {
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}
@Composable
private inline fun <T> VerticalGrid(
items: ImmutableList<T>,
modifier: Modifier = Modifier,
crossinline content: @Composable (T) -> Unit,
) {
val columnLength = items.size / 2
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
repeat(2) { index ->
Column {
for (i in 0 until columnLength) {
val item = items[index * columnLength + i]
content(item)
}
}
}
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -170,7 +104,7 @@ private fun Preview() {
state = ManualBackupPhraseUM(
onContinueClick = {},
words = List(12) {
ManualBackupPhraseUM.MnemonicGridItem(
EnumeratedTwoColumnGridItem(
index = it + 1,
mnemonic = "word${it + 1}",
)

View file

@ -35,11 +35,11 @@ internal class UpdateAccessCodeModel @Inject constructor(
}
}
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) {
stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode))
}
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
router.pop()
}
}

View file

@ -19,7 +19,6 @@ internal class UpdateAccessCodeChildFactory @Inject constructor(
is UpdateAccessCodeRoute.SetAccessCode -> accessCodeComponentFactory.create(
context = childContext,
params = AccessCodeComponent.Params(
isConfirmMode = false,
userWalletId = route.userWalletId,
callbacks = model,
),
@ -27,7 +26,6 @@ internal class UpdateAccessCodeChildFactory @Inject constructor(
is UpdateAccessCodeRoute.ConfirmAccessCode -> accessCodeComponentFactory.create(
context = childContext,
params = AccessCodeComponent.Params(
isConfirmMode = true,
accessCodeToConfirm = route.accessCode,
userWalletId = route.userWalletId,
callbacks = model,

View file

@ -7,8 +7,13 @@ import kotlinx.serialization.Serializable
internal sealed class UpdateAccessCodeRoute : Route {
@Serializable
data class SetAccessCode(val userWalletId: UserWalletId) : UpdateAccessCodeRoute()
data class SetAccessCode(
val userWalletId: UserWalletId,
) : UpdateAccessCodeRoute()
@Serializable
data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : UpdateAccessCodeRoute()
data class ConfirmAccessCode(
val userWalletId: UserWalletId,
val accessCode: String,
) : UpdateAccessCodeRoute()
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.hotwallet.upgradewallet
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.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.upgradewallet.ui.UpgradeWalletContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultUpgradeWalletComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: UpgradeWalletComponent.Params,
) : UpgradeWalletComponent, AppComponentContext by context {
private val model: UpgradeWalletModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
UpgradeWalletContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : UpgradeWalletComponent.Factory {
override fun create(
context: AppComponentContext,
params: UpgradeWalletComponent.Params,
): DefaultUpgradeWalletComponent
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.hotwallet.upgradewallet
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class UpgradeWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
) : Model() {
internal val uiState: StateFlow<UpgradeWalletUM>
field = MutableStateFlow(
UpgradeWalletUM(
onBackClick = { router.pop() },
onBuyTangemWalletClick = ::onBuyTangemWalletClick,
onScanDeviceClick = ::onScanDeviceClick,
),
)
private fun onBuyTangemWalletClick() {
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun onScanDeviceClick() {
// TODO [REDACTED_TASK_KEY]
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.hotwallet.upgradewallet.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.upgradewallet.DefaultUpgradeWalletComponent
import com.tangem.features.hotwallet.upgradewallet.UpgradeWalletModel
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 UpgradeWalletModule {
@Binds
fun bindUpgradeWalletComponentFactory(impl: DefaultUpgradeWalletComponent.Factory): UpgradeWalletComponent.Factory
@Binds
@IntoMap
@ClassKey(UpgradeWalletModel::class)
fun bindUpgradeWalletModel(model: UpgradeWalletModel): Model
}

View file

@ -0,0 +1,7 @@
package com.tangem.features.hotwallet.upgradewallet.entity
internal data class UpgradeWalletUM(
val onBackClick: () -> Unit,
val onBuyTangemWalletClick: () -> Unit,
val onScanDeviceClick: () -> Unit,
)

View file

@ -0,0 +1,161 @@
package com.tangem.features.hotwallet.upgradewallet.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.PrimaryButtonIconEnd
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
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.upgradewallet.entity.UpgradeWalletUM
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
TangemTopAppBar(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onBackClick),
title = TextReference.EMPTY,
)
Column(
modifier = Modifier
.weight(1f)
.padding(
start = 16.dp,
top = 24.dp,
end = 16.dp,
),
) {
Icon(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(R.drawable.ic_tangem_64),
contentDescription = null,
tint = Color.Unspecified,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
top = 20.dp,
end = 16.dp,
),
text = stringResourceSafe(R.string.hw_upgrade_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
FeatureBlock(
modifier = Modifier
.padding(top = 32.dp),
title = stringResourceSafe(R.string.hw_upgrade_key_migration_title),
description = stringResourceSafe(R.string.hw_upgrade_key_migration_description),
iconRes = R.drawable.ic_mobile_security_24,
)
FeatureBlock(
modifier = Modifier
.padding(top = 24.dp),
title = stringResourceSafe(R.string.hw_upgrade_funds_access_title),
description = stringResourceSafe(R.string.hw_upgrade_funds_access_description),
iconRes = R.drawable.ic_knight_shield_24,
)
FeatureBlock(
modifier = Modifier
.padding(top = 24.dp),
title = stringResourceSafe(R.string.hw_upgrade_general_security_title),
description = stringResourceSafe(R.string.hw_upgrade_general_security_description),
iconRes = R.drawable.ic_protect_24,
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(R.string.details_buy_wallet),
onClick = state.onBuyTangemWalletClick,
)
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(R.string.hw_upgrade_scan_device),
onClick = state.onScanDeviceClick,
iconResId = R.drawable.ic_tangem_24,
)
}
}
}
@Composable
private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier
.padding(horizontal = 12.dp),
painter = painterResource(iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewUpgradeWalletContent() {
TangemThemePreview {
UpgradeWalletContent(
state = UpgradeWalletUM(
onBackClick = {},
onBuyTangemWalletClick = {},
onScanDeviceClick = {},
),
)
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.features.hotwallet.viewphrase
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.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel
import com.tangem.features.hotwallet.viewphrase.ui.ViewPhraseContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultViewPhraseComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: ViewPhraseComponent.Params,
) : ViewPhraseComponent, AppComponentContext by context {
private val model: ViewPhraseModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ViewPhraseContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : ViewPhraseComponent.Factory {
override fun create(
context: AppComponentContext,
params: ViewPhraseComponent.Params,
): DefaultViewPhraseComponent
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.hotwallet.viewphrase.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.DefaultViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel
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 ViewPhraseModule {
@Binds
@IntoMap
@ClassKey(ViewPhraseModel::class)
fun bindViewPhraseModel(model: ViewPhraseModel): Model
@Binds
fun bindViewPhraseComponentFactory(factory: DefaultViewPhraseComponent.Factory): ViewPhraseComponent.Factory
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.hotwallet.viewphrase.entity
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class ViewPhraseUM(
val onBackClick: () -> Unit,
val words: ImmutableList<EnumeratedTwoColumnGridItem> = persistentListOf(),
)

View file

@ -0,0 +1,76 @@
package com.tangem.features.hotwallet.viewphrase.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase
import com.tangem.domain.wallets.usecase.ExportSeedPhraseUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class ViewPhraseModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val exportSeedPhraseUseCase: ExportSeedPhraseUseCase,
private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase,
) : Model() {
private val params = paramsContainer.require<ViewPhraseComponent.Params>()
private var hotWalletId: HotWalletId? = null
internal val uiState: StateFlow<ViewPhraseUM>
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
init {
loadSeedPhrase()
}
private fun loadSeedPhrase() {
val userWallet = getUserWalletUseCase(params.userWalletId)
.getOrElse { error("User wallet with id ${params.userWalletId} not found") }
if (userWallet is UserWallet.Hot) {
hotWalletId = userWallet.hotWalletId
modelScope.launch {
val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId)
.getOrElse { error("Unable to export seed phrase for wallet with id ${params.userWalletId}") }
.mnemonic
.mnemonicComponents
uiState.update {
it.copy(
words = words.mapIndexed { index, s ->
EnumeratedTwoColumnGridItem(index + 1, s)
}.toImmutableList(),
)
}
}
}
}
override fun onDestroy() {
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) }
super.onDestroy()
}
}

View file

@ -0,0 +1,103 @@
package com.tangem.features.hotwallet.viewphrase.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
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.impl.R
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun ViewPhraseContent(state: ViewPhraseUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
AppBarWithBackButton(
text = stringResourceSafe(R.string.common_backup),
onBackClick = state.onBackClick,
)
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.imePadding()
.weight(1f),
) {
TitleBlock(
state = state,
modifier = Modifier.padding(top = 20.dp),
)
EnumeratedTwoColumnGrid(
items = state.words,
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 32.dp),
)
}
}
}
@Composable
private fun TitleBlock(state: ViewPhraseUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size36)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = stringResourceSafe(R.string.backup_seed_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResourceSafe(
R.string.backup_seed_caution,
state.words.size,
),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
ViewPhraseContent(
state = ViewPhraseUM(
onBackClick = {},
words = List(12) {
EnumeratedTwoColumnGridItem(
index = it + 1,
mnemonic = "word${it + 1}",
)
}.toImmutableList(),
),
)
}
}

View file

@ -4,10 +4,16 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.router.stack.replaceAll
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.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
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.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
@ -32,6 +38,7 @@ internal class WalletActivationModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
val params = paramsContainer.require<WalletActivationComponent.Params>()
@ -79,13 +86,31 @@ internal class WalletActivationModel @Inject constructor(
stackNavigation.replaceAll(WalletActivationRoute.SetupFinished)
}
private fun showSkipAccessCodeWarningDialog() {
uiMessageSender.send(
DialogMessage(
message = resourceReference(R.string.access_code_alert_skip_description),
title = resourceReference(R.string.access_code_alert_skip_title),
firstAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {},
),
secondAction = EventMessageAction(
title = resourceReference(R.string.access_code_alert_skip_ok),
onClick = { navigateToPushNotificationsOrNext() },
),
dismissOnFirstAction = true,
),
)
}
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
override fun onBackClick() {
onChildBack()
}
override fun onSkipClick() {
navigateToPushNotificationsOrNext()
showSkipAccessCodeWarningDialog()
}
}
@ -116,11 +141,11 @@ internal class WalletActivationModel @Inject constructor(
}
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) {
stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode))
}
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
navigateToPushNotificationsOrNext()
}
}

View file

@ -55,7 +55,6 @@ internal class WalletActivationChildFactory @Inject constructor(
is WalletActivationRoute.SetAccessCode -> accessCodeComponentFactory.create(
context = childContext,
params = AccessCodeComponent.Params(
isConfirmMode = false,
userWalletId = model.params.userWalletId,
callbacks = model.accessCodeModelCallbacks,
),
@ -63,7 +62,6 @@ internal class WalletActivationChildFactory @Inject constructor(
is WalletActivationRoute.ConfirmAccessCode -> accessCodeComponentFactory.create(
context = childContext,
params = AccessCodeComponent.Params(
isConfirmMode = true,
accessCodeToConfirm = route.accessCode,
userWalletId = model.params.userWalletId,
callbacks = model.accessCodeModelCallbacks,

View file

@ -4,10 +4,12 @@ import com.tangem.core.ui.components.label.entity.LabelUM
internal data class WalletBackupUM(
val onBackClick: () -> Unit,
val recoveryPhraseStatus: LabelUM?,
val googleDriveStatus: LabelUM?,
val recoveryPhraseOption: LabelUM?,
val googleDriveOption: LabelUM?,
val googleDriveStatus: BackupStatus,
val onRecoveryPhraseClick: () -> Unit,
val onGoogleDriveClick: () -> Unit,
val onHardwareWalletClick: () -> Unit,
val backedUp: Boolean,
)

View file

@ -1,37 +1,33 @@
package com.tangem.features.hotwallet.walletbackup.model
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.navigation.Router
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.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.common.routing.AppRoute
import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class WalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
getWalletUseCase: GetUserWalletUseCase,
private val router: Router,
private val getWalletUseCase: GetUserWalletUseCase,
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
override val dispatchers: CoroutineDispatcherProvider,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val router: Router,
) : Model() {
private val params: WalletBackupComponent.Params = paramsContainer.require()
@ -40,48 +36,32 @@ internal class WalletBackupModel @Inject constructor(
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseStatus = LabelUM(
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
googleDriveStatus = LabelUM(
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
backedUp = false,
),
)
private val makeBackupAtFirstAlertBS
get() = bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_passcode_lock_32) {
type = MessageBottomSheetUMV2.Icon.Type.Accent
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_backup_need_title)
body = resourceReference(R.string.hw_backup_need_description)
}
secondaryButton {
text = resourceReference(R.string.hw_backup_need_action)
onClick {
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
closeBs()
}
}
}
init {
getWalletUseCase.invokeFlow(params.userWalletId)
.map { it.getOrNull() }
.distinctUntilChanged()
.filterNotNull()
.onEach {
updateBackupStatuses(it)
}
.launchIn(modelScope)
getWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = {
updateBackupStatuses(it)
},
)
}
private fun updateBackupStatuses(userWallet: UserWallet) {
@ -95,7 +75,7 @@ internal class WalletBackupModel @Inject constructor(
}
private fun WalletBackupUM.updateBackupStatusesHotWallet(userWallet: UserWallet.Hot): WalletBackupUM = copy(
recoveryPhraseStatus = if (userWallet.backedUp) {
recoveryPhraseOption = if (userWallet.backedUp) {
LabelUM(
text = resourceReference(R.string.common_done),
style = LabelStyle.ACCENT,
@ -106,7 +86,7 @@ internal class WalletBackupModel @Inject constructor(
style = LabelStyle.WARNING,
)
},
googleDriveStatus = LabelUM(
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
@ -115,9 +95,41 @@ internal class WalletBackupModel @Inject constructor(
private fun onRecoveryPhraseClick() {
if (uiState.value.backedUp) {
// TODO [REDACTED_TASK_KEY]
getWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = { userWallet ->
when (userWallet) {
is UserWallet.Cold -> {
val userWalletId = userWallet.walletId
Timber.e("Unexpected cold wallet when request seed phrase: $userWalletId")
}
is UserWallet.Hot -> showSeedPhrase(userWallet)
}
},
)
} else {
uiMessageSender.send(makeBackupAtFirstAlertBS)
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
}
}
private fun showSeedPhrase(hotWallet: UserWallet.Hot) {
modelScope.launch {
unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId)
.fold(
ifLeft = {
Timber.e("Error while export seed phrase: $it")
},
ifRight = { seedPhrasePrivateInfo ->
router.push(AppRoute.ViewPhrase(params.userWalletId))
},
)
}
}
private fun onHardwareWalletClick() {
router.push(AppRoute.UpgradeWallet(params.userWalletId))
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@ -25,6 +26,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.rows.NetworkTitle
import com.tangem.core.ui.extensions.resourceReference
@OptIn(ExperimentalMaterial3Api::class)
@ -47,11 +49,12 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod
.padding(horizontal = 16.dp),
) {
OptionBlock(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(top = 8.dp),
title = stringResourceSafe(R.string.hw_backup_seed_title),
description = stringResourceSafe(R.string.hw_backup_seed_description),
badge = {
state.recoveryPhraseStatus?.let { Label(it) }
state.recoveryPhraseOption?.let { Label(it) }
},
onClick = state.onRecoveryPhraseClick,
enabled = true,
@ -59,16 +62,37 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod
)
OptionBlock(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(top = 8.dp),
title = stringResourceSafe(R.string.hw_backup_google_drive_title),
description = stringResourceSafe(R.string.hw_backup_google_drive_description),
badge = {
state.googleDriveStatus?.let { Label(it) }
state.googleDriveOption?.let { Label(it) }
},
onClick = state.onGoogleDriveClick,
enabled = state.googleDriveStatus != BackupStatus.ComingSoon,
backgroundColor = TangemTheme.colors.background.primary,
)
NetworkTitle(
title = {
Text(
modifier = Modifier,
text = stringResourceSafe(R.string.express_provider_recommended),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
)
OptionBlock(
modifier = Modifier.fillMaxWidth(),
title = stringResourceSafe(R.string.hw_backup_hardware_title),
description = stringResourceSafe(R.string.hw_backup_hardware_description),
badge = null,
onClick = state.onHardwareWalletClick,
enabled = true,
backgroundColor = TangemTheme.colors.background.primary,
)
}
}
}
@ -85,45 +109,51 @@ private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider:
private class WalletBackupUMProvider : CollectionPreviewParameterProvider<WalletBackupUM>(
collection = listOf(
WalletBackupUM(
recoveryPhraseStatus = LabelUM(
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
googleDriveStatus = LabelUM(
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onBackClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
onHardwareWalletClick = {},
backedUp = false,
),
WalletBackupUM(
recoveryPhraseStatus = LabelUM(
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
googleDriveStatus = LabelUM(
googleDriveOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
googleDriveStatus = BackupStatus.NoBackup,
onBackClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
onHardwareWalletClick = {},
backedUp = false,
),
WalletBackupUM(
recoveryPhraseStatus = LabelUM(
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.common_done),
style = LabelStyle.ACCENT,
),
googleDriveStatus = LabelUM(
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_done),
style = LabelStyle.ACCENT,
),
googleDriveStatus = BackupStatus.Done,
onBackClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
onHardwareWalletClick = {},
backedUp = false,
),
),

View file

@ -4,14 +4,9 @@ import com.tangem.core.decompose.context.AppComponentContext
interface KycComponent {
fun launch(params: Params)
fun launch()
interface Factory {
fun create(appComponentContext: AppComponentContext): KycComponent
}
data class Params(
val targetAddress: String,
val cardId: String,
)
}

View file

@ -18,7 +18,7 @@ class DefaultKycComponent @AssistedInject constructor(
private val model: DefaultKycModel = getOrCreateModel()
override fun launch(params: KycComponent.Params) {
override fun launch() {
componentScope.launch {
model.uiState.collect {
it?.let { startInfo ->
@ -35,7 +35,7 @@ class DefaultKycComponent @AssistedInject constructor(
}
}
}
model.getKycToken(params)
model.getKycToken()
}
@AssistedFactory

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.pay.KycStartInfo
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.pay.usecase.KycStartInfoUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -15,18 +15,15 @@ import javax.inject.Inject
@ModelScoped
class DefaultKycModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
kycRepositoryFactory: KycRepository.Factory,
private val kycStartInfoUseCase: KycStartInfoUseCase,
) : Model() {
private val kycRepository = kycRepositoryFactory.create()
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
val uiState = _uiState.asStateFlow()
fun getKycToken(params: KycComponent.Params) {
fun getKycToken() {
modelScope.launch {
kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull()
?.let { _uiState.emit(it) }
kycStartInfoUseCase().getOrNull()?.let { _uiState.emit(it) }
}
}
}

View file

@ -2,12 +2,11 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
interface AddCustomTokenComponent : ComposableBottomSheetComponent {
data class Params(
val userWalletId: UserWalletId,
val mode: AddCustomTokenMode,
val source: ManageTokensSource,
val onDismiss: () -> Unit,
val onCurrencyAdded: () -> Unit,

View file

@ -7,9 +7,16 @@ import com.tangem.domain.models.wallet.UserWalletId
interface ManageTokensComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId?,
val mode: ManageTokensMode,
val source: ManageTokensSource,
)
) {
constructor(userWalletId: UserWalletId?, source: ManageTokensSource) : this(
source = source,
mode = userWalletId
?.let { ManageTokensMode.Wallet(userWalletId) }
?: ManageTokensMode.None,
)
}
interface Factory : ComponentFactory<Params, ManageTokensComponent>
}

View file

@ -1,8 +1,22 @@
package com.tangem.features.managetokens.component
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
enum class ManageTokensSource(val analyticsName: String) {
STORIES(analyticsName = "Stories"),
ONBOARDING(analyticsName = "Onboarding"),
SETTINGS(analyticsName = "Settings"),
SEND_VIA_SWAP(analyticsName = "SendViaSwap"),
}
sealed interface ManageTokensMode {
data class Wallet(val userWalletId: UserWalletId) : ManageTokensMode
data class Account(val accountId: AccountId) : ManageTokensMode
data object None : ManageTokensMode
}
sealed interface AddCustomTokenMode {
data class Wallet(val userWalletId: UserWalletId) : AddCustomTokenMode
data class Account(val accountId: AccountId) : AddCustomTokenMode
}

View file

@ -22,6 +22,7 @@ import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBot
import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent.Source
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
@ -29,6 +30,7 @@ import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
import com.tangem.features.managetokens.utils.list.ManageTokensUseCasesFacade
import com.tangem.features.managetokens.utils.list.getLoadingItems
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
@ -51,12 +53,19 @@ internal class ChooseManagedTokensModel @Inject constructor(
private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
) : Model() {
private val params: ChooseManagedTokensComponent.Params = paramsContainer.require()
private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory
.create(mode = ManageTokensMode.None)
private val manageTokensListManager = manageTokensListManagerFactory.create(
scope = modelScope,
source = ManageTokensSource.SEND_VIA_SWAP,
mode = ManageTokensMode.None,
useCasesFacade = useCasesFacade,
onCurrencySelect = { token ->
bottomSheetNavigation.activate(
ChooseManageTokensBottomSheetConfig.SwapTokensBottomSheetConfig(
@ -86,7 +95,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
observeSearchQueryChanges()
modelScope.launch {
manageTokensListManager.launchPagination(source = ManageTokensSource.SEND_VIA_SWAP, userWalletId = null)
manageTokensListManager.launchPagination()
}
}
@ -159,7 +168,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
}
}
.sample(periodMillis = 1_000)
.onEach { query -> manageTokensListManager.search(userWalletId = null, query = query) }
.onEach { query -> manageTokensListManager.search(query = query) }
.launchIn(modelScope)
}
@ -295,7 +304,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
if (state.readContent.isInitialBatchLoading || state.readContent.isNextBatchLoading) return false
modelScope.launch {
manageTokensListManager.loadMore(userWalletId = null, query = state.readContent.search.query)
manageTokensListManager.loadMore(query = state.readContent.search.query)
}
return true

View file

@ -2,13 +2,12 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableDialogComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent {
data class Params(
val userWalletId: UserWalletId,
val mode: AddCustomTokenMode,
val onConfirm: (SelectedDerivationPath) -> Unit,
val onDismiss: () -> Unit,
)

View file

@ -2,7 +2,6 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
@ -10,7 +9,7 @@ import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
internal interface CustomTokenFormComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val mode: AddCustomTokenMode,
val network: SelectedNetwork,
val derivationPath: SelectedDerivationPath?,
val formValues: CustomTokenFormValues,

View file

@ -2,7 +2,6 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
@ -11,13 +10,13 @@ internal interface CustomTokenSelectorComponent : ComposableContentComponent {
sealed class Params {
data class NetworkSelector(
val userWalletId: UserWalletId,
val mode: AddCustomTokenMode,
val selectedNetwork: SelectedNetwork?,
val onNetworkSelected: (SelectedNetwork) -> Unit,
) : Params()
data class DerivationPathSelector(
val userWalletId: UserWalletId,
val mode: AddCustomTokenMode,
val selectedNetwork: SelectedNetwork,
val selectedDerivationPath: SelectedDerivationPath?,
val onDerivationPathSelected: (SelectedDerivationPath) -> Unit,

View file

@ -38,7 +38,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
) : AddCustomTokenComponent, AppComponentContext by context {
private val initialConfiguration = AddCustomTokenConfig(
userWalletId = params.userWalletId,
mode = params.mode,
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
)
@ -105,7 +105,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
selectorComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = config.userWalletId,
mode = config.mode,
selectedNetwork = null,
onNetworkSelected = ::changeSelectedNetwork,
),
@ -115,7 +115,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
selectorComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = config.userWalletId,
mode = config.mode,
selectedNetwork = config.selectedNetwork,
onNetworkSelected = ::changeSelectedNetwork,
),
@ -125,7 +125,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
selectorComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = config.userWalletId,
mode = config.mode,
selectedNetwork = requireNotNull(config.selectedNetwork) {
"Network is not selected"
},
@ -138,7 +138,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
formComponentFactory.create(
context = childByContext(componentContext),
params = CustomTokenFormComponent.Params(
userWalletId = config.userWalletId,
mode = config.mode,
network = requireNotNull(config.selectedNetwork) {
"Network is not selected"
},

View file

@ -41,7 +41,7 @@ internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor(
is CustomTokenSelectorDialogConfig.CustomDerivationInput -> customTokenDerivationInputComponentFactory.create(
context = childByContext(context),
params = CustomTokenDerivationInputComponent.Params(
userWalletId = config.userWalletId,
mode = config.mode,
onConfirm = model::selectCustomDerivationPath,
onDismiss = model.dialogNavigation::dismiss,
),

View file

@ -13,6 +13,7 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig
import com.tangem.features.managetokens.model.ManageTokensModel
@ -52,18 +53,20 @@ internal class DefaultManageTokensComponent @AssistedInject constructor(
private fun bottomSheetChild(
config: ManageTokensBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
is ManageTokensBottomSheetConfig.AddCustomToken -> {
addCustomTokenComponentFactory.create(
context = childByContext(componentContext),
params = AddCustomTokenComponent.Params(
userWalletId = config.userWalletId,
source = params.source,
onDismiss = model.bottomSheetNavigation::dismiss,
onCurrencyAdded = model::reloadList,
),
)
): ComposableBottomSheetComponent {
val mode = when (config) {
is ManageTokensBottomSheetConfig.AddWalletCustomToken -> AddCustomTokenMode.Wallet(config.userWalletId)
is ManageTokensBottomSheetConfig.AddAccountCustomToken -> AddCustomTokenMode.Account(config.accountId)
}
return addCustomTokenComponentFactory.create(
context = childByContext(componentContext),
params = AddCustomTokenComponent.Params(
mode = mode,
source = params.source,
onDismiss = model.bottomSheetNavigation::dismiss,
onCurrencyAdded = model::reloadList,
),
)
}
@AssistedFactory

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
@ -14,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class PreviewAddCustomTokenComponent(
initialState: AddCustomTokenConfig = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")),
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
),
) : AddCustomTokenComponent {
@ -41,7 +42,7 @@ internal class PreviewAddCustomTokenComponent(
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = config.userWalletId,
mode = config.mode,
selectedNetwork = null,
onNetworkSelected = {},
),
@ -50,7 +51,7 @@ internal class PreviewAddCustomTokenComponent(
AddCustomTokenConfig.Step.NETWORK_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = config.userWalletId,
mode = config.mode,
selectedNetwork = config.selectedNetwork,
onNetworkSelected = {},
),
@ -59,7 +60,7 @@ internal class PreviewAddCustomTokenComponent(
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = config.userWalletId,
mode = config.mode,
selectedNetwork = config.selectedNetwork!!,
selectedDerivationPath = config.selectedDerivationPath!!,
onDerivationPathSelected = {},

View file

@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
@ -18,7 +19,7 @@ import kotlinx.collections.immutable.toImmutableList
internal class PreviewCustomTokenSelectorComponent(
private val params: Params = Params.NetworkSelector(
userWalletId = UserWalletId(stringValue = "321"),
mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")),
selectedNetwork = null,
onNetworkSelected = {},
),

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
@ -38,8 +39,10 @@ internal class PreviewManageTokensComponent(
value = ManageTokensUM.ManageContent(
popBack = {},
items = items,
topBar = if (params.userWalletId != null) {
ManageTokensTopBarUM.ManageContent(
topBar = when (params.mode) {
is ManageTokensMode.Account,
is ManageTokensMode.Wallet,
-> ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = {},
endButton = TopAppBarButtonUM.Icon(
@ -47,8 +50,7 @@ internal class PreviewManageTokensComponent(
onClicked = {},
),
)
} else {
ManageTokensTopBarUM.ReadContent(
ManageTokensMode.None -> ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = {},
)

View file

@ -1,13 +1,13 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenMode
import kotlinx.serialization.Serializable
@Serializable
internal data class AddCustomTokenConfig(
val step: Step,
val userWalletId: UserWalletId,
val mode: AddCustomTokenMode,
val selectedNetwork: SelectedNetwork? = null,
val selectedDerivationPath: SelectedDerivationPath? = null,
val formValues: CustomTokenFormValues = CustomTokenFormValues(),

View file

@ -1,6 +1,6 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenMode
import kotlinx.serialization.Serializable
@Serializable
@ -8,6 +8,6 @@ internal sealed class CustomTokenSelectorDialogConfig {
@Serializable
data class CustomDerivationInput(
val userWalletId: UserWalletId,
val mode: AddCustomTokenMode,
) : CustomTokenSelectorDialogConfig()
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.managetokens.entity.managetokens
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@ -7,7 +8,12 @@ import kotlinx.serialization.Serializable
internal sealed class ManageTokensBottomSheetConfig {
@Serializable
data class AddCustomToken(
data class AddWalletCustomToken(
val userWalletId: UserWalletId,
) : ManageTokensBottomSheetConfig()
@Serializable
data class AddAccountCustomToken(
val accountId: AccountId,
) : ManageTokensBottomSheetConfig()
}

View file

@ -12,9 +12,6 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
@ -25,6 +22,7 @@ import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.CustomCurrencyFormBuilder
import com.tangem.features.managetokens.utils.CustomCurrencyValidator
import com.tangem.features.managetokens.utils.list.CustomTokenFormUseCasesFacade
import com.tangem.features.managetokens.utils.mapper.mapToDomainModel
import com.tangem.features.managetokens.utils.ui.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -40,18 +38,17 @@ import javax.inject.Inject
@ModelScoped
internal class CustomTokenFormModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val customCurrencyValidator: CustomCurrencyValidator,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val messageSender: UiMessageSender,
private val customTokenFormManager: CustomCurrencyFormBuilder,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory,
) : Model() {
private val params: CustomTokenFormComponent.Params = paramsContainer.require()
private var createdCurrency: CryptoCurrency? = null
private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode)
private val customCurrencyValidator = CustomCurrencyValidator(useCasesFacade)
val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
value = getInitialState(),
@ -117,7 +114,6 @@ internal class CustomTokenFormModel @Inject constructor(
.drop(count = 1) // Skip initial state
.onEach { formValues ->
customCurrencyValidator.validateForm(
userWalletId = params.userWalletId,
networkId = params.network.id,
derivationPath = getDerivationPath(),
formValues = formValues,
@ -156,7 +152,6 @@ internal class CustomTokenFormModel @Inject constructor(
private fun validatePrefilledForm() = modelScope.launch {
customCurrencyValidator.validateForm(
userWalletId = params.userWalletId,
networkId = params.network.id,
derivationPath = getDerivationPath(),
formValues = state.value.tokenForm.mapToDomainModel(),
@ -169,8 +164,7 @@ internal class CustomTokenFormModel @Inject constructor(
isAlreadyAdded: Boolean,
isCustom: Boolean,
) = modelScope.launch {
val needToAddDerivation = hasMissedDerivationsUseCase(
userWalletId = params.userWalletId,
val needToAddDerivation = useCasesFacade.hasMissedDerivationsUseCase(
networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value),
)
@ -343,13 +337,13 @@ internal class CustomTokenFormModel @Inject constructor(
)
analyticsEventHandler.send(event)
derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse {
useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse {
Timber.e(it, "Failed to derive public keys")
showErrorDialog()
return@resource
}
addCryptoCurrenciesUseCase(params.userWalletId, currency).getOrElse {
useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse {
Timber.e(it, "Failed to add currency")
showErrorDialog()
return@resource

View file

@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.DerivationPathSelector
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.NetworkSelector
@ -83,7 +83,7 @@ internal class CustomTokenSelectorModel @Inject constructor(
}
private suspend fun loadNetworks(selector: NetworkSelector): List<SelectableItemUM> {
return getSupportedNetworks(selector.userWalletId).map { network ->
return getSupportedNetworks(selector.mode).map { network ->
network.toCurrencyNetworkModel(
isSelected = network.id == selector.selectedNetwork?.id,
onSelectedStateChange = {
@ -122,7 +122,7 @@ internal class CustomTokenSelectorModel @Inject constructor(
derivationPaths.add(defaultPath)
}
getSupportedNetworks(selector.userWalletId)
getSupportedNetworks(selector.mode)
.mapNotNullTo(derivationPaths) { network ->
if (network.id == selector.selectedNetwork.id) {
return@mapNotNullTo null // Skip default path
@ -146,8 +146,9 @@ internal class CustomTokenSelectorModel @Inject constructor(
return derivationPaths
}
private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
return getSupportedNetworksUseCase(userWalletId).getOrElse { e ->
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error))
messageSender.send(message)
@ -158,7 +159,7 @@ internal class CustomTokenSelectorModel @Inject constructor(
private fun showCustomDerivationInput() {
val config = when (params) {
is NetworkSelector -> return
is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.userWalletId)
is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.mode)
}
dialogNavigation.activate(config)

View file

@ -17,12 +17,10 @@ import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
@ -30,6 +28,7 @@ import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
import com.tangem.features.managetokens.utils.list.ManageTokensUseCasesFacade
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -47,18 +46,24 @@ internal class ManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val messageSender: UiMessageSender,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
paramsContainer: ParamsContainer,
) : Model() {
private val params: ManageTokensComponent.Params = paramsContainer.require()
private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory
.create(mode = params.mode)
private val manageTokensListManager = manageTokensListManagerFactory.create()
private val manageTokensListManager = manageTokensListManagerFactory.create(
scope = modelScope,
source = params.source,
mode = params.mode,
useCasesFacade = useCasesFacade,
)
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(getInitialState(params.userWalletId))
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(getInitialState())
val bottomSheetNavigation: SlotNavigation<ManageTokensBottomSheetConfig> = SlotNavigation()
init {
@ -79,23 +84,24 @@ internal class ManageTokensModel @Inject constructor(
observeSearchQueryChanges()
modelScope.launch {
manageTokensListManager.launchPagination(source = params.source, userWalletId = params.userWalletId)
manageTokensListManager.launchPagination()
}
}
fun reloadList() {
modelScope.launch {
manageTokensListManager.reload(params.userWalletId)
manageTokensListManager.reload()
}
}
private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM {
private fun getInitialState(): ManageTokensUM {
analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(params.source))
return if (userWalletId == null) {
createReadContentModel()
} else {
createManageContentModel()
return when (params.mode) {
is ManageTokensMode.Wallet,
is ManageTokensMode.Account,
-> createManageContentModel()
ManageTokensMode.None -> createReadContentModel()
}
}
@ -164,7 +170,7 @@ internal class ManageTokensModel @Inject constructor(
}
}
.sample(periodMillis = 1_000)
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
.onEach { query -> manageTokensListManager.search(query = query) }
.launchIn(modelScope)
}
@ -261,19 +267,16 @@ internal class ManageTokensModel @Inject constructor(
private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) {
modelScope.launch {
val hasMissedDerivations = params.userWalletId?.let { walletId ->
val networks = currenciesToAdd.values
.flatten()
.toSet()
.associate { it.backendId to null }
hasMissedDerivationsUseCase(walletId, networks)
}
val networks = currenciesToAdd.values
.flatten()
.toSet()
.associate { it.backendId to null }
val hasMissedDerivations = useCasesFacade.hasMissedDerivationsUseCase(networks)
state.update { state ->
state.copySealed(
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
needToAddDerivations = hasMissedDerivations ?: false,
needToAddDerivations = hasMissedDerivations,
)
}
}
@ -284,7 +287,7 @@ internal class ManageTokensModel @Inject constructor(
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
modelScope.launch {
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
manageTokensListManager.loadMore(query = state.search.query)
}
return true
@ -292,9 +295,14 @@ internal class ManageTokensModel @Inject constructor(
private fun navigateToAddCustomToken() {
analyticsEventHandler.send(CustomTokenAnalyticsEvent.ButtonCustomToken(params.source))
params.userWalletId?.let {
bottomSheetNavigation.activate(ManageTokensBottomSheetConfig.AddCustomToken(it))
when (val portfolio = params.mode) {
is ManageTokensMode.Wallet ->
bottomSheetNavigation
.activate(ManageTokensBottomSheetConfig.AddWalletCustomToken(portfolio.userWalletId))
is ManageTokensMode.Account ->
bottomSheetNavigation
.activate(ManageTokensBottomSheetConfig.AddAccountCustomToken(portfolio.accountId))
ManageTokensMode.None -> Unit
}
}
@ -308,8 +316,7 @@ internal class ManageTokensModel @Inject constructor(
)
analyticsEventHandler.send(event)
saveManagedTokensUseCase(
userWalletId = requireNotNull(params.userWalletId),
useCasesFacade.saveManagedTokensUseCase(
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
).getOrElse {

View file

@ -13,11 +13,10 @@ import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
import com.tangem.domain.redux.OnboardingManageTokensAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
@ -25,6 +24,7 @@ import com.tangem.features.managetokens.entity.managetokens.OnboardingManageToke
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
import com.tangem.features.managetokens.utils.list.ManageTokensUseCasesFacade
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -42,15 +42,22 @@ internal class OnboardingManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val messageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
paramsContainer: ParamsContainer,
) : Model() {
private val params: OnboardingManageTokensComponent.Params = paramsContainer.require()
private val manageTokensListManager = manageTokensListManagerFactory.create()
private val portfolio = ManageTokensMode.Wallet(params.userWalletId)
private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory
.create(mode = portfolio)
private val manageTokensListManager = manageTokensListManagerFactory.create(
scope = modelScope,
source = ManageTokensSource.ONBOARDING,
useCasesFacade = useCasesFacade,
mode = portfolio,
)
val state: MutableStateFlow<OnboardingManageTokensUM> = MutableStateFlow(getInitialState())
val returnToParentComponentFlow = MutableSharedFlow<Unit>()
@ -73,10 +80,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
observeSearchQueryChanges()
modelScope.launch {
manageTokensListManager.launchPagination(
source = ManageTokensSource.ONBOARDING,
userWalletId = params.userWalletId,
)
manageTokensListManager.launchPagination()
}
}
@ -119,7 +123,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
}
}
.sample(periodMillis = 1_000)
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
.onEach { query -> manageTokensListManager.search(query = query) }
.launchIn(modelScope)
}
@ -208,13 +212,11 @@ internal class OnboardingManageTokensModel @Inject constructor(
)
}
} else {
val hasMissedDerivations = hasMissedDerivationsUseCase.invoke(
userWalletId = params.userWalletId,
networksWithDerivationPath = currenciesToAdd.values
.flatten()
.toSet()
.associate { it.backendId to null },
)
val network = currenciesToAdd.values
.flatten()
.toSet()
.associate { it.backendId to null }
val hasMissedDerivations = useCasesFacade.hasMissedDerivationsUseCase(network = network)
state.update { state ->
state.copy(
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Continue(
@ -231,7 +233,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
modelScope.launch {
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
manageTokensListManager.loadMore(query = state.search.query)
}
return true
@ -255,8 +257,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
)
analyticsEventHandler.send(event)
saveManagedTokensUseCase(
userWalletId = requireNotNull(params.userWalletId),
useCasesFacade.saveManagedTokensUseCase(
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
).getOrElse {
@ -281,8 +282,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
) {
analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater)
saveManagedTokensUseCase(
userWalletId = requireNotNull(params.userWalletId),
useCasesFacade.saveManagedTokensUseCase(
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
).getOrElse {

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenUM
@ -68,12 +69,13 @@ private fun Preview_AddCustomTokenBottomSheet(
}
private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<AddCustomTokenComponent> {
private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321"))
override val values: Sequence<AddCustomTokenComponent>
get() = sequenceOf(
PreviewAddCustomTokenComponent(),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
mode = mode,
step = AddCustomTokenConfig.Step.FORM,
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "1", derivationPath = Network.DerivationPath.None),
@ -85,7 +87,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
mode = mode,
step = AddCustomTokenConfig.Step.NETWORK_SELECTOR,
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0", derivationPath = Network.DerivationPath.None),
@ -97,7 +99,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
mode = mode,
step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
selectedDerivationPath = SelectedDerivationPath(
id = Network.ID(value = "0", derivationPath = Network.DerivationPath.None),

View file

@ -32,6 +32,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
@ -268,12 +269,13 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider :
PreviewParameterProvider<CustomTokenSelectorComponent> {
private val derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0")
private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321"))
override val values: Sequence<CustomTokenSelectorComponent>
get() = sequenceOf(
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = UserWalletId(stringValue = "321"),
mode = mode,
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0", derivationPath = derivationPath),
name = "Ethereum",
@ -291,7 +293,7 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider :
),
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = UserWalletId(stringValue = "321"),
mode = mode,
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0", derivationPath = derivationPath),
name = "Ethereum",

View file

@ -1,31 +1,21 @@
package com.tangem.features.managetokens.utils
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
import com.tangem.domain.managetokens.FindTokenUseCase
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.AddCustomTokenForm
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.utils.list.CustomTokenFormUseCasesFacade
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveInAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class CustomCurrencyValidator @Inject constructor(
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
private val findTokenUseCase: FindTokenUseCase,
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
internal class CustomCurrencyValidator(
private val useCasesFacade: CustomTokenFormUseCasesFacade,
) {
private val validateFormJobHolder = JobHolder()
@ -45,14 +35,13 @@ internal class CustomCurrencyValidator @Inject constructor(
}
suspend fun validateForm(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
formValues: AddCustomTokenForm.Raw,
) = coroutineScope {
updateStatus(Status.Validating)
val result = validateTokenFormUseCase(
val result = useCasesFacade.validateTokenFormUseCase(
networkId = networkId,
formValues = formValues,
)
@ -73,20 +62,19 @@ internal class CustomCurrencyValidator @Inject constructor(
launch {
when (validatedForm) {
is AddCustomTokenForm.Validated.All -> {
findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm)
findOrCreateCurrency(networkId, derivationPath, validatedForm)
}
is AddCustomTokenForm.Validated.ContractAddressOnly -> {
findToken(userWalletId, networkId, derivationPath, validatedForm)
findToken(networkId, derivationPath, validatedForm)
}
is AddCustomTokenForm.Validated.Empty -> {
createCurrency(userWalletId, networkId, derivationPath, validatedForm = null)
createCurrency(networkId, derivationPath, validatedForm = null)
}
}
}.saveInAndJoin(validateFormJobHolder)
}
private suspend fun findOrCreateCurrency(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
validatedForm: AddCustomTokenForm.Validated.All,
@ -96,14 +84,13 @@ internal class CustomCurrencyValidator @Inject constructor(
currentState.prevFoundOrCreatedCurrency.contractAddress == validatedForm.contractAddress
) {
// No need to search for token again if contract address is not changed
createCurrency(userWalletId, networkId, derivationPath, validatedForm)
createCurrency(networkId, derivationPath, validatedForm)
return
}
updateStatus(Status.SearchingToken)
val foundToken = findTokenUseCase(
userWalletId = userWalletId,
val foundToken = useCasesFacade.findTokenUseCase(
contractAddress = validatedForm.contractAddress,
networkId = networkId,
derivationPath = derivationPath,
@ -121,22 +108,20 @@ internal class CustomCurrencyValidator @Inject constructor(
}
if (foundToken != null) {
updateStateToValidated(userWalletId, foundToken, fillForm = true, isCustom = false)
updateStateToValidated(foundToken, fillForm = true, isCustom = false)
} else {
createCurrency(userWalletId, networkId, derivationPath, validatedForm)
createCurrency(networkId, derivationPath, validatedForm)
}
}
private suspend fun findToken(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
validatedForm: AddCustomTokenForm.Validated.ContractAddressOnly,
) {
updateStatus(Status.SearchingToken)
val token = findTokenUseCase(
userWalletId = userWalletId,
val token = useCasesFacade.findTokenUseCase(
contractAddress = validatedForm.contractAddress,
networkId = networkId,
derivationPath = derivationPath,
@ -155,17 +140,15 @@ internal class CustomCurrencyValidator @Inject constructor(
return
}
updateStateToValidated(userWalletId, token, fillForm = true, isCustom = false)
updateStateToValidated(token, fillForm = true, isCustom = false)
}
private suspend fun createCurrency(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
validatedForm: AddCustomTokenForm.Validated.All?,
) {
val currency = createCryptoCurrencyUseCase(
userWalletId = userWalletId,
val currency = useCasesFacade.createCryptoCurrencyUseCase(
networkId = networkId,
derivationPath = derivationPath,
formValues = validatedForm,
@ -175,20 +158,14 @@ internal class CustomCurrencyValidator @Inject constructor(
return
}
updateStateToValidated(userWalletId, currency, fillForm = false, isCustom = validatedForm != null)
updateStateToValidated(currency, fillForm = false, isCustom = validatedForm != null)
}
private suspend fun updateStateToValidated(
userWalletId: UserWalletId,
currency: CryptoCurrency,
fillForm: Boolean,
isCustom: Boolean,
) {
private suspend fun updateStateToValidated(currency: CryptoCurrency, fillForm: Boolean, isCustom: Boolean) {
val currentStatus = state.value.status
if (currentStatus is Status.Validated && currentStatus.currency == currency) return
val isNotAdded = checkIsCurrencyNotAddedUseCase(
userWalletId = userWalletId,
val isNotAdded = useCasesFacade.checkIsCurrencyNotAddedUseCase(
networkId = currency.network.id,
derivationPath = currency.network.derivationPath,
contractAddress = when (currency) {

View file

@ -0,0 +1,117 @@
package com.tangem.features.managetokens.utils.list
import arrow.core.Either
import arrow.core.NonEmptyList
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
import com.tangem.domain.managetokens.FindTokenUseCase
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.AddCustomTokenForm
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.BackendId
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.features.managetokens.component.AddCustomTokenMode
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("LongParameterList")
internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
private val findTokenUseCase: FindTokenUseCase,
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
@Assisted private val mode: AddCustomTokenMode,
) {
suspend fun hasMissedDerivationsUseCase(networksWithDerivationPath: Map<BackendId, String?>): Boolean =
when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> hasMissedDerivationsUseCase.invoke(
userWalletId = mode.userWalletId,
networksWithDerivationPath = networksWithDerivationPath,
)
}
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> addCryptoCurrenciesUseCase.invoke(
userWalletId = mode.userWalletId,
currency = currency,
)
}
suspend fun derivePublicKeysUseCase(currencies: List<CryptoCurrency>): Either<Throwable, Unit> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> derivePublicKeysUseCase.invoke(
userWalletId = mode.userWalletId,
currencies = currencies,
)
}
suspend fun checkIsCurrencyNotAddedUseCase(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
contractAddress: String?,
): Either<Throwable, Boolean> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> checkIsCurrencyNotAddedUseCase.invoke(
userWalletId = mode.userWalletId,
networkId = networkId,
derivationPath = derivationPath,
contractAddress = contractAddress,
)
}
suspend fun createCryptoCurrencyUseCase(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
formValues: AddCustomTokenForm.Validated.All?,
): Either<Throwable, CryptoCurrency> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> createCryptoCurrencyUseCase.invoke(
userWalletId = mode.userWalletId,
networkId = networkId,
derivationPath = derivationPath,
formValues = formValues,
)
}
suspend fun findTokenUseCase(
contractAddress: String,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): Either<FindTokenException, CryptoCurrency.Token> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> findTokenUseCase.invoke(
userWalletId = mode.userWalletId,
contractAddress = contractAddress,
networkId = networkId,
derivationPath = derivationPath,
)
}
suspend fun validateTokenFormUseCase(
networkId: Network.ID,
formValues: AddCustomTokenForm.Raw,
): Either<NonEmptyList<CustomTokenFormValidationException>, AddCustomTokenForm.Validated> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> validateTokenFormUseCase.invoke(
networkId = networkId,
formValues = formValues,
)
}
@AssistedFactory
interface Factory {
fun create(mode: AddCustomTokenMode): CustomTokenFormUseCasesFacade
}
}

View file

@ -9,13 +9,14 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.model.*
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
@ -24,7 +25,6 @@ import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchListState
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -42,37 +42,36 @@ import timber.log.Timber
@Suppress("LongParameterList", "LargeClass")
internal class ManageTokensListManager @AssistedInject constructor(
private val getManagedTokensUseCase: GetManagedTokensUseCase,
private val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase,
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase,
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
private val messageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val clipboardManager: ClipboardManager,
manageTokensWarningDelegateFactory: ManageTokensWarningDelegate.Factory,
@Assisted private val useCasesFacade: ManageTokensUseCasesFacade,
@Assisted private val source: ManageTokensSource,
@Assisted private val mode: ManageTokensMode,
@Assisted private val scope: CoroutineScope,
@Assisted private val onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {},
) : ManageTokensUiActions {
private lateinit var scope: CoroutineScope
private lateinit var source: ManageTokensSource
private val jobHolder = JobHolder()
private val actionsFlow: MutableSharedFlow<ManageTokensBatchAction> = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val state: MutableStateFlow<ManageTokensListState> = MutableStateFlow(ManageTokensListState())
private val state: MutableStateFlow<ManageTokensListState> =
MutableStateFlow(ManageTokensListState(mode = mode))
private val manageTokensWarningDelegate: ManageTokensWarningDelegate = manageTokensWarningDelegateFactory
.create(mode, source, this)
private val changedCurrenciesManager = ChangedCurrenciesManager()
private val uiManager = ManageTokensUiManager(
state = state,
messageSender = messageSender,
manageTokensWarningDelegate = manageTokensWarningDelegate,
dispatchers = dispatchers,
actions = this,
scopeProvider = Provider { scope },
sourceProvider = Provider { source },
scope = scope,
)
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd.asStateFlow()
@ -84,62 +83,61 @@ internal class ManageTokensListManager @AssistedInject constructor(
.distinctUntilChanged()
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
suspend fun launchPagination(source: ManageTokensSource, userWalletId: UserWalletId?) = coroutineScope {
scope = this
this@ManageTokensListManager.source = source
val batchFlow = getManagedTokensUseCase(
suspend fun launchPagination() = coroutineScope {
val loadUserTokensFromRemote = when (mode) {
is ManageTokensMode.Wallet -> source == ManageTokensSource.ONBOARDING
is ManageTokensMode.Account,
ManageTokensMode.None,
-> false
}
val batchFlow = useCasesFacade.getManagedTokensUseCase(
context = ManageTokensListBatchingContext(
actionsFlow = actionsFlow,
coroutineScope = this,
),
// only for onboarding case, change carefully and check repository implementation
loadUserTokensFromRemote = userWalletId != null && source == ManageTokensSource.ONBOARDING,
loadUserTokensFromRemote = loadUserTokensFromRemote,
)
batchFlow.state
.onEach { state -> updateState(state, userWalletId) }
.onEach { state -> updateState(state) }
.flowOn(dispatchers.default)
.launchIn(scope = this)
.saveIn(jobHolder)
// Initial load
reload(userWalletId)
reload()
}
suspend fun reload(userWalletId: UserWalletId?) {
state.value = ManageTokensListState()
suspend fun reload() {
state.value = ManageTokensListState(mode = mode)
actionsFlow.emit(
BatchAction.Reload(
requestParams = ManageTokensListConfig(userWalletId, searchText = null),
requestParams = useCasesFacade.manageTokensListConfig(searchText = null),
),
)
}
suspend fun loadMore(userWalletId: UserWalletId?, query: String) {
suspend fun loadMore(query: String) {
actionsFlow.emit(
BatchAction.LoadMore(
requestParams = ManageTokensListConfig(userWalletId, query),
requestParams = useCasesFacade.manageTokensListConfig(query),
),
)
}
suspend fun search(userWalletId: UserWalletId?, query: String) {
state.value = ManageTokensListState(searchQuery = query)
suspend fun search(query: String) {
state.value = ManageTokensListState(mode = mode, searchQuery = query)
actionsFlow.emit(
BatchAction.Reload(
requestParams = ManageTokensListConfig(
userWalletId = userWalletId,
requestParams = useCasesFacade.manageTokensListConfig(
searchText = query,
),
),
)
}
private fun updateState(
batchListState: BatchListState<Int, List<ManagedCryptoCurrency>>,
userWalletId: UserWalletId?,
) {
private fun updateState(batchListState: BatchListState<Int, List<ManagedCryptoCurrency>>) {
state.update { state ->
state.copy(
status = batchListState.status,
@ -154,7 +152,6 @@ internal class ManageTokensListManager @AssistedInject constructor(
) {
state.update { state ->
state.copy(
userWalletId = userWalletId,
currencyBatches = emptyList(),
uiBatches = listOf(
Batch(
@ -170,7 +167,7 @@ internal class ManageTokensListManager @AssistedInject constructor(
scope.launch {
state.update { state ->
val newBatches = getDistinctManagedTokensUseCase(batchListState.data)
val newBatches = useCasesFacade.getDistinctManagedTokensUseCase(batchListState.data)
val currentBatches = state.currencyBatches
// Distinct until changed
@ -181,9 +178,13 @@ internal class ManageTokensListManager @AssistedInject constructor(
return@launch
}
val canEditItems = userWalletId != null
val canEditItems = when (state.mode) {
is ManageTokensMode.Account,
is ManageTokensMode.Wallet,
-> true
ManageTokensMode.None -> false
}
state.copy(
userWalletId = userWalletId,
currencyBatches = newBatches,
uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems),
canEditItems = canEditItems,
@ -216,10 +217,10 @@ internal class ManageTokensListManager @AssistedInject constructor(
sendSelectCurrencyAnalyticsEvent(currency, isSelected = false)
}
override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) {
override fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) {
scope.launch {
removeCustomCurrencyUseCase.invoke(userWalletId, currency)
.onRight { reload(userWalletId) }
useCasesFacade.removeCustomCurrencyUseCase(currency)
.onRight { reload() }
.onLeft { Timber.e(it) }
}
}
@ -258,9 +259,8 @@ internal class ManageTokensListManager @AssistedInject constructor(
actionsFlow.tryEmit(action)
}
override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean {
return checkHasLinkedTokensUseCase(
userWalletId = userWalletId,
override suspend fun checkHasLinkedTokens(network: Network): Boolean {
return useCasesFacade.checkHasLinkedTokensUseCase(
network = network,
tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value,
tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value,
@ -269,7 +269,7 @@ internal class ManageTokensListManager @AssistedInject constructor(
it,
"""
Failed to check linked tokens
|- User wallet ID: $userWalletId
|- Mode: $mode
|- Network ID: ${network.id}
""".trimIndent(),
)
@ -286,18 +286,16 @@ internal class ManageTokensListManager @AssistedInject constructor(
}
override suspend fun checkCurrencyUnsupportedState(
userWalletId: UserWalletId,
sourceNetwork: ManagedCryptoCurrency.SourceNetwork,
): CurrencyUnsupportedState? {
return checkCurrencyUnsupportedUseCase(
userWalletId = userWalletId,
return useCasesFacade.checkCurrencyUnsupportedUseCase(
sourceNetwork = sourceNetwork,
).getOrElse {
Timber.e(
it,
"""
Failed to check currency unsupported state
|- User wallet ID: $userWalletId
|- Mode: $mode
|- Source Network: $sourceNetwork
""".trimIndent(),
)
@ -359,8 +357,7 @@ internal class ManageTokensListManager @AssistedInject constructor(
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
val userWalletId = state.value.userWalletId
val unsupportedState = userWalletId?.let { checkCurrencyUnsupportedState(it, source) }
val unsupportedState = checkCurrencyUnsupportedState(source)
if (unsupportedState != null) {
showUnsupportedWarning(unsupportedState)
} else {
@ -368,7 +365,7 @@ internal class ManageTokensListManager @AssistedInject constructor(
}
} else {
if (checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
showRemoveNetworkWarning(
manageTokensWarningDelegate.showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
@ -404,67 +401,6 @@ internal class ManageTokensListManager @AssistedInject constructor(
messageSender.send(message)
}
private suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
isCoin: Boolean,
onConfirm: () -> Unit,
) {
val userWalletId = state.value.userWalletId
val hasLinkedTokens = if (userWalletId == null || !isCoin) {
false
} else {
checkHasLinkedTokens(userWalletId, network)
}
val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING
if (hasLinkedTokens) {
showLinkedTokensWarning(currency, network)
} else if (canHideWithoutConfirming) {
onConfirm()
} else {
showHideTokenWarning(currency, onConfirm)
}
}
private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_unable_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(
id = R.string.token_details_unable_hide_alert_message,
formatArgs = wrappedList(
currency.name,
currency.symbol,
network.name,
),
),
)
messageSender.send(message)
}
private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(R.string.token_details_hide_alert_message),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.token_details_hide_alert_hide),
warning = true,
onClick = onConfirm,
)
},
secondActionBuilder = { cancelAction() },
)
messageSender.send(message)
}
private fun Batch<Int, List<ManagedCryptoCurrency>>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int {
return data
.indexOfFirst { it.id == id }
@ -474,6 +410,12 @@ internal class ManageTokensListManager @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {}): ManageTokensListManager
fun create(
scope: CoroutineScope,
mode: ManageTokensMode,
source: ManageTokensSource,
useCasesFacade: ManageTokensUseCasesFacade,
onCurrencySelect: (ManagedCryptoCurrency.Token) -> Unit = {},
): ManageTokensListManager
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.ManageTokensListConfig
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
@ -13,7 +13,7 @@ internal typealias ManageTokensBatchAction = BatchAction<Int, ManageTokensListCo
internal data class ManageTokensListState(
val status: PaginationStatus<*> = PaginationStatus.None,
val userWalletId: UserWalletId? = null,
val mode: ManageTokensMode,
val uiBatches: List<Batch<Int, List<CurrencyItemUM>>> = mutableListOf(),
val currencyBatches: List<Batch<Int, List<ManagedCryptoCurrency>>> = mutableListOf(),
val canEditItems: Boolean = true,

View file

@ -3,7 +3,6 @@ package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
internal interface ManageTokensUiActions {
@ -13,14 +12,13 @@ internal interface ManageTokensUiActions {
fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom)
fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom)
fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean
suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean
suspend fun checkHasLinkedTokens(network: Network): Boolean
suspend fun checkCurrencyUnsupportedState(
userWalletId: UserWalletId,
sourceNetwork: ManagedCryptoCurrency.SourceNetwork,
): CurrencyUnsupportedState?
}

View file

@ -1,19 +1,10 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.mapper.toUiModel
import com.tangem.features.managetokens.utils.ui.update
import com.tangem.pagination.Batch
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.collections.immutable.ImmutableList
@ -29,19 +20,12 @@ import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class ManageTokensUiManager(
private val state: MutableStateFlow<ManageTokensListState>,
private val messageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val scopeProvider: Provider<CoroutineScope>,
private val sourceProvider: Provider<ManageTokensSource>,
private val scope: CoroutineScope,
private val actions: ManageTokensUiActions,
private val manageTokensWarningDelegate: ManageTokensWarningDelegate,
) {
private val scope: CoroutineScope
get() = scopeProvider()
private val source: ManageTokensSource
get() = sourceProvider()
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<CurrencyItemUM>> = state
.mapLatest { state ->
@ -109,75 +93,11 @@ internal class ManageTokensUiManager(
}
private fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) = scope.launch(dispatchers.default) {
showRemoveNetworkWarning(
manageTokensWarningDelegate.showRemoveNetworkWarning(
currency = currency,
network = currency.network,
isCoin = currency is ManagedCryptoCurrency.Custom.Coin,
onConfirm = {
val userWalletId = requireNotNull(state.value.userWalletId) { "UserWalletId is null. Can not remove" }
actions.removeCustomCurrency(userWalletId = userWalletId, currency = currency)
},
onConfirm = { actions.removeCustomCurrency(currency = currency) },
)
}
private suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
isCoin: Boolean,
onConfirm: () -> Unit,
) {
val userWalletId = state.value.userWalletId
val hasLinkedTokens = if (userWalletId == null || !isCoin) {
false
} else {
actions.checkHasLinkedTokens(userWalletId, network)
}
val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING
if (hasLinkedTokens) {
showLinkedTokensWarning(currency, network)
} else if (canHideWithoutConfirming) {
onConfirm()
} else {
showHideTokenWarning(currency, onConfirm)
}
}
private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_unable_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(
id = R.string.token_details_unable_hide_alert_message,
formatArgs = wrappedList(
currency.name,
currency.symbol,
network.name,
),
),
)
messageSender.send(message)
}
private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(R.string.token_details_hide_alert_message),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.token_details_hide_alert_hide),
warning = true,
onClick = onConfirm,
)
},
secondActionBuilder = { cancelAction() },
)
messageSender.send(message)
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.features.managetokens.utils.list
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManageTokensListConfig
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.features.managetokens.component.ManageTokensMode
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("LongParameterList")
internal class ManageTokensUseCasesFacade @AssistedInject constructor(
val getManagedTokensUseCase: GetManagedTokensUseCase,
val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase,
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase,
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
@Assisted private val mode: ManageTokensMode,
) {
private val nonePortfolioError: IllegalStateException
get() = IllegalStateException("Unsupported")
fun manageTokensListConfig(searchText: String?): ManageTokensListConfig {
val userWalletId: UserWalletId? = when (mode) {
is ManageTokensMode.Account -> TODO("Account")
ManageTokensMode.None -> null
is ManageTokensMode.Wallet -> mode.userWalletId
}
return ManageTokensListConfig(userWalletId, searchText)
}
suspend fun removeCustomCurrencyUseCase(customCurrency: ManagedCryptoCurrency.Custom): Either<Throwable, Unit> {
return when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Wallet -> removeCustomCurrencyUseCase.invoke(
userWalletId = mode.userWalletId,
customCurrency = customCurrency,
)
ManageTokensMode.None -> nonePortfolioError.left()
}
}
suspend fun checkHasLinkedTokensUseCase(
network: Network,
tempAddedTokens: Map<ManagedCryptoCurrency.Token, Set<Network>>,
tempRemovedTokens: Map<ManagedCryptoCurrency.Token, Set<Network>>,
): Either<Throwable, Boolean> {
return when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Wallet -> checkHasLinkedTokensUseCase.invoke(
userWalletId = mode.userWalletId,
network = network,
tempAddedTokens = tempAddedTokens,
tempRemovedTokens = tempRemovedTokens,
)
ManageTokensMode.None -> nonePortfolioError.left()
}
}
suspend fun checkCurrencyUnsupportedUseCase(
sourceNetwork: ManagedCryptoCurrency.SourceNetwork,
): Either<Throwable, CurrencyUnsupportedState?> {
return when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Wallet -> checkCurrencyUnsupportedUseCase.invoke(
userWalletId = mode.userWalletId,
sourceNetwork = sourceNetwork,
)
ManageTokensMode.None -> nonePortfolioError.left()
}
}
suspend fun hasMissedDerivationsUseCase(network: Map<String, Nothing?>): Boolean = when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Wallet -> hasMissedDerivationsUseCase.invoke(
userWalletId = mode.userWalletId,
networksWithDerivationPath = network,
)
ManageTokensMode.None -> false
}
suspend fun saveManagedTokensUseCase(
currenciesToAdd: Map<ManagedCryptoCurrency.Token, Set<Network>>,
currenciesToRemove: Map<ManagedCryptoCurrency.Token, Set<Network>>,
): Either<Throwable, Unit> = when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Wallet -> saveManagedTokensUseCase.invoke(
userWalletId = mode.userWalletId,
currenciesToAdd = currenciesToAdd,
currenciesToRemove = currenciesToRemove,
)
ManageTokensMode.None -> nonePortfolioError.left()
}
@AssistedFactory
interface Factory {
fun create(mode: ManageTokensMode): ManageTokensUseCasesFacade
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.features.managetokens.utils.list
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.impl.R
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class ManageTokensWarningDelegate @AssistedInject constructor(
private val messageSender: UiMessageSender,
@Assisted private val mode: ManageTokensMode,
@Assisted private val source: ManageTokensSource,
@Assisted private val uiActions: ManageTokensUiActions,
) {
suspend fun showRemoveNetworkWarning(
currency: ManagedCryptoCurrency,
network: Network,
isCoin: Boolean,
onConfirm: () -> Unit,
) {
val isNonePortfolio = when (mode) {
ManageTokensMode.None -> true
is ManageTokensMode.Account,
is ManageTokensMode.Wallet,
-> false
}
val hasLinkedTokens = if (isNonePortfolio || !isCoin) {
false
} else {
uiActions.checkHasLinkedTokens(network)
}
val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING
if (hasLinkedTokens) {
showLinkedTokensWarning(currency, network)
} else if (canHideWithoutConfirming) {
onConfirm()
} else {
showHideTokenWarning(currency, onConfirm)
}
}
private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_unable_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(
id = R.string.token_details_unable_hide_alert_message,
formatArgs = wrappedList(
currency.name,
currency.symbol,
network.name,
),
),
)
messageSender.send(message)
}
private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) {
val message = DialogMessage(
title = resourceReference(
id = R.string.token_details_hide_alert_title,
formatArgs = wrappedList(currency.name),
),
message = resourceReference(R.string.token_details_hide_alert_message),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.token_details_hide_alert_hide),
warning = true,
onClick = onConfirm,
)
},
secondActionBuilder = { cancelAction() },
)
messageSender.send(message)
}
@AssistedFactory
interface Factory {
fun create(
mode: ManageTokensMode,
source: ManageTokensSource,
uiActions: ManageTokensUiActions,
): ManageTokensWarningDelegate
}
}

View file

@ -17,6 +17,7 @@ dependencies {
api(projects.features.onramp.api)
api(projects.features.sendV2.api)
api(projects.features.tokenRecieve.api)
api(projects.features.wallet.api)
/* Data */
implementation(projects.data.common)

View file

@ -7,7 +7,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -56,7 +55,7 @@ internal class AddToPortfolioBSContentUMFactory(
portfolioUIData: PortfolioUIData,
selectedWallet: UserWallet?,
alreadyAddedNetworks: Set<String>?,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
return (currentState ?: TangemBottomSheetConfig.Empty).copy(
isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility,
@ -110,7 +109,7 @@ internal class AddToPortfolioBSContentUMFactory(
}
private fun UserWallet.toSelectedUserWalletItemUM(
artwork: ArtworkModel? = null,
artwork: UserWalletItemUM.ImageState? = null,
portfolioData: PortfolioData,
balance: TotalFiatBalance?,
): UserWalletItemUM {
@ -128,7 +127,7 @@ internal class AddToPortfolioBSContentUMFactory(
isShow: Boolean,
portfolioData: PortfolioData,
selectedWalletId: UserWalletId,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
return TangemBottomSheetConfig(
isShown = isShow,

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -21,7 +22,6 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.markets.SaveMarketTokensUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
@ -32,7 +32,6 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.features.markets.impl.R
@ -43,13 +42,14 @@ import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
@ -67,21 +67,17 @@ internal class MarketsPortfolioModel @Inject constructor(
private val portfolioDataLoader: PortfolioDataLoader,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val getCardImageUseCase: GetCardImageUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getEnsNameUseCase: GetEnsNameUseCase,
private val userWalletImageFetcher: UserWalletImageFetcher,
) : Model() {
val state: StateFlow<MyPortfolioUM> get() = _state
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
private val loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
private val artworksState: MutableStateFlow<HashMap<UserWalletId, ArtworkModel>> = MutableStateFlow(hashMapOf())
private val loadArtworksMutex = Mutex()
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = params.token,
@ -197,38 +193,33 @@ internal class MarketsPortfolioModel @Inject constructor(
private fun subscribeOnStateUpdates() {
combine(
flow = loadPortfolioData(params.token.id),
flow = loadPortfolioDataWithArtworks(params.token.id),
flow2 = getPortfolioUIDataFlow(),
flow3 = artworksState,
transform = factory::create,
transform = { pair, portfolioUIData ->
val (portfolioData, artworks) = pair
factory.create(portfolioData, portfolioUIData, artworks)
},
)
.onEach { _state.value = it }
.launchIn(modelScope)
}
private fun loadPortfolioData(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
portfolioDataLoader.load(currencyRawId).onEach {
loadArtworks(it.walletsWithCurrencies.keys.toList())
}.also { return it }
}
private fun loadPortfolioDataWithArtworks(
currencyRawId: CryptoCurrency.RawID,
): Flow<Pair<PortfolioData, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
val wallets = Channel<Set<UserWallet>>()
val portfolioFlow = portfolioDataLoader
.load(currencyRawId)
.onEach { wallets.trySend(it.walletsWithCurrencies.keys) }
private fun loadArtworks(wallets: List<UserWallet>) {
modelScope.launch {
loadArtworksMutex.withLock {
wallets.filterIsInstance<UserWallet.Cold>().forEach { wallet ->
if (!loadedArtworks.containsKey(wallet.walletId)) {
val artwork = getCardImageUseCase(
cardId = wallet.cardId,
manufacturerName = wallet.scanResponse.card.manufacturer.name,
firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(),
cardPublicKey = wallet.scanResponse.card.cardPublicKey,
)
loadedArtworks[wallet.walletId] = artwork
artworksState.emit(loadedArtworks)
}
}
}
}
val artworksFlow = wallets.receiveAsFlow()
.distinctUntilChanged()
.flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
return combine(
flow = portfolioFlow,
flow2 = artworksFlow,
) { portfolioData, artworks -> portfolioData to artworks }
}
private fun getPortfolioUIDataFlow(): Flow<PortfolioUIData> {

View file

@ -1,8 +1,8 @@
package com.tangem.features.markets.portfolio.impl.model
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -36,7 +36,7 @@ internal class MyPortfolioUMFactory(
fun create(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): MyPortfolioUM {
val addToPortfolioData = portfolioUIData.addToPortfolioData
@ -89,7 +89,7 @@ internal class MyPortfolioUMFactory(
private fun createAddToPortfolioBSConfig(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: HashMap<UserWalletId, ArtworkModel>,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
val selectedWallet = portfolioData.walletsWithCurrencies.keys
.firstOrNull { it.walletId == portfolioUIData.selectedWalletId }

View file

@ -13,12 +13,14 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
@ -46,6 +48,8 @@ internal class OnboardingEntryModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
private val userWalletsListManager: UserWalletsListManager,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
) : Model() {
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
@ -145,7 +149,7 @@ internal class OnboardingEntryModel @Inject constructor(
doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated,
) {
modelScope.launch {
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowAskBiometry()) {
doIfVisa {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened)
}
@ -197,6 +201,19 @@ internal class OnboardingEntryModel @Inject constructor(
}
private fun exitComponentScreen() {
// new flow
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
if (userWalletsListRepository.userWalletsSync().isEmpty()) {
router.replaceAll(AppRoute.Home())
} else {
router.replaceAll(AppRoute.Wallet)
}
}
return
}
// legacy flow
if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
@ -39,7 +39,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val cardRepository: CardRepository,
private val analyticsHandler: AnalyticsEventHandler,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
@ -168,7 +168,8 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO
@ -19,8 +19,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateWalletUseCase
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
@ -37,7 +37,10 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.StringsSigns
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -49,11 +52,11 @@ internal class MultiWalletFinalizeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
private val tangemSdkManager: TangemSdkManager,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val userWalletsListManager: UserWalletsListManager,
private val saveWalletUseCase: SaveWalletUseCase,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val updateWalletUseCase: UpdateWalletUseCase,
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
@ -244,7 +247,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
userWalletCreated
}
OnboardingMultiWalletComponent.Mode.AddBackup -> {
val userWallet = userWalletsListManager.userWallets.first()
val userWallet = getUserWalletsUseCase.invokeSync()
.firstOrNull {
it is UserWallet.Cold &&
it.scanResponse.primaryCard?.cardId == scanResponse.primaryCard?.cardId
@ -333,7 +336,8 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}

View file

@ -10,7 +10,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.hotwallet.MnemonicRepository
@ -44,7 +44,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
private val urlOpener: UrlOpener,
private val tangemSdkManager: TangemSdkManager,
private val cardRepository: CardRepository,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
@ -257,7 +257,8 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
@ -20,10 +21,10 @@ internal class GenerateSeedPhraseUiStateBuilder(
option: GeneratedWordsType,
): MultiWalletSeedPhraseUM.GenerateSeedPhrase {
val words12 = generatedWords12.mnemonicComponents.mapIndexed { index, s ->
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s)
EnumeratedTwoColumnGridItem(index + 1, s)
}.toImmutableList()
val words24 = generatedWords24.mnemonicComponents.mapIndexed { index, s ->
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s)
EnumeratedTwoColumnGridItem(index + 1, s)
}.toImmutableList()
return MultiWalletSeedPhraseUM.GenerateSeedPhrase(
@ -45,8 +46,8 @@ internal class GenerateSeedPhraseUiStateBuilder(
private fun switchType(
newType: GeneratedWordsType,
generatedWords12: ImmutableList<MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem>,
generatedWords24: ImmutableList<MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem>,
generatedWords12: ImmutableList<EnumeratedTwoColumnGridItem>,
generatedWords24: ImmutableList<EnumeratedTwoColumnGridItem>,
) {
updateUiState { uiSt ->
changeGeneratedWordsType(newType)

View file

@ -5,15 +5,14 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -21,8 +20,6 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -45,8 +42,8 @@ internal fun MultiWalletSeedPhraseWords(
TitleBlock(state)
SeedPhraseGridBlock(
mnemonicGridItems = state.words,
EnumeratedTwoColumnGrid(
items = state.words,
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 32.dp),
@ -127,67 +124,6 @@ private fun TitleBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifi
}
}
@Composable
private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList<MnemonicGridItem>, modifier: Modifier = Modifier) {
VerticalGrid(
modifier = modifier,
items = mnemonicGridItems,
) { item ->
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
verticalAlignment = Alignment.CenterVertically,
) {
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
} else {
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}
@Composable
private inline fun <T> VerticalGrid(
items: ImmutableList<T>,
modifier: Modifier = Modifier,
crossinline content: @Composable (T) -> Unit,
) {
val columnLength = items.size / 2
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
repeat(2) { index ->
Column {
for (i in 0 until columnLength) {
val item = items[index * columnLength + i]
content(item)
}
}
}
}
}
@Preview(showBackground = true, heightDp = 640)
@Composable
private fun Preview() {
@ -195,7 +131,7 @@ private fun Preview() {
MultiWalletSeedPhraseWords(
state = MultiWalletSeedPhraseUM.GenerateSeedPhrase(
words = List(24) {
MnemonicGridItem(
EnumeratedTwoColumnGridItem(
index = it + 1,
mnemonic = "word1",
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.s
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
@ -22,15 +23,10 @@ internal sealed class MultiWalletSeedPhraseUM(
data class GenerateSeedPhrase(
val option: GeneratedWordsType = GeneratedWordsType.Words12,
val words: ImmutableList<MnemonicGridItem> = persistentListOf(),
val words: ImmutableList<EnumeratedTwoColumnGridItem> = persistentListOf(),
val onOptionChange: (GeneratedWordsType) -> Unit = {},
val onContinueClick: () -> Unit = {},
) : MultiWalletSeedPhraseUM(order = 1) {
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,
)
}
) : MultiWalletSeedPhraseUM(order = 1)
data class GeneratedWordsCheck(
val wordFields: ImmutableList<WordField> = persistentListOf(),

View file

@ -10,7 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
@Assisted val context: AppComponentContext,
@Assisted val params: OnboardingStepperComponent.Params,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsHandler: AnalyticsEventHandler,
) : OnboardingStepperComponent, AppComponentContext by context {
@ -40,7 +40,7 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
)
componentScope.launch {
val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(
if (params.scanResponse.card.isVisa) {
FeedbackEmailType.Visa.Activation(cardInfo)

View file

@ -18,7 +18,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
@ -44,7 +44,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
@Suppress("UnusedPrivateMember")
private val tangemSdkManager: TangemSdkManager,
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val uiMessageSender: UiMessageSender,
private val analyticsEventsHandler: AnalyticsEventHandler,
) : Model() {
@ -155,7 +155,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
loading(true)
modelScope.launch {
val challengeToSign = visaAuthRepository.getCardAuthChallenge(
val challengeToSign = visaAuthRemoteDataSource.getCardAuthChallenge(
cardId = activationInput.cardId,
cardPublicKey = activationInput.cardPublicKey,
).getOrElse {

View file

@ -18,7 +18,7 @@ import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
@ -42,7 +42,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
paramsContainer: ParamsContainer,
visaActivationRepositoryFactory: VisaActivationRepository.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val otpStorage: VisaOTPStorage,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
@ -166,7 +166,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
val authTokens = visaAuthTokenStorage.get(params.scanResponse.card.cardId)
?: error("Auth tokens are not found. This should not happen.")
val newTokens = visaAuthRepository.exchangeAccessToken(authTokens)
val newTokens = visaAuthRemoteDataSource.exchangeAccessToken(authTokens)
.getOrElse {
uiMessageSender.showErrorDialog(it)
return

View file

@ -8,7 +8,12 @@ import com.tangem.domain.models.wallet.UserWalletId
interface OnrampComponent : ComposableContentComponent {
data class Params(val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, val source: OnrampSource)
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val source: OnrampSource,
val launchSepa: Boolean = false,
)
interface Factory : ComponentFactory<Params, OnrampComponent>
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.alloffers
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
internal interface AllOffersComponent : ComposableBottomSheetComponent {
data class Params(
val userWallet: UserWallet,
val cryptoCurrency: CryptoCurrency,
val onDismiss: () -> Unit,
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
val amountCurrencyCode: String,
)
interface Factory : ComponentFactory<Params, AllOffersComponent>
}

View file

@ -0,0 +1,38 @@
package com.tangem.features.onramp.alloffers
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.alloffers.model.AllOffersModel
import com.tangem.features.onramp.alloffers.ui.AllOffersContentSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAllOffersComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: AllOffersComponent.Params,
) : AllOffersComponent, AppComponentContext by context {
private val model: AllOffersModel = getOrCreateModel(params)
override fun dismiss() {
model.dismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsState()
AllOffersContentSheet(
state = state,
onCloseClick = { dismiss() },
)
}
@AssistedFactory
interface Factory : AllOffersComponent.Factory {
override fun create(context: AppComponentContext, params: AllOffersComponent.Params): DefaultAllOffersComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.alloffers.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.alloffers.model.AllOffersModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface AllOffersComponentModelModule {
@Binds
@IntoMap
@ClassKey(AllOffersModel::class)
fun bindAllOffersModel(model: AllOffersModel): Model
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.onramp.alloffers.di
import com.tangem.features.onramp.alloffers.AllOffersComponent
import com.tangem.features.onramp.alloffers.DefaultAllOffersComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AllOffersComponentModule {
@Binds
@Singleton
fun bindAllOffersComponentFactory(factory: DefaultAllOffersComponent.Factory): AllOffersComponent.Factory
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.onramp.alloffers.entity
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
internal interface AllOffersIntents {
fun onPaymentMethodClicked(paymentMethodId: String)
fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM)
fun onBackClicked()
fun onRefresh()
}

View file

@ -0,0 +1,159 @@
package com.tangem.features.onramp.alloffers.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.MINUS
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class AllOffersStateFactory(
private val analyticsEventHandler: AnalyticsEventHandler,
private val currentStateProvider: Provider<AllOffersStateUM>,
private val allOffersIntents: AllOffersIntents,
) {
fun getLoadedPaymentsState(methodGroups: List<OnrampPaymentMethodGroup>): AllOffersStateUM {
return AllOffersStateUM.Content(
methods = methodGroups.map { methodGroup ->
AllOffersPaymentMethodUM(
offers = mapOffersToUM(methodGroup.offers).toPersistentList(),
methodConfig = OnrampPaymentMethodConfig(
method = methodGroup.paymentMethod,
onClick = { allOffersIntents.onPaymentMethodClicked(methodGroup.paymentMethod.id) },
),
diff = methodGroup
.bestRateOffer
?.rateDif
?.takeIf { it > BigDecimal.ZERO }
?.let { diff ->
stringReference("$MINUS${diff.format { percent() }}")
},
rate = methodGroup.bestRateOffer?.let { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value.format {
crypto(
symbol = quote.toAmount.symbol,
decimals = quote.toAmount.decimals,
)
}
else -> ""
}
} ?: "",
providersCount = methodGroup.providerCount,
isBestRate = methodGroup.isBestPaymentMethod,
)
}.toPersistentList(),
currentMethod = null,
onBackClicked = { allOffersIntents.onBackClicked() },
)
}
fun getPaymentsState(): AllOffersStateUM {
return when (val currentState = currentStateProvider.invoke()) {
is AllOffersStateUM.Content -> {
analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened)
currentState.copy(currentMethod = null)
}
AllOffersStateUM.Loading,
is AllOffersStateUM.Error,
-> currentState
}
}
fun getOnrampErrorState(onrampError: OnrampError): AllOffersStateUM {
return when (onrampError) {
is OnrampError.DataError -> getErrorState(
errorCode = onrampError.code,
onRefresh = allOffersIntents::onRefresh,
)
OnrampError.PairsNotFound,
is OnrampError.DomainError,
-> getErrorState(onRefresh = allOffersIntents::onRefresh)
is OnrampError.AmountError.TooBigError,
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
OnrampError.RedirectError.WrongRequestId,
-> currentStateProvider()
}
}
private fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): AllOffersStateUM {
val state = currentStateProvider()
return when (state) {
is AllOffersStateUM.Content,
AllOffersStateUM.Loading,
-> AllOffersStateUM.Error(
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
errorCode = errorCode,
onRefresh = onRefresh,
),
)
is AllOffersStateUM.Error -> state
}
}
private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM {
return when (advantages) {
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
}
}
private fun mapOffersToUM(offers: List<OnrampOffer>): List<OnrampOfferUM> {
return buildList {
offers.forEach { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> {
add(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
paymentMethod = quote.paymentMethod,
providerId = quote.provider.id,
providerName = quote.provider.info.name,
rate = quote.toAmount.value.format {
crypto(
symbol = quote.toAmount.symbol,
decimals = quote.toAmount.decimals,
)
},
diff = offer
.rateDif
?.takeIf { it > BigDecimal.ZERO }
?.let { diff ->
stringReference("$MINUS${diff.format { percent() }}")
},
onBuyClicked = {
allOffersIntents.onBuyClick(
quote = OnrampProviderWithQuote.Data(
provider = quote.provider,
paymentMethod = quote.paymentMethod,
toAmount = quote.toAmount,
fromAmount = quote.fromAmount,
),
onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages),
)
},
),
)
}
is OnrampQuote.AmountError,
is OnrampQuote.Error,
-> Unit
}
}
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.onramp.alloffers.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import kotlinx.collections.immutable.ImmutableList
internal sealed interface AllOffersStateUM {
data object Loading : AllOffersStateUM
data class Content(
val methods: ImmutableList<AllOffersPaymentMethodUM>,
val currentMethod: AllOffersPaymentMethodUM? = null,
val onBackClicked: () -> Unit,
) : AllOffersStateUM
data class Error(val errorNotification: NotificationUM) : AllOffersStateUM
}
internal data class AllOffersPaymentMethodUM(
val offers: ImmutableList<OnrampOfferUM>,
val methodConfig: OnrampPaymentMethodConfig,
val diff: TextReference?,
val rate: String,
val providersCount: Int,
val isBestRate: Boolean,
)
internal data class OnrampPaymentMethodConfig(
val method: OnrampPaymentMethod,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,109 @@
package com.tangem.features.onramp.alloffers.model
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.onramp.GetOnrampAllOffersUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.alloffers.AllOffersComponent
import com.tangem.features.onramp.alloffers.entity.AllOffersIntents
import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
internal class AllOffersModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getOnrampAllOffersUseCase: GetOnrampAllOffersUseCase,
paramsContainer: ParamsContainer,
) : Model(), AllOffersIntents {
private var quotesJob: Job? = null
private val stateFactory: AllOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
AllOffersStateFactory(
analyticsEventHandler = analyticsEventHandler,
currentStateProvider = Provider { state.value },
allOffersIntents = this,
)
}
private val params: AllOffersComponent.Params = paramsContainer.require()
private val _state: MutableStateFlow<AllOffersStateUM> = MutableStateFlow(AllOffersStateUM.Loading)
val state: StateFlow<AllOffersStateUM> = _state.asStateFlow()
init {
subscribeOnAllOffers()
analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened)
analyticsEventHandler.send(OnrampAnalyticsEvent.AllOffersClicked)
}
fun dismiss() {
params.onDismiss()
}
override fun onPaymentMethodClicked(paymentMethodId: String) {
val contentState = state.value as? AllOffersStateUM.Content ?: return
val method = contentState.methods.firstOrNull { it.methodConfig.method.id == paymentMethodId } ?: return
analyticsEventHandler.send(
event = OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = method.methodConfig.method.name),
)
_state.update { contentState.copy(currentMethod = method) }
}
override fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) {
analyticsEventHandler.send(
OnrampAnalyticsEvent.OnBuyClick(
providerName = quote.provider.info.name,
currency = params.amountCurrencyCode,
tokenSymbol = params.cryptoCurrency.symbol,
),
)
onrampOfferAdvantagesUM.toAnalyticsEvent(
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
providerName = quote.provider.info.name,
paymentMethodName = quote.paymentMethod.name,
)?.let { analyticsEventHandler::send }
params.openRedirectPage(quote)
}
override fun onBackClicked() {
_state.update { stateFactory.getPaymentsState() }
}
override fun onRefresh() {
subscribeOnAllOffers()
}
private fun subscribeOnAllOffers() {
quotesJob?.cancel()
quotesJob = modelScope.launch(dispatchers.default) {
getOnrampAllOffersUseCase.invoke(
userWalletId = params.userWallet.walletId,
cryptoCurrencyId = params.cryptoCurrency.id,
).collectLatest { maybeOffers ->
maybeOffers.fold(
ifLeft = ::handleOnrampError,
ifRight = { offersGroup ->
_state.update { stateFactory.getLoadedPaymentsState(offersGroup) }
},
)
}
}
}
private fun handleOnrampError(onrampError: OnrampError) {
Timber.e(onrampError.toString())
_state.update { stateFactory.getOnrampErrorState(onrampError) }
}
}

View file

@ -0,0 +1,322 @@
package com.tangem.features.onramp.alloffers.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
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.components.notifications.Notification
import com.tangem.core.ui.extensions.TextReference
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.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import com.tangem.features.onramp.mainv2.ui.Offer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@Composable
internal fun AllOffersContentSheet(state: AllOffersStateUM, onCloseClick: () -> Unit) {
val onBack = remember(state) {
{
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
state.onBackClicked()
} else {
onCloseClick()
}
}
}
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onCloseClick,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = onBack,
containerColor = TangemTheme.colors.background.tertiary,
title = {
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
ProviderTitle(
onCloseClick = onCloseClick,
onBackClick = onBack,
)
} else {
PaymentMethodTitle(onCloseClick = onCloseClick)
}
},
content = {
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
.animateContentSize(),
) {
AnimatedContent(
targetState = state is AllOffersStateUM.Content && state.currentMethod != null,
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
label = "Change offers and payment method state",
) { shouldShowOffersScreen ->
when (state) {
AllOffersStateUM.Loading -> AllOffersContentLoading()
is AllOffersStateUM.Error -> AllOffersError(state.errorNotification)
is AllOffersStateUM.Content -> {
if (shouldShowOffersScreen) {
state.currentMethod?.let {
OffersBasedOnPaymentMethodContent(offers = it.offers)
}
} else {
PaymentMethodsContent(methods = state.methods)
}
}
}
}
}
},
)
}
@Composable
private fun ProviderTitle(onBackClick: () -> Unit, onCloseClick: () -> Unit) {
TangemModalBottomSheetTitle(
title = TextReference.Res(R.string.onramp_all_offers_button_title),
subtitle = TextReference.Res(R.string.express_choose_providers_title),
startIconRes = R.drawable.ic_back_24,
onStartClick = onBackClick,
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
onEndClick = onCloseClick,
)
}
@Composable
private fun PaymentMethodTitle(onCloseClick: () -> Unit) {
TangemModalBottomSheetTitle(
title = TextReference.Res(R.string.onramp_all_offers_button_title),
subtitle = TextReference.Res(R.string.onramp_payment_method_subtitle),
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
onEndClick = onCloseClick,
)
}
@Composable
private fun OffersBasedOnPaymentMethodContent(offers: ImmutableList<OnrampOfferUM>) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
offers.fastForEach { offer ->
key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") {
Offer(offer)
SpacerH(8.dp)
}
}
}
}
@Composable
private fun AllOffersContentLoading() {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.tertiary)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RectangleShimmer(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.height(96.dp)
.size(width = 76.dp, height = 20.dp),
radius = 14.dp,
)
RectangleShimmer(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.height(96.dp)
.size(width = 76.dp, height = 20.dp),
radius = 14.dp,
)
RectangleShimmer(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.height(96.dp)
.size(width = 76.dp, height = 20.dp),
radius = 14.dp,
)
}
}
@Composable
fun AllOffersError(errorNotification: NotificationUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Notification(config = errorNotification.config)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AllOffersContentSheetPaymentPreview() {
val method = AllOffersPaymentMethodUM(
offers = persistentListOf(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.BestRate,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId1",
providerName = "Simplex",
rate = "0,0245334 BTC",
diff = null,
onBuyClicked = {},
),
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.Default,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId2",
providerName = "Simplex",
rate = "0,00145334 BTC",
diff = stringReference("0.07%"),
onBuyClicked = {},
),
),
methodConfig = OnrampPaymentMethodConfig(
method = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
onClick = {},
),
diff = null,
rate = "0,0245334 BTC",
providersCount = 2,
isBestRate = true,
)
TangemThemePreview {
AllOffersContentSheet(
state = AllOffersStateUM.Content(
methods = persistentListOf(),
currentMethod = method,
onBackClicked = {},
),
onCloseClick = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AllOffersContentSheetOffersPreview() {
val methods = List(5) {
AllOffersPaymentMethodUM(
offers = persistentListOf(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.BestRate,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl =
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId1",
providerName = "Simplex",
rate = "0,0245334 BTC",
diff = null,
onBuyClicked = {},
),
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.Default,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl =
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId2",
providerName = "Simplex",
rate = "0,00145334 BTC",
diff = stringReference("0.07%"),
onBuyClicked = {},
),
),
methodConfig = OnrampPaymentMethodConfig(
method = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
onClick = {},
),
diff = null,
rate = "0,0245334 BTC",
providersCount = 2,
isBestRate = true,
)
}
TangemThemePreview {
AllOffersContentSheet(
state = AllOffersStateUM.Content(
methods = methods.toPersistentList(),
currentMethod = null,
onBackClicked = {},
),
onCloseClick = {},
)
}
}

View file

@ -0,0 +1,262 @@
package com.tangem.features.onramp.alloffers.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import com.tangem.features.onramp.mainv2.ui.TimingBlock
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PaymentMethodsContent(methods: ImmutableList<AllOffersPaymentMethodUM>) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
methods.fastForEach { method ->
key(method.methodConfig.method.id) {
PaymentMethod(methodUM = method)
SpacerH(8.dp)
}
}
}
}
@Composable
private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = RoundedCornerShape(14.dp),
)
.clickable(
indication = ripple(),
interactionSource = remember { MutableInteractionSource() },
onClick = methodUM.methodConfig.onClick,
)
.padding(
start = 12.dp,
end = 12.dp,
top = 14.dp,
bottom = 12.dp,
),
) {
PaymentMethodIcon(
modifier = Modifier.size(36.dp),
imageUrl = methodUM.methodConfig.method.imageUrl,
)
SpacerW(12.dp)
Column {
PaymentMethodInfoBlock(
paymentMethodName = methodUM.methodConfig.method.name,
rate = methodUM.rate,
diff = methodUM.diff,
isBestRate = methodUM.isBestRate,
)
Row(verticalAlignment = Alignment.CenterVertically) {
ProvidersCountBlockInfo(providersCount = methodUM.providersCount)
SpacerW(8.dp)
TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed())
}
}
}
}
@Composable
private fun PaymentMethodInfoBlock(
paymentMethodName: String,
rate: String,
diff: TextReference?,
isBestRate: Boolean,
) {
Column(modifier = Modifier.padding(bottom = 14.dp)) {
Text(
text = paymentMethodName,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
SpacerH(2.dp)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = stringResourceSafe(R.string.onramp_up_to_rate),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = rate,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
when {
isBestRate -> {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_12),
contentDescription = null,
)
}
diff != null -> {
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.text.warning.copy(alpha = 0.1f),
shape = RoundedCornerShape(4.dp),
)
.padding(horizontal = 4.dp),
text = diff.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.warning,
)
}
}
}
}
}
@Composable
private fun ProvidersCountBlockInfo(providersCount: Int) {
BorderedRow {
Icon(
modifier = Modifier.size(10.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerW(4.dp)
Text(
text = pluralStringResourceSafe(
id = R.plurals.onramp_providers_count,
count = providersCount,
providersCount,
),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun TimingBlockInfo(speed: PaymentMethodType.PaymentSpeed) {
BorderedRow {
Icon(
modifier = Modifier.size(10.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerW(4.dp)
TimingBlock(speed)
}
}
@Composable
fun BorderedRow(content: @Composable RowScope.() -> Unit) {
Row(
modifier = Modifier
.border(
width = 1.dp,
color = TangemTheme.colors.stroke.primary,
shape = RoundedCornerShape(6.dp),
)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
}
@Preview
@Composable
private fun PaymentMethodsContentPreview() {
val method = AllOffersPaymentMethodUM(
offers = persistentListOf(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.BestRate,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId1",
providerName = "Simplex",
rate = "0,0245334 BTC",
diff = null,
onBuyClicked = {},
),
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.Default,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId2",
providerName = "Simplex",
rate = "0,00145334 BTC",
diff = stringReference("0.07%"),
onBuyClicked = {},
),
),
methodConfig = OnrampPaymentMethodConfig(
method = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
onClick = {},
),
diff = null,
rate = "0,0245334 BTC",
providersCount = 2,
isBestRate = true,
)
TangemThemePreview {
PaymentMethodsContent(persistentListOf(method))
}
}

View file

@ -15,6 +15,7 @@ internal interface OnrampMainComponent : ComposableContentComponent {
val source: OnrampSource,
val openSettings: () -> Unit,
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
val launchSepa: Boolean,
)
interface Factory : ComponentFactory<Params, OnrampMainComponent>

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