Updated on 2026-08-14
This commit is contained in:
commit
ff24b00af4
404 changed files with 12634 additions and 3818 deletions
|
|
@ -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 },
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.hotwallet.upgradewallet.entity
|
||||
|
||||
internal data class UpgradeWalletUM(
|
||||
val onBackClick: () -> Unit,
|
||||
val onBuyTangemWalletClick: () -> Unit,
|
||||
val onScanDeviceClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.common.routing.AppRoute
|
||||
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.*
|
||||
|
|
@ -40,16 +41,18 @@ 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,
|
||||
),
|
||||
)
|
||||
|
|
@ -95,7 +98,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 +109,7 @@ internal class WalletBackupModel @Inject constructor(
|
|||
style = LabelStyle.WARNING,
|
||||
)
|
||||
},
|
||||
googleDriveStatus = LabelUM(
|
||||
googleDriveOption = LabelUM(
|
||||
text = resourceReference(R.string.common_coming_soon),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
|
|
@ -120,4 +123,8 @@ internal class WalletBackupModel @Inject constructor(
|
|||
uiMessageSender.send(makeBackupAtFirstAlertBS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
router.push(AppRoute.UpgradeWallet(params.userWalletId))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -49,7 +49,7 @@ 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,
|
||||
|
|
@ -79,7 +79,8 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
// sets proper artwork state for initial step
|
||||
// (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY])
|
||||
when (getInitialStep()) {
|
||||
MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ }
|
||||
MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */
|
||||
}
|
||||
MultiWalletFinalizeUM.Step.BackupDevice1 -> {
|
||||
onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded)
|
||||
}
|
||||
|
|
@ -333,7 +334,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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ interface OnrampIntents {
|
|||
fun openProviders()
|
||||
fun onRefresh()
|
||||
fun onLinkClick(link: String)
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
|
@ -148,4 +148,8 @@ internal class OnrampStateFactory(
|
|||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val PREDEFINED_SEPA_AMOUNT = "100"
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
|||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT
|
||||
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
|
||||
import com.tangem.features.onramp.providers.entity.SelectProviderResult
|
||||
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
||||
|
|
@ -64,6 +65,8 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
|
||||
private val params: OnrampMainComponent.Params = paramsContainer.require()
|
||||
|
||||
private var isSepaLaunched = false
|
||||
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
private val stateFactory = OnrampStateFactory(
|
||||
|
|
@ -148,11 +151,16 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
if (country == null) return@onEach
|
||||
_state.update {
|
||||
if (it is OnrampMainComponentUM.InitialLoading) {
|
||||
stateFactory.getReadyState(country.defaultCurrency)
|
||||
val state = stateFactory.getReadyState(country.defaultCurrency)
|
||||
if (params.launchSepa) {
|
||||
onAmountValueChanged(PREDEFINED_SEPA_AMOUNT)
|
||||
}
|
||||
state
|
||||
} else {
|
||||
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
updatePairsAndQuotes()
|
||||
},
|
||||
)
|
||||
|
|
@ -281,6 +289,8 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
|
||||
override fun onLinkClick(link: String) = urlOpener.openUrl(link)
|
||||
|
||||
override fun onContinueClick() = Unit
|
||||
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
quotesTaskScheduler.cancelTask()
|
||||
|
|
@ -315,8 +325,18 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
|
||||
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
|
||||
|
||||
val sepaQuote = if (params.launchSepa && !isSepaLaunched) {
|
||||
isSepaLaunched = true
|
||||
|
||||
quotes.filterIsInstance<OnrampQuote.Data>().firstOrNull {
|
||||
it.provider.id == MERCURYO_PROVIDER_ID && it.paymentMethod.id == SEPA_METHOD_ID
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Check if amount, country or currency has changed
|
||||
val newQuote = if (checkLastInputState(quoteToCheck)) {
|
||||
val newQuote = sepaQuote ?: if (checkLastInputState(quoteToCheck)) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
val state = state.value as? OnrampMainComponentUM.Content
|
||||
|
|
@ -418,5 +438,8 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
|
||||
private companion object {
|
||||
const val UPDATE_DELAY = 10_000L
|
||||
|
||||
const val MERCURYO_PROVIDER_ID = "mercuryo"
|
||||
const val SEPA_METHOD_ID = "sepa"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
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.onramp.confirmresidency.ConfirmResidencyComponent
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig
|
||||
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
||||
import com.tangem.features.onramp.mainv2.ui.OnrampNewMainScreen
|
||||
import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: OnrampV2MainComponent.Params,
|
||||
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
|
||||
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
|
||||
) : OnrampV2MainComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnrampV2MainComponentModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = null,
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsState()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
OnrampNewMainScreen(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: OnrampV2MainBottomSheetConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
is OnrampV2MainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = ConfirmResidencyComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
country = config.country,
|
||||
onDismiss = { model.bottomSheetNavigation.dismiss() },
|
||||
),
|
||||
)
|
||||
is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = SelectCurrencyComponent.Params(
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnrampV2MainComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: OnrampV2MainComponent.Params,
|
||||
): DefaultOnrampV2MainComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
class DefaultOnrampV2MainFeatureToggle(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : OnrampV2MainFeatureToggle {
|
||||
override val isOnrampNewMainEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
|
||||
internal interface OnrampV2MainComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val source: OnrampSource,
|
||||
val openSettings: () -> Unit,
|
||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnrampV2MainComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
internal interface OnrampV2MainFeatureToggle {
|
||||
val isOnrampNewMainEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.onramp.mainv2.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface OnrampMainV2ComponentModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampV2MainComponentModel::class)
|
||||
fun bindOnrampSelectCountryModel(model: OnrampV2MainComponentModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.onramp.mainv2.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainFeatureToggle
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface OnrampNewMainComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnrampV2MainComponentFactory(factory: DefaultOnrampV2MainComponent.Factory): OnrampV2MainComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureToggleModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOnrampV2MainFeatureToggle(featureTogglesManager: FeatureTogglesManager): OnrampV2MainFeatureToggle {
|
||||
return DefaultOnrampV2MainFeatureToggle(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampOffersBlockUM {
|
||||
|
||||
val isBlockVisible: Boolean
|
||||
|
||||
data object Empty : OnrampOffersBlockUM {
|
||||
override val isBlockVisible: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
data class Loading(
|
||||
override val isBlockVisible: Boolean,
|
||||
) : OnrampOffersBlockUM
|
||||
|
||||
data class Content(
|
||||
override val isBlockVisible: Boolean,
|
||||
val offers: ImmutableList<OnrampOfferUM>,
|
||||
) : OnrampOffersBlockUM
|
||||
}
|
||||
|
||||
internal data class OnrampOfferUM(
|
||||
val category: OnrampOfferCategory,
|
||||
val advantages: OnrampOfferAdvantages,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
val providerId: String,
|
||||
val providerName: String,
|
||||
)
|
||||
|
||||
internal enum class OnrampOfferCategory {
|
||||
RecentlyUsed, Recommended
|
||||
}
|
||||
|
||||
internal enum class OnrampOfferAdvantages {
|
||||
Default, BestRate, Fastest
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class OnrampNewAmountBlockUM(
|
||||
val currencyUM: OnrampNewCurrencyUM,
|
||||
val amountFieldModel: AmountFieldModel,
|
||||
val secondaryFieldModel: OnrampNewAmountSecondaryFieldUM,
|
||||
)
|
||||
|
||||
internal data class OnrampNewCurrencyUM(
|
||||
val unit: String,
|
||||
val code: String,
|
||||
val iconUrl: String?,
|
||||
val precision: Int,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampNewAmountSecondaryFieldUM {
|
||||
data object Loading : OnrampNewAmountSecondaryFieldUM
|
||||
data class Content(val amount: TextReference) : OnrampNewAmountSecondaryFieldUM
|
||||
data class Error(val error: TextReference) : OnrampNewAmountSecondaryFieldUM
|
||||
}
|
||||
|
||||
internal sealed interface OnrampV2AmountButtonUMState {
|
||||
data class Loaded(val amountButtons: ImmutableList<OnrampAmountButtonUM>) : OnrampV2AmountButtonUMState
|
||||
data object None : OnrampV2AmountButtonUMState
|
||||
}
|
||||
|
||||
internal data class OnrampAmountButtonUM(
|
||||
val value: Int,
|
||||
val currency: String,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampCountry
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface OnrampV2MainBottomSheetConfig {
|
||||
@Serializable
|
||||
data class ConfirmResidency(val country: OnrampCountry) : OnrampV2MainBottomSheetConfig
|
||||
|
||||
@Serializable
|
||||
data object CurrenciesList : OnrampV2MainBottomSheetConfig
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampV2MainComponentUM {
|
||||
|
||||
val topBarConfig: OnrampV2MainTopBarUM
|
||||
val continueButtonConfig: ContinueButtonUM
|
||||
val errorNotification: NotificationUM?
|
||||
|
||||
data class InitialLoading(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val continueButtonConfig: ContinueButtonUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
) : OnrampV2MainComponentUM
|
||||
|
||||
data class Content(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val continueButtonConfig: ContinueButtonUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
val amountBlockState: OnrampNewAmountBlockUM,
|
||||
val offersBlockState: OnrampOffersBlockUM,
|
||||
val onrampAmountButtonUMState: OnrampV2AmountButtonUMState,
|
||||
val onrampProviderState: OnrampV2ProvidersUM,
|
||||
) : OnrampV2MainComponentUM
|
||||
}
|
||||
|
||||
internal data class ContinueButtonUM(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val enabled: Boolean,
|
||||
val showProgress: Boolean = false,
|
||||
)
|
||||
|
||||
internal data class OnrampV2MainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
val endButtonUM: TopAppBarButtonUM,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
sealed interface OnrampV2ProvidersUM {
|
||||
|
||||
data object Empty : OnrampV2ProvidersUM
|
||||
|
||||
data object Loading : OnrampV2ProvidersUM
|
||||
|
||||
data class Content(
|
||||
val providerId: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
) : OnrampV2ProvidersUM
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.converter
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.utils.parseBigDecimalOrNull
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class OnrampV2AmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Converter<String, OnrampV2MainComponentUM> {
|
||||
|
||||
override fun convert(value: String): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
if (state !is OnrampV2MainComponentUM.Content) return state
|
||||
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
val amountState = state.amountBlockState
|
||||
val amountTextField = amountState.amountFieldModel
|
||||
val fiatDecimal = value.parseBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
val amountFieldModel = amountState.amountFieldModel.copy(
|
||||
fiatValue = value,
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
)
|
||||
|
||||
return state.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||
),
|
||||
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
|
||||
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.Content {
|
||||
val amountFieldModel = amountBlockState.amountFieldModel.copy(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO),
|
||||
fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO),
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
)
|
||||
return copy(
|
||||
amountBlockState = amountBlockState.copy(
|
||||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
continueButtonConfig = continueButtonConfig.copy(enabled = false),
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencySymbol = amountBlockState.currencyUM.unit,
|
||||
currencyCode = amountBlockState.currencyUM.code,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class OnrampAmountButtonUMStateFactory {
|
||||
|
||||
private val defaultPreselectedAmount = listOf(50, 100, 200, 300, 500)
|
||||
|
||||
fun createOnrampAmountActionButton(
|
||||
currencyCode: String,
|
||||
currencySymbol: String,
|
||||
onAmountValueChanged: (String) -> Unit,
|
||||
): OnrampV2AmountButtonUMState {
|
||||
return when (currencyCode) {
|
||||
USD_CODE, EUR_CODE -> {
|
||||
val buttons = defaultPreselectedAmount.map { value ->
|
||||
OnrampAmountButtonUM(
|
||||
value = value,
|
||||
currency = currencySymbol,
|
||||
onClick = { onAmountValueChanged(value.toString()) },
|
||||
)
|
||||
}.toPersistentList()
|
||||
OnrampV2AmountButtonUMState.Loaded(buttons)
|
||||
}
|
||||
else -> OnrampV2AmountButtonUMState.None
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val USD_CODE = "USD"
|
||||
private const val EUR_CODE = "EUR"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
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.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class OnrampV2AmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
) {
|
||||
|
||||
private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy(
|
||||
mode = LazyThreadSafetyMode.NONE,
|
||||
) {
|
||||
OnrampV2AmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
onrampIntents = onrampIntents,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM {
|
||||
return onrampAmountFieldChangeConverter.convert(value)
|
||||
}
|
||||
|
||||
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
currencyUM = amountState.currencyUM.copy(
|
||||
unit = currency.unit,
|
||||
code = currency.code,
|
||||
iconUrl = currency.image,
|
||||
precision = currency.precision,
|
||||
),
|
||||
amountFieldModel = amountState.amountFieldModel.copy(
|
||||
isError = false,
|
||||
fiatAmount = amountState.amountFieldModel.fiatAmount.copy(
|
||||
currencySymbol = currency.unit,
|
||||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencyCode = currency.code,
|
||||
currencySymbol = currency.unit,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryLoadingState(): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||
),
|
||||
offersBlockState = OnrampOffersBlockUM.Loading(isBlockVisible = false),
|
||||
continueButtonConfig = currentState.continueButtonConfig.copy(enabled = false),
|
||||
errorNotification = null,
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
amountFieldModel = amountState.amountFieldModel.copy(isError = false),
|
||||
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel,
|
||||
),
|
||||
continueButtonConfig = currentState.continueButtonConfig.copy(
|
||||
enabled = quote is OnrampQuote.Data,
|
||||
onClick = onrampIntents::onContinueClick,
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getUpdatedProviderState(selectedQuote: OnrampQuote): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ProviderCalculated(
|
||||
providerName = selectedQuote.provider.info.name,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
paymentMethod = selectedQuote.paymentMethod.name,
|
||||
),
|
||||
)
|
||||
return currentState.copy(
|
||||
onrampProviderState = selectedQuote.toProviderBlockState(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryResetState(): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
if (amountState.secondaryFieldModel is OnrampNewAmountSecondaryFieldUM.Content) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
amount = stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getShowProvidersState(): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
return when (currentState.offersBlockState) {
|
||||
is OnrampOffersBlockUM.Content -> {
|
||||
currentState.copy(
|
||||
offersBlockState = currentState.offersBlockState.copy(isBlockVisible = true),
|
||||
)
|
||||
}
|
||||
OnrampOffersBlockUM.Empty,
|
||||
is OnrampOffersBlockUM.Loading,
|
||||
-> currentState
|
||||
}
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toProviderBlockState(): OnrampV2ProvidersUM {
|
||||
return OnrampV2ProvidersUM.Content(
|
||||
paymentMethod = paymentMethod,
|
||||
providerId = provider.id,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toSecondaryFieldUiModel(
|
||||
amountState: OnrampNewAmountBlockUM,
|
||||
): OnrampNewAmountSecondaryFieldUM? {
|
||||
return when (this) {
|
||||
is OnrampQuote.Error -> null
|
||||
is OnrampQuote.Data -> {
|
||||
val amount = toAmount.value.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
}
|
||||
val contentAmount = combinedReference(stringReference("\u007E"), stringReference(amount))
|
||||
OnrampNewAmountSecondaryFieldUM.Content(contentAmount)
|
||||
}
|
||||
is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
|
||||
amountState: OnrampNewAmountBlockUM,
|
||||
): OnrampNewAmountSecondaryFieldUM.Error {
|
||||
val amount = error.requiredAmount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
|
||||
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
|
||||
)
|
||||
}
|
||||
|
||||
val errorTextRes = when (error) {
|
||||
is OnrampError.AmountError.TooBigError -> {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError)
|
||||
R.string.onramp_max_amount_restriction
|
||||
}
|
||||
is OnrampError.AmountError.TooSmallError -> {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError)
|
||||
R.string.onramp_min_amount_restriction
|
||||
}
|
||||
}
|
||||
|
||||
return OnrampNewAmountSecondaryFieldUM.Error(
|
||||
resourceReference(
|
||||
errorTextRes,
|
||||
wrappedList(amount),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.tokens.model.convertToAmount
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class OnrampV2StateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
) {
|
||||
|
||||
fun getInitialState(
|
||||
currency: String,
|
||||
onClose: () -> Unit,
|
||||
openSettings: () -> Unit,
|
||||
): OnrampV2MainComponentUM.InitialLoading {
|
||||
return OnrampV2MainComponentUM.InitialLoading(
|
||||
errorNotification = null,
|
||||
topBarConfig = OnrampV2MainTopBarUM(
|
||||
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
|
||||
startButtonUM = TopAppBarButtonUM.Close(
|
||||
onCloseClick = onClose,
|
||||
enabled = true,
|
||||
),
|
||||
endButtonUM = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_more_vertical_24,
|
||||
onClicked = openSettings,
|
||||
enabled = false,
|
||||
),
|
||||
),
|
||||
continueButtonConfig = ContinueButtonUM(
|
||||
text = resourceReference(R.string.common_continue),
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content {
|
||||
val state = currentStateProvider()
|
||||
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
is TopAppBarButtonUM.Icon -> button.copy(enabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(enabled = true)
|
||||
}
|
||||
|
||||
val initialAmountBlockState = getInitialAmountBlockState(currency)
|
||||
|
||||
return OnrampV2MainComponentUM.Content(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
continueButtonConfig = ContinueButtonUM(
|
||||
text = resourceReference(R.string.common_continue),
|
||||
onClick = onrampIntents::onContinueClick,
|
||||
enabled = false,
|
||||
),
|
||||
amountBlockState = initialAmountBlockState,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = null,
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencyCode = currency.code,
|
||||
currencySymbol = currency.unit,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnrampErrorState(onrampError: OnrampError): OnrampV2MainComponentUM {
|
||||
return when (onrampError) {
|
||||
OnrampError.PairsNotFound -> getNoPairsErrorState()
|
||||
is OnrampError.DataError -> getErrorState(
|
||||
errorCode = onrampError.code,
|
||||
onRefresh = onrampIntents::onRefresh,
|
||||
)
|
||||
is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh)
|
||||
is OnrampError.AmountError.TooBigError,
|
||||
is OnrampError.AmountError.TooSmallError,
|
||||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
-> currentStateProvider() // ignore error state
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampV2MainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
continueButtonConfig = contentState.continueButtonConfig.copy(enabled = false),
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
is TopAppBarButtonUM.Icon -> button.copy(enabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(enabled = true)
|
||||
}
|
||||
|
||||
return when (state) {
|
||||
is OnrampV2MainComponentUM.Content -> state.copy(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||
)
|
||||
is OnrampV2MainComponentUM.InitialLoading -> state.copy(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM {
|
||||
return OnrampNewAmountBlockUM(
|
||||
currencyUM = OnrampNewCurrencyUM(
|
||||
code = currency.code,
|
||||
iconUrl = currency.image,
|
||||
precision = currency.precision,
|
||||
onClick = onrampIntents::openCurrenciesList,
|
||||
unit = currency.unit,
|
||||
),
|
||||
amountFieldModel = AmountFieldModel(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
onValueChange = onrampIntents::onAmountValueChanged,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
isFiatValue = true,
|
||||
cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency),
|
||||
fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency),
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = TextReference.EMPTY,
|
||||
isFiatUnavailable = false,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
),
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount(
|
||||
currencySymbol = currency.unit,
|
||||
value = this,
|
||||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
package com.tangem.features.onramp.mainv2.model
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.fields.InputManager
|
||||
import com.tangem.domain.onramp.*
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.OnrampAvailability
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||
import com.tangem.features.onramp.main.entity.OnrampLastUpdate
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory
|
||||
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class OnrampV2MainComponentModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
|
||||
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
|
||||
private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase,
|
||||
private val fetchQuotesUseCase: OnrampFetchQuotesUseCase,
|
||||
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
|
||||
private val fetchPairsUseCase: OnrampFetchPairsUseCase,
|
||||
private val amountInputManager: InputManager,
|
||||
paramsContainer: ParamsContainer,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model(), OnrampIntents {
|
||||
|
||||
val params = paramsContainer.require<OnrampV2MainComponent.Params>()
|
||||
|
||||
private var loadQuotesJob: Job? = null
|
||||
|
||||
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
|
||||
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountButtonUMStateFactory()
|
||||
}
|
||||
|
||||
private val stateFactory = OnrampV2StateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
|
||||
private val amountStateFactory = OnrampV2AmountStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
|
||||
private val _state: MutableStateFlow<OnrampV2MainComponentUM> = MutableStateFlow(
|
||||
value = stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = ::onCloseClick,
|
||||
openSettings = ::openSettings,
|
||||
),
|
||||
)
|
||||
val state: StateFlow<OnrampV2MainComponentUM> get() = _state.asStateFlow()
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampV2MainBottomSheetConfig> = SlotNavigation()
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase()
|
||||
}
|
||||
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
loadQuotesJob?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAmountValueChanged(value: String) {
|
||||
_state.update { amountStateFactory.getOnAmountValueChange(value) }
|
||||
modelScope.launch { amountInputManager.update(value) }
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
params.openSettings
|
||||
}
|
||||
|
||||
override fun openCurrenciesList() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened)
|
||||
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList)
|
||||
}
|
||||
|
||||
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
|
||||
// TODO in [REDACTED_TASK_KEY] to be continued
|
||||
}
|
||||
|
||||
override fun openProviders() {
|
||||
// TODO in [REDACTED_TASK_KEY] to be continued
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
_state.update {
|
||||
stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = router::pop,
|
||||
openSettings = ::openSettings,
|
||||
)
|
||||
}
|
||||
loadQuotesJob?.cancel()
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkClick(link: String) {
|
||||
// TODO in [REDACTED_TASK_KEY] will be deleted
|
||||
}
|
||||
|
||||
override fun onContinueClick() {
|
||||
val currentState = _state.value
|
||||
if (currentState is OnrampV2MainComponentUM.Content) {
|
||||
_state.update { amountStateFactory.getShowProvidersState() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkResidenceCountry() {
|
||||
modelScope.launch {
|
||||
checkOnrampAvailabilityUseCase(userWallet)
|
||||
.onRight(::handleOnrampAvailability)
|
||||
.onLeft(::handleOnrampError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnrampAvailability(availability: OnrampAvailability) {
|
||||
when (availability) {
|
||||
is OnrampAvailability.Available -> Unit
|
||||
is OnrampAvailability.ConfirmResidency,
|
||||
is OnrampAvailability.NotSupported,
|
||||
-> bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.ConfirmResidency(availability.country))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp)
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||
loadQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||
getOnrampCountryUseCase.invoke()
|
||||
.onEach { maybeCountry ->
|
||||
maybeCountry.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { country ->
|
||||
if (country == null) return@onEach
|
||||
_state.update {
|
||||
when (it) {
|
||||
is OnrampV2MainComponentUM.Content -> {
|
||||
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||
}
|
||||
is OnrampV2MainComponentUM.InitialLoading -> {
|
||||
stateFactory.getReadyState(country.defaultCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePairsAndQuotes()
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToQuotesUpdate() {
|
||||
getOnrampQuotesUseCase.invoke()
|
||||
.conflate()
|
||||
.onEach { maybeQuotes ->
|
||||
maybeQuotes.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = ::handleQuoteResult,
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
|
||||
sendOnrampQuotesErrorAnalytic(quotes)
|
||||
|
||||
val quote = selectOrUpdateQuote(quotes)
|
||||
|
||||
if (quote == null) {
|
||||
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
lastUpdateState.value = null
|
||||
return
|
||||
}
|
||||
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
|
||||
}
|
||||
|
||||
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
|
||||
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
|
||||
|
||||
// Check if amount, country or currency has changed
|
||||
val newQuote = if (checkLastInputState(quoteToCheck)) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
val state = state.value as? OnrampV2MainComponentUM.Content
|
||||
val providerState = state?.onrampProviderState as? OnrampV2ProvidersUM.Content
|
||||
|
||||
// Get current selected quote to update
|
||||
val lastSelectedQuote = quotes.firstOrNull {
|
||||
it.provider.id == providerState?.providerId &&
|
||||
it.paymentMethod.id == providerState.paymentMethod.id
|
||||
}
|
||||
|
||||
if (lastSelectedQuote is OnrampQuote.Error) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
lastSelectedQuote
|
||||
}
|
||||
}
|
||||
newQuote?.let { updateProvider(newQuote) }
|
||||
|
||||
return newQuote
|
||||
}
|
||||
|
||||
private fun onRetryQuotes() {
|
||||
_state.update {
|
||||
(it as? OnrampV2MainComponentUM.Content)?.copy(
|
||||
errorNotification = null,
|
||||
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading(isBlockVisible = false),
|
||||
amountBlockState = it.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||
),
|
||||
) ?: it
|
||||
}
|
||||
loadQuotes()
|
||||
}
|
||||
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
val state = state.value as? OnrampV2MainComponentUM.Content
|
||||
|
||||
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
|
||||
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||
}
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = {
|
||||
_state.update {
|
||||
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
|
||||
return@fold
|
||||
} else {
|
||||
amountStateFactory.getAmountSecondaryResetState()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
loadQuotes()
|
||||
}
|
||||
|
||||
private fun loadQuotes() {
|
||||
loadQuotesJob?.cancel()
|
||||
loadQuotesJob = modelScope.launch {
|
||||
runCatching {
|
||||
val content = state.value as? OnrampV2MainComponentUM.Content ?: return@runCatching
|
||||
if (content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) return@runCatching
|
||||
fetchQuotesUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
amount = content.amountBlockState.amountFieldModel.fiatAmount,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
).onLeft(::handleOnrampError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
_state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
private fun updateProvider(quote: OnrampQuote) {
|
||||
lastUpdateState.value = OnrampLastUpdate(
|
||||
quote.fromAmount,
|
||||
quote.countryCode,
|
||||
)
|
||||
|
||||
_state.update {
|
||||
amountStateFactory.getUpdatedProviderState(selectedQuote = quote)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
|
||||
quotes.forEach { errorState ->
|
||||
when (errorState) {
|
||||
is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent(
|
||||
error = errorState.error,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = errorState.provider.info.name,
|
||||
paymentMethod = errorState.paymentMethod.name,
|
||||
)
|
||||
is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent(
|
||||
error = errorState.error,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = errorState.provider.info.name,
|
||||
paymentMethod = errorState.paymentMethod.name,
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
|
||||
return lastUpdateState.value?.lastAmount != quote?.fromAmount ||
|
||||
lastUpdateState.value?.lastCountryString != quote?.countryCode
|
||||
}
|
||||
|
||||
private fun sendScreenOpenAnalytics() {
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
source = params.source,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampFooterContent(
|
||||
state: OnrampV2MainComponentUM.Content,
|
||||
boxScope: BoxScope,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
boxScope.apply {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.align(Alignment.BottomCenter),
|
||||
visible = state.offersBlockState.isBlockVisible.not(),
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it },
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { it },
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
),
|
||||
label = "Footer block animation",
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringResourceSafe(id = R.string.common_continue),
|
||||
onClick = {
|
||||
state.continueButtonConfig.onClick()
|
||||
keyboardController?.hide()
|
||||
},
|
||||
enabled = state.continueButtonConfig.enabled,
|
||||
)
|
||||
SpacerH(16.dp)
|
||||
OnrampAmountButtons(state = state.onrampAmountButtonUMState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) {
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state is OnrampV2AmountButtonUMState.Loaded,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampV2AmountButtonUMState.Loaded -> {
|
||||
if (keyboard is Keyboard.Opened) {
|
||||
LazyRow(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.button.secondary),
|
||||
contentPadding = PaddingValues(
|
||||
vertical = 10.dp,
|
||||
horizontal = 8.dp,
|
||||
),
|
||||
state = rememberLazyListState(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(
|
||||
items = state.amountButtons,
|
||||
key = OnrampAmountButtonUM::value,
|
||||
) {
|
||||
AmountButton(button = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OnrampV2AmountButtonUMState.None -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountButton(button: OnrampAmountButtonUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.sizeIn(minHeight = 24.dp, minWidth = 62.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
.clickable(onClick = button.onClick)
|
||||
.padding(vertical = 4.dp, horizontal = 20.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "${button.value}${button.currency}",
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampNewMainScreen(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) {
|
||||
Scaffold(
|
||||
modifier = modifier.systemBarsPadding(),
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
startButton = state.topBarConfig.startButtonUM,
|
||||
endButton = state.topBarConfig.endButtonUM,
|
||||
title = state.topBarConfig.title.resolveReference(),
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
) { scaffoldPaddings ->
|
||||
OnrampNewMainComponentContent(
|
||||
state = state,
|
||||
modifier = Modifier.padding(scaffoldPaddings),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampV2MainComponentUM.InitialLoading -> InitialLoading(state = state)
|
||||
is OnrampV2MainComponentUM.Content -> Content(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
if (state is OnrampV2MainComponentUM.Content) {
|
||||
OnrampFooterContent(
|
||||
state = state,
|
||||
boxScope = this,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampAmountContentLoading()
|
||||
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountContentLoading() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(vertical = TangemTheme.dimens.spacing28),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16)
|
||||
.size(width = 76.dp, height = 20.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.size(width = 136.dp, height = 44.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.size(width = 52.dp, height = 16.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing20)
|
||||
.size(width = 84.dp, height = 28.dp),
|
||||
radius = TangemTheme.dimens.radius14,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
bottom = 76.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampV2AmountContent(state = state)
|
||||
|
||||
OnrampOffersContent(state = state.offersBlockState)
|
||||
|
||||
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
|
||||
AnimatedVisibility(
|
||||
visible = state.isBlockVisible,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it },
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { it },
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
),
|
||||
label = "Offers block animation",
|
||||
) {
|
||||
Text(
|
||||
text = "Some offers",
|
||||
style = TangemTheme.typography.head,
|
||||
)
|
||||
// TODO in [REDACTED_TASK_KEY] to be continued
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampNewAmountSecondaryFieldUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
val padding = remember(state.offersBlockState.isBlockVisible) {
|
||||
if (state.offersBlockState.isBlockVisible) {
|
||||
22.dp
|
||||
} else {
|
||||
46.dp
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||
)
|
||||
.padding(vertical = padding)
|
||||
.animateContentSize(animationSpec = tween(durationMillis = 300)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnrampHeaderTitle()
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
OnrampAmountField(
|
||||
amountField = state.amountBlockState.amountFieldModel,
|
||||
currencyCode = state.amountBlockState.currencyUM.code,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampHeaderTitle() {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_you_will_pay_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val requester = remember { FocusRequester() }
|
||||
AmountTextField(
|
||||
value = amountField.fiatValue,
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
symbol = currencyCode,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
symbolColor = TangemTheme.colors.text.disabled,
|
||||
),
|
||||
onValueChange = amountField.onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
keyboardActions = amountField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.head.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
isEnabled = !amountField.isError,
|
||||
isAutoResize = true,
|
||||
isValuePasted = amountField.isValuePasted,
|
||||
onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss,
|
||||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.requiredHeightIn(min = TangemTheme.dimens.size32)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD),
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountSecondary(state: OnrampNewAmountSecondaryFieldUM) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampNewAmountSecondaryFieldUM.Content -> Text(
|
||||
text = state.amount.resolveReference(),
|
||||
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
is OnrampNewAmountSecondaryFieldUM.Error -> Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
is OnrampNewAmountSecondaryFieldUM.Loading -> TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.width(TangemTheme.dimens.size62),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampCurrencyIcon(currencyUM: OnrampNewCurrencyUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(onClick = currencyUM.onClick)
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clip(CircleShape)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON),
|
||||
model = currencyUM.iconUrl,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = currencyUM.code,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +81,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
},
|
||||
launchSepa = params.launchSepa,
|
||||
),
|
||||
)
|
||||
is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create(
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
uiState.update(FeeItemSelectedTransformer(feeItem))
|
||||
if (feeItem !is FeeItem.Custom) {
|
||||
onDoneClick()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
|
|
|
|||
|
|
@ -82,15 +82,19 @@ internal fun FeeSelectorModalBottomSheet(
|
|||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
text = stringResourceSafe(R.string.common_done),
|
||||
onClick = feeSelectorIntents::onDoneClick,
|
||||
)
|
||||
footer = if (state.selectedFeeItem is FeeItem.Custom) {
|
||||
{
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
text = stringResourceSafe(R.string.common_done),
|
||||
onClick = feeSelectorIntents::onDoneClick,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -426,7 +430,14 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<
|
|||
FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))),
|
||||
customFeeItem,
|
||||
),
|
||||
selectedFeeItem = customFeeItem,
|
||||
selectedFeeItem = FeeItem.Slow(
|
||||
fee = Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.01"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = true,
|
||||
isFeeConvertibleToFiat = true,
|
||||
|
|
|
|||
|
|
@ -21,14 +21,12 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
|
|
@ -90,7 +88,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
|
|
@ -329,15 +327,9 @@ internal class SendConfirmModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow
|
||||
}
|
||||
|
||||
val cardInfo =
|
||||
getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return
|
||||
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
|
||||
val metaInfo = getWalletMetaInfoUseCase.invoke(userWallet.walletId).getOrNull() ?: return@launch
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
|
|
@ -26,7 +26,6 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
|
|
@ -91,7 +90,7 @@ internal class SendModel @Inject constructor(
|
|||
private val parseQrCodeUseCase: ParseQrCodeUseCase,
|
||||
private val sendConfirmAlertFactory: SendConfirmAlertFactory,
|
||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
|
||||
|
|
@ -472,15 +471,9 @@ internal class SendModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow
|
||||
}
|
||||
|
||||
val cardInfo =
|
||||
getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return
|
||||
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
|
||||
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(walletMetaInfo = metaInfo))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue