Updated on 2026-08-14
This commit is contained in:
commit
2f62d482ca
635 changed files with 18802 additions and 9118 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,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase
|
||||
import com.tangem.domain.settings.SetAskBiometryShownUseCase
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
|
|
@ -39,7 +39,7 @@ import javax.inject.Inject
|
|||
internal class AskBiometryModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase,
|
||||
private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
|
|
@ -66,7 +66,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
|
||||
init {
|
||||
modelScope.launch {
|
||||
setSaveWalletScreenShownUseCase()
|
||||
setAskBiometryShownUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +111,6 @@ internal class AskBiometryModel @Inject constructor(
|
|||
|
||||
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
|
||||
walletsRepository.saveShouldSaveUserWallets(item = true)
|
||||
settingsRepository.setShouldSaveAccessCodes(value = true)
|
||||
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
walletsRepository.setUseBiometricAuthentication(value = true)
|
||||
|
|
@ -120,6 +119,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(),
|
||||
)
|
||||
} else {
|
||||
settingsRepository.setShouldSaveAccessCodes(value = true)
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = userWallet.hasAccessCode,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ private fun WalletBlock(
|
|||
.padding(top = 8.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.secondary,
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
private val userWalletsFetcher = userWalletsFetcherFactory.create(
|
||||
messageSender = messageSender,
|
||||
onlyMultiCurrency = false,
|
||||
authMode = false,
|
||||
isAuthMode = false,
|
||||
onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.hotwallet
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface ViewPhraseComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, ViewPhraseComponent>
|
||||
}
|
||||
|
|
@ -34,12 +34,11 @@ internal class AccessCodeComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String)
|
||||
fun onAccessCodeConfirmed(userWalletId: UserWalletId)
|
||||
fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String)
|
||||
fun onAccessCodeUpdated(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val isConfirmMode: Boolean,
|
||||
val accessCodeToConfirm: String? = null,
|
||||
val userWalletId: UserWalletId,
|
||||
val callbacks: ModelCallbacks,
|
||||
|
|
|
|||
|
|
@ -5,43 +5,64 @@ import arrow.core.getOrElse
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.SetAskBiometryShownUseCase
|
||||
import com.tangem.domain.settings.ShouldShowAskBiometryUseCase
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetHotWalletContextualUnlockUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.hot.sdk.model.UnlockHotWallet
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class AccessCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getHotWalletContextualUnlockUseCase: GetHotWalletContextualUnlockUseCase,
|
||||
private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase,
|
||||
private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase,
|
||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AccessCodeComponent.Params>()
|
||||
|
||||
private var hotWalletId: HotWalletId? = null
|
||||
|
||||
internal val uiState: StateFlow<AccessCodeUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
private fun getInitialState() = AccessCodeUM(
|
||||
accessCode = "",
|
||||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
isConfirmMode = params.isConfirmMode,
|
||||
isConfirmMode = params.accessCodeToConfirm != null,
|
||||
buttonEnabled = false,
|
||||
buttonInProgress = false,
|
||||
onButtonClick = ::onButtonClick,
|
||||
|
|
@ -51,7 +72,7 @@ internal class AccessCodeModel @Inject constructor(
|
|||
uiState.update {
|
||||
it.copy(
|
||||
accessCode = value,
|
||||
buttonEnabled = if (params.isConfirmMode) {
|
||||
buttonEnabled = if (params.accessCodeToConfirm != null) {
|
||||
value == params.accessCodeToConfirm
|
||||
} else {
|
||||
value.length == uiState.value.accessCodeLength
|
||||
|
|
@ -61,12 +82,10 @@ internal class AccessCodeModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onButtonClick() {
|
||||
if (!params.isConfirmMode) {
|
||||
params.callbacks.onAccessCodeSet(params.userWalletId, uiState.value.accessCode)
|
||||
if (params.accessCodeToConfirm == null) {
|
||||
params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode)
|
||||
} else {
|
||||
params.accessCodeToConfirm?.let {
|
||||
setCode(params.userWalletId, it)
|
||||
}
|
||||
setCode(params.userWalletId, params.accessCodeToConfirm)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,12 +100,16 @@ internal class AccessCodeModel @Inject constructor(
|
|||
.getOrElse { error("User wallet with id $userWalletId not found") }
|
||||
if (userWallet !is UserWallet.Hot) return@launch
|
||||
|
||||
val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
|
||||
val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId)
|
||||
.getOrNull()
|
||||
?: UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
|
||||
var updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
auth = HotAuth.Password(accessCode.toCharArray()),
|
||||
)
|
||||
|
||||
tryToAskForBiometry()
|
||||
|
||||
if (walletsRepository.requireAccessCode().not()) {
|
||||
updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
|
|
@ -117,7 +140,7 @@ internal class AccessCodeModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
|
||||
params.callbacks.onAccessCodeUpdated(params.userWalletId)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
|
||||
|
|
@ -127,4 +150,54 @@ internal class AccessCodeModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun tryToAskForBiometry() {
|
||||
if (!shouldAskForBiometry()) return
|
||||
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.common_attention),
|
||||
message = resourceReference(R.string.hot_access_code_set_biometric_ask),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_allow),
|
||||
onClick = {
|
||||
modelScope.launch {
|
||||
setAskBiometryShownUseCase()
|
||||
walletsRepository.setUseBiometricAuthentication(true)
|
||||
walletsRepository.setRequireAccessCode(false)
|
||||
continuation.resumeWith(Result.success(Unit))
|
||||
}
|
||||
},
|
||||
),
|
||||
secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.save_user_wallet_agreement_dont_allow),
|
||||
onClick = {
|
||||
modelScope.launch {
|
||||
setAskBiometryShownUseCase()
|
||||
continuation.resumeWith(Result.success(Unit))
|
||||
}
|
||||
},
|
||||
),
|
||||
onDismissRequest = {
|
||||
modelScope.launch {
|
||||
continuation.resumeWith(Result.success(Unit))
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun shouldAskForBiometry(): Boolean {
|
||||
val canUseBiometry = canUseBiometryUseCase()
|
||||
val shouldShowAskBiometry = shouldShowAskBiometryUseCase()
|
||||
|
||||
return canUseBiometry && shouldShowAskBiometry
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) }
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,11 +127,11 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
|
||||
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
|
||||
override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) {
|
||||
stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode))
|
||||
}
|
||||
|
||||
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
|
||||
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
|
||||
navigateToPushNotificationsOrNext()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ internal class AddExistingWalletChildFactory @Inject constructor(
|
|||
is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = false,
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
),
|
||||
|
|
@ -54,7 +53,6 @@ internal class AddExistingWalletChildFactory @Inject constructor(
|
|||
is AddExistingWalletRoute.ConfirmAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = true,
|
||||
accessCodeToConfirm = route.accessCode,
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ internal class UpdateAccessCodeModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
|
||||
override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) {
|
||||
stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode))
|
||||
}
|
||||
|
||||
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
|
||||
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ internal class UpdateAccessCodeChildFactory @Inject constructor(
|
|||
is UpdateAccessCodeRoute.SetAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = false,
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model,
|
||||
),
|
||||
|
|
@ -27,7 +26,6 @@ internal class UpdateAccessCodeChildFactory @Inject constructor(
|
|||
is UpdateAccessCodeRoute.ConfirmAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = true,
|
||||
accessCodeToConfirm = route.accessCode,
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@ import kotlinx.serialization.Serializable
|
|||
internal sealed class UpdateAccessCodeRoute : Route {
|
||||
|
||||
@Serializable
|
||||
data class SetAccessCode(val userWalletId: UserWalletId) : UpdateAccessCodeRoute()
|
||||
data class SetAccessCode(
|
||||
val userWalletId: UserWalletId,
|
||||
) : UpdateAccessCodeRoute()
|
||||
|
||||
@Serializable
|
||||
data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : UpdateAccessCodeRoute()
|
||||
data class ConfirmAccessCode(
|
||||
val userWalletId: UserWalletId,
|
||||
val accessCode: String,
|
||||
) : UpdateAccessCodeRoute()
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.hotwallet.viewphrase
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.hotwallet.ViewPhraseComponent
|
||||
import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel
|
||||
import com.tangem.features.hotwallet.viewphrase.ui.ViewPhraseContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultViewPhraseComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: ViewPhraseComponent.Params,
|
||||
) : ViewPhraseComponent, AppComponentContext by context {
|
||||
private val model: ViewPhraseModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ViewPhraseContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ViewPhraseComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: ViewPhraseComponent.Params,
|
||||
): DefaultViewPhraseComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.hotwallet.viewphrase.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.ViewPhraseComponent
|
||||
import com.tangem.features.hotwallet.viewphrase.DefaultViewPhraseComponent
|
||||
import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ViewPhraseModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ViewPhraseModel::class)
|
||||
fun bindViewPhraseModel(model: ViewPhraseModel): Model
|
||||
|
||||
@Binds
|
||||
fun bindViewPhraseComponentFactory(factory: DefaultViewPhraseComponent.Factory): ViewPhraseComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.hotwallet.viewphrase.entity
|
||||
|
||||
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class ViewPhraseUM(
|
||||
val onBackClick: () -> Unit,
|
||||
val words: ImmutableList<EnumeratedTwoColumnGridItem> = persistentListOf(),
|
||||
)
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.features.hotwallet.viewphrase.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase
|
||||
import com.tangem.domain.wallets.usecase.ExportSeedPhraseUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.hotwallet.ViewPhraseComponent
|
||||
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class ViewPhraseModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val exportSeedPhraseUseCase: ExportSeedPhraseUseCase,
|
||||
private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ViewPhraseComponent.Params>()
|
||||
|
||||
private var hotWalletId: HotWalletId? = null
|
||||
|
||||
internal val uiState: StateFlow<ViewPhraseUM>
|
||||
field = MutableStateFlow(
|
||||
ViewPhraseUM(
|
||||
onBackClick = { router.pop() },
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
loadSeedPhrase()
|
||||
}
|
||||
|
||||
private fun loadSeedPhrase() {
|
||||
val userWallet = getUserWalletUseCase(params.userWalletId)
|
||||
.getOrElse { error("User wallet with id ${params.userWalletId} not found") }
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
hotWalletId = userWallet.hotWalletId
|
||||
modelScope.launch {
|
||||
val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId)
|
||||
.getOrElse { error("Unable to export seed phrase for wallet with id ${params.userWalletId}") }
|
||||
.mnemonic
|
||||
.mnemonicComponents
|
||||
uiState.update {
|
||||
it.copy(
|
||||
words = words.mapIndexed { index, s ->
|
||||
EnumeratedTwoColumnGridItem(index + 1, s)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) }
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.features.hotwallet.viewphrase.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid
|
||||
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun ViewPhraseContent(state: ViewPhraseUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding(),
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
text = stringResourceSafe(R.string.common_backup),
|
||||
onBackClick = state.onBackClick,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.imePadding()
|
||||
.weight(1f),
|
||||
) {
|
||||
TitleBlock(
|
||||
state = state,
|
||||
modifier = Modifier.padding(top = 20.dp),
|
||||
)
|
||||
|
||||
EnumeratedTwoColumnGrid(
|
||||
items = state.words,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp, bottom = 32.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleBlock(state: ViewPhraseUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens.size36)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.backup_seed_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(
|
||||
R.string.backup_seed_caution,
|
||||
state.words.size,
|
||||
),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
ViewPhraseContent(
|
||||
state = ViewPhraseUM(
|
||||
onBackClick = {},
|
||||
words = List(12) {
|
||||
EnumeratedTwoColumnGridItem(
|
||||
index = it + 1,
|
||||
mnemonic = "word${it + 1}",
|
||||
)
|
||||
}.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,16 @@ import com.arkivanov.decompose.router.stack.StackNavigation
|
|||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.push
|
||||
import com.arkivanov.decompose.router.stack.replaceAll
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.settings.ShouldAskPermissionUseCase
|
||||
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
|
||||
|
|
@ -32,6 +38,7 @@ internal class WalletActivationModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<WalletActivationComponent.Params>()
|
||||
|
|
@ -79,13 +86,31 @@ internal class WalletActivationModel @Inject constructor(
|
|||
stackNavigation.replaceAll(WalletActivationRoute.SetupFinished)
|
||||
}
|
||||
|
||||
private fun showSkipAccessCodeWarningDialog() {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
message = resourceReference(R.string.access_code_alert_skip_description),
|
||||
title = resourceReference(R.string.access_code_alert_skip_title),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
),
|
||||
secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.access_code_alert_skip_ok),
|
||||
onClick = { navigateToPushNotificationsOrNext() },
|
||||
),
|
||||
dismissOnFirstAction = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
|
||||
override fun onBackClick() {
|
||||
onChildBack()
|
||||
}
|
||||
|
||||
override fun onSkipClick() {
|
||||
navigateToPushNotificationsOrNext()
|
||||
showSkipAccessCodeWarningDialog()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,11 +141,11 @@ internal class WalletActivationModel @Inject constructor(
|
|||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
|
||||
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
|
||||
override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) {
|
||||
stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode))
|
||||
}
|
||||
|
||||
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
|
||||
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
|
||||
navigateToPushNotificationsOrNext()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ internal class WalletActivationChildFactory @Inject constructor(
|
|||
is WalletActivationRoute.SetAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = false,
|
||||
userWalletId = model.params.userWalletId,
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
),
|
||||
|
|
@ -63,7 +62,6 @@ internal class WalletActivationChildFactory @Inject constructor(
|
|||
is WalletActivationRoute.ConfirmAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = true,
|
||||
accessCodeToConfirm = route.accessCode,
|
||||
userWalletId = model.params.userWalletId,
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,33 @@
|
|||
package com.tangem.features.hotwallet.walletbackup.model
|
||||
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
|
||||
import com.tangem.core.ui.components.bottomsheets.message.icon
|
||||
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.message.onClick
|
||||
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase
|
||||
import com.tangem.features.hotwallet.WalletBackupComponent
|
||||
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
|
||||
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class WalletBackupModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
getWalletUseCase: GetUserWalletUseCase,
|
||||
private val router: Router,
|
||||
private val getWalletUseCase: GetUserWalletUseCase,
|
||||
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val router: Router,
|
||||
) : Model() {
|
||||
|
||||
private val params: WalletBackupComponent.Params = paramsContainer.require()
|
||||
|
|
@ -40,48 +36,32 @@ internal class WalletBackupModel @Inject constructor(
|
|||
field = MutableStateFlow(
|
||||
WalletBackupUM(
|
||||
onBackClick = { router.pop() },
|
||||
recoveryPhraseStatus = LabelUM(
|
||||
recoveryPhraseOption = LabelUM(
|
||||
text = resourceReference(R.string.hw_backup_no_backup),
|
||||
style = LabelStyle.WARNING,
|
||||
),
|
||||
googleDriveStatus = LabelUM(
|
||||
googleDriveOption = LabelUM(
|
||||
text = resourceReference(R.string.common_coming_soon),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
googleDriveStatus = BackupStatus.ComingSoon,
|
||||
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
|
||||
onGoogleDriveClick = { },
|
||||
onHardwareWalletClick = ::onHardwareWalletClick,
|
||||
backedUp = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val makeBackupAtFirstAlertBS
|
||||
get() = bottomSheetMessage {
|
||||
infoBlock {
|
||||
icon(R.drawable.ic_passcode_lock_32) {
|
||||
type = MessageBottomSheetUMV2.Icon.Type.Accent
|
||||
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
|
||||
}
|
||||
title = resourceReference(R.string.hw_backup_need_title)
|
||||
body = resourceReference(R.string.hw_backup_need_description)
|
||||
}
|
||||
secondaryButton {
|
||||
text = resourceReference(R.string.hw_backup_need_action)
|
||||
onClick {
|
||||
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
getWalletUseCase.invokeFlow(params.userWalletId)
|
||||
.map { it.getOrNull() }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach {
|
||||
updateBackupStatuses(it)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
getWalletUseCase.invoke(params.userWalletId)
|
||||
.fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = {
|
||||
updateBackupStatuses(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateBackupStatuses(userWallet: UserWallet) {
|
||||
|
|
@ -95,7 +75,7 @@ internal class WalletBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun WalletBackupUM.updateBackupStatusesHotWallet(userWallet: UserWallet.Hot): WalletBackupUM = copy(
|
||||
recoveryPhraseStatus = if (userWallet.backedUp) {
|
||||
recoveryPhraseOption = if (userWallet.backedUp) {
|
||||
LabelUM(
|
||||
text = resourceReference(R.string.common_done),
|
||||
style = LabelStyle.ACCENT,
|
||||
|
|
@ -106,7 +86,7 @@ internal class WalletBackupModel @Inject constructor(
|
|||
style = LabelStyle.WARNING,
|
||||
)
|
||||
},
|
||||
googleDriveStatus = LabelUM(
|
||||
googleDriveOption = LabelUM(
|
||||
text = resourceReference(R.string.common_coming_soon),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
|
|
@ -115,9 +95,41 @@ internal class WalletBackupModel @Inject constructor(
|
|||
|
||||
private fun onRecoveryPhraseClick() {
|
||||
if (uiState.value.backedUp) {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
getWalletUseCase.invoke(params.userWalletId)
|
||||
.fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val userWalletId = userWallet.walletId
|
||||
Timber.e("Unexpected cold wallet when request seed phrase: $userWalletId")
|
||||
}
|
||||
is UserWallet.Hot -> showSeedPhrase(userWallet)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
uiMessageSender.send(makeBackupAtFirstAlertBS)
|
||||
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSeedPhrase(hotWallet: UserWallet.Hot) {
|
||||
modelScope.launch {
|
||||
unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId)
|
||||
.fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error while export seed phrase: $it")
|
||||
},
|
||||
ifRight = { seedPhrasePrivateInfo ->
|
||||
router.push(AppRoute.ViewPhrase(params.userWalletId))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
router.push(AppRoute.UpgradeWallet(params.userWalletId))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,14 +4,9 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
|
||||
interface KycComponent {
|
||||
|
||||
fun launch(params: Params)
|
||||
fun launch()
|
||||
|
||||
interface Factory {
|
||||
fun create(appComponentContext: AppComponentContext): KycComponent
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val targetAddress: String,
|
||||
val cardId: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ class DefaultKycComponent @AssistedInject constructor(
|
|||
|
||||
private val model: DefaultKycModel = getOrCreateModel()
|
||||
|
||||
override fun launch(params: KycComponent.Params) {
|
||||
override fun launch() {
|
||||
componentScope.launch {
|
||||
model.uiState.collect {
|
||||
it?.let { startInfo ->
|
||||
|
|
@ -35,7 +35,7 @@ class DefaultKycComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
model.getKycToken(params)
|
||||
model.getKycToken()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.usecase.KycStartInfoUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -15,18 +15,15 @@ import javax.inject.Inject
|
|||
@ModelScoped
|
||||
class DefaultKycModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
kycRepositoryFactory: KycRepository.Factory,
|
||||
private val kycStartInfoUseCase: KycStartInfoUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val kycRepository = kycRepositoryFactory.create()
|
||||
|
||||
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun getKycToken(params: KycComponent.Params) {
|
||||
fun getKycToken() {
|
||||
modelScope.launch {
|
||||
kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull()
|
||||
?.let { _uiState.emit(it) }
|
||||
kycStartInfoUseCase().getOrNull()?.let { _uiState.emit(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>()
|
||||
|
|
@ -145,7 +149,7 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
) {
|
||||
modelScope.launch {
|
||||
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
|
||||
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowAskBiometry()) {
|
||||
doIfVisa {
|
||||
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened)
|
||||
}
|
||||
|
|
@ -197,6 +201,19 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun exitComponentScreen() {
|
||||
// new flow
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
modelScope.launch {
|
||||
if (userWalletsListRepository.userWalletsSync().isEmpty()) {
|
||||
router.replaceAll(AppRoute.Home())
|
||||
} else {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// legacy flow
|
||||
if (userWalletsListManager.hasUserWallets) {
|
||||
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -19,8 +19,8 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.UpdateWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
|
||||
|
|
@ -37,7 +37,10 @@ import com.tangem.sdk.api.TangemSdkManager
|
|||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -49,11 +52,11 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val backupServiceHolder: BackupServiceHolder,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase,
|
||||
private val updateWalletUseCase: UpdateWalletUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
|
|
@ -244,7 +247,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
userWalletCreated
|
||||
}
|
||||
OnboardingMultiWalletComponent.Mode.AddBackup -> {
|
||||
val userWallet = userWalletsListManager.userWallets.first()
|
||||
val userWallet = getUserWalletsUseCase.invokeSync()
|
||||
.firstOrNull {
|
||||
it is UserWallet.Cold &&
|
||||
it.scanResponse.primaryCard?.cardId == scanResponse.primaryCard?.cardId
|
||||
|
|
@ -333,7 +336,8 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
|
||||
private fun navigateToSupportScreen() {
|
||||
modelScope.launch {
|
||||
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
|
||||
val cardInfo =
|
||||
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
|
||||
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.onramp.alloffers
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
|
||||
internal interface AllOffersComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val onDismiss: () -> Unit,
|
||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||
val amountCurrencyCode: String,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, AllOffersComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.onramp.alloffers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.onramp.alloffers.model.AllOffersModel
|
||||
import com.tangem.features.onramp.alloffers.ui.AllOffersContentSheet
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultAllOffersComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: AllOffersComponent.Params,
|
||||
) : AllOffersComponent, AppComponentContext by context {
|
||||
|
||||
private val model: AllOffersModel = getOrCreateModel(params)
|
||||
|
||||
override fun dismiss() {
|
||||
model.dismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.state.collectAsState()
|
||||
AllOffersContentSheet(
|
||||
state = state,
|
||||
onCloseClick = { dismiss() },
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AllOffersComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: AllOffersComponent.Params): DefaultAllOffersComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.onramp.alloffers.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.alloffers.model.AllOffersModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface AllOffersComponentModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AllOffersModel::class)
|
||||
fun bindAllOffersModel(model: AllOffersModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.onramp.alloffers.di
|
||||
|
||||
import com.tangem.features.onramp.alloffers.AllOffersComponent
|
||||
import com.tangem.features.onramp.alloffers.DefaultAllOffersComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AllOffersComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAllOffersComponentFactory(factory: DefaultAllOffersComponent.Factory): AllOffersComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.onramp.alloffers.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
|
||||
internal interface AllOffersIntents {
|
||||
|
||||
fun onPaymentMethodClicked(paymentMethodId: String)
|
||||
|
||||
fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM)
|
||||
|
||||
fun onBackClicked()
|
||||
|
||||
fun onRefresh()
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.features.onramp.alloffers.entity
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.*
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class AllOffersStateFactory(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val currentStateProvider: Provider<AllOffersStateUM>,
|
||||
private val allOffersIntents: AllOffersIntents,
|
||||
) {
|
||||
|
||||
fun getLoadedPaymentsState(methodGroups: List<OnrampPaymentMethodGroup>): AllOffersStateUM {
|
||||
return AllOffersStateUM.Content(
|
||||
methods = methodGroups.map { methodGroup ->
|
||||
AllOffersPaymentMethodUM(
|
||||
offers = mapOffersToUM(methodGroup.offers).toPersistentList(),
|
||||
methodConfig = OnrampPaymentMethodConfig(
|
||||
method = methodGroup.paymentMethod,
|
||||
onClick = { allOffersIntents.onPaymentMethodClicked(methodGroup.paymentMethod.id) },
|
||||
),
|
||||
diff = methodGroup
|
||||
.bestRateOffer
|
||||
?.rateDif
|
||||
?.takeIf { it > BigDecimal.ZERO }
|
||||
?.let { diff ->
|
||||
stringReference("$MINUS${diff.format { percent() }}")
|
||||
},
|
||||
rate = methodGroup.bestRateOffer?.let { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> quote.toAmount.value.format {
|
||||
crypto(
|
||||
symbol = quote.toAmount.symbol,
|
||||
decimals = quote.toAmount.decimals,
|
||||
)
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
} ?: "",
|
||||
providersCount = methodGroup.providerCount,
|
||||
isBestRate = methodGroup.isBestPaymentMethod,
|
||||
)
|
||||
}.toPersistentList(),
|
||||
currentMethod = null,
|
||||
onBackClicked = { allOffersIntents.onBackClicked() },
|
||||
)
|
||||
}
|
||||
|
||||
fun getPaymentsState(): AllOffersStateUM {
|
||||
return when (val currentState = currentStateProvider.invoke()) {
|
||||
is AllOffersStateUM.Content -> {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened)
|
||||
currentState.copy(currentMethod = null)
|
||||
}
|
||||
AllOffersStateUM.Loading,
|
||||
is AllOffersStateUM.Error,
|
||||
-> currentState
|
||||
}
|
||||
}
|
||||
|
||||
fun getOnrampErrorState(onrampError: OnrampError): AllOffersStateUM {
|
||||
return when (onrampError) {
|
||||
is OnrampError.DataError -> getErrorState(
|
||||
errorCode = onrampError.code,
|
||||
onRefresh = allOffersIntents::onRefresh,
|
||||
)
|
||||
OnrampError.PairsNotFound,
|
||||
is OnrampError.DomainError,
|
||||
-> getErrorState(onRefresh = allOffersIntents::onRefresh)
|
||||
is OnrampError.AmountError.TooBigError,
|
||||
is OnrampError.AmountError.TooSmallError,
|
||||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
-> currentStateProvider()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): AllOffersStateUM {
|
||||
val state = currentStateProvider()
|
||||
return when (state) {
|
||||
is AllOffersStateUM.Content,
|
||||
AllOffersStateUM.Loading,
|
||||
-> AllOffersStateUM.Error(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
)
|
||||
is AllOffersStateUM.Error -> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM {
|
||||
return when (advantages) {
|
||||
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
|
||||
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
|
||||
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapOffersToUM(offers: List<OnrampOffer>): List<OnrampOfferUM> {
|
||||
return buildList {
|
||||
offers.forEach { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
add(
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||
paymentMethod = quote.paymentMethod,
|
||||
providerId = quote.provider.id,
|
||||
providerName = quote.provider.info.name,
|
||||
rate = quote.toAmount.value.format {
|
||||
crypto(
|
||||
symbol = quote.toAmount.symbol,
|
||||
decimals = quote.toAmount.decimals,
|
||||
)
|
||||
},
|
||||
diff = offer
|
||||
.rateDif
|
||||
?.takeIf { it > BigDecimal.ZERO }
|
||||
?.let { diff ->
|
||||
stringReference("$MINUS${diff.format { percent() }}")
|
||||
},
|
||||
onBuyClicked = {
|
||||
allOffersIntents.onBuyClick(
|
||||
quote = OnrampProviderWithQuote.Data(
|
||||
provider = quote.provider,
|
||||
paymentMethod = quote.paymentMethod,
|
||||
toAmount = quote.toAmount,
|
||||
fromAmount = quote.fromAmount,
|
||||
),
|
||||
onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
is OnrampQuote.AmountError,
|
||||
is OnrampQuote.Error,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.onramp.alloffers.entity
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed interface AllOffersStateUM {
|
||||
|
||||
data object Loading : AllOffersStateUM
|
||||
|
||||
data class Content(
|
||||
val methods: ImmutableList<AllOffersPaymentMethodUM>,
|
||||
val currentMethod: AllOffersPaymentMethodUM? = null,
|
||||
val onBackClicked: () -> Unit,
|
||||
) : AllOffersStateUM
|
||||
|
||||
data class Error(val errorNotification: NotificationUM) : AllOffersStateUM
|
||||
}
|
||||
|
||||
internal data class AllOffersPaymentMethodUM(
|
||||
val offers: ImmutableList<OnrampOfferUM>,
|
||||
val methodConfig: OnrampPaymentMethodConfig,
|
||||
val diff: TextReference?,
|
||||
val rate: String,
|
||||
val providersCount: Int,
|
||||
val isBestRate: Boolean,
|
||||
)
|
||||
|
||||
internal data class OnrampPaymentMethodConfig(
|
||||
val method: OnrampPaymentMethod,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package com.tangem.features.onramp.alloffers.model
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.onramp.GetOnrampAllOffersUseCase
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.features.onramp.alloffers.AllOffersComponent
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersIntents
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AllOffersModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getOnrampAllOffersUseCase: GetOnrampAllOffersUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(), AllOffersIntents {
|
||||
|
||||
private var quotesJob: Job? = null
|
||||
|
||||
private val stateFactory: AllOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
AllOffersStateFactory(
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
currentStateProvider = Provider { state.value },
|
||||
allOffersIntents = this,
|
||||
)
|
||||
}
|
||||
|
||||
private val params: AllOffersComponent.Params = paramsContainer.require()
|
||||
|
||||
private val _state: MutableStateFlow<AllOffersStateUM> = MutableStateFlow(AllOffersStateUM.Loading)
|
||||
val state: StateFlow<AllOffersStateUM> = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
subscribeOnAllOffers()
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened)
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.AllOffersClicked)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
params.onDismiss()
|
||||
}
|
||||
|
||||
override fun onPaymentMethodClicked(paymentMethodId: String) {
|
||||
val contentState = state.value as? AllOffersStateUM.Content ?: return
|
||||
val method = contentState.methods.firstOrNull { it.methodConfig.method.id == paymentMethodId } ?: return
|
||||
analyticsEventHandler.send(
|
||||
event = OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = method.methodConfig.method.name),
|
||||
)
|
||||
_state.update { contentState.copy(currentMethod = method) }
|
||||
}
|
||||
|
||||
override fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) {
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.OnBuyClick(
|
||||
providerName = quote.provider.info.name,
|
||||
currency = params.amountCurrencyCode,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
onrampOfferAdvantagesUM.toAnalyticsEvent(
|
||||
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethodName = quote.paymentMethod.name,
|
||||
)?.let { analyticsEventHandler::send }
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
|
||||
override fun onBackClicked() {
|
||||
_state.update { stateFactory.getPaymentsState() }
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
subscribeOnAllOffers()
|
||||
}
|
||||
|
||||
private fun subscribeOnAllOffers() {
|
||||
quotesJob?.cancel()
|
||||
quotesJob = modelScope.launch(dispatchers.default) {
|
||||
getOnrampAllOffersUseCase.invoke(
|
||||
userWalletId = params.userWallet.walletId,
|
||||
cryptoCurrencyId = params.cryptoCurrency.id,
|
||||
).collectLatest { maybeOffers ->
|
||||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offersGroup ->
|
||||
_state.update { stateFactory.getLoadedPaymentsState(offersGroup) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
_state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
package com.tangem.features.onramp.alloffers.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.mainv2.ui.Offer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
internal fun AllOffersContentSheet(state: AllOffersStateUM, onCloseClick: () -> Unit) {
|
||||
val onBack = remember(state) {
|
||||
{
|
||||
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
|
||||
state.onBackClicked()
|
||||
} else {
|
||||
onCloseClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onCloseClick,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
onBack = onBack,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
title = {
|
||||
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
|
||||
ProviderTitle(
|
||||
onCloseClick = onCloseClick,
|
||||
onBackClick = onBack,
|
||||
)
|
||||
} else {
|
||||
PaymentMethodTitle(onCloseClick = onCloseClick)
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 8.dp)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = state is AllOffersStateUM.Content && state.currentMethod != null,
|
||||
transitionSpec = {
|
||||
fadeIn(tween(durationMillis = 220)) togetherWith
|
||||
fadeOut(tween(durationMillis = 220))
|
||||
},
|
||||
label = "Change offers and payment method state",
|
||||
) { shouldShowOffersScreen ->
|
||||
when (state) {
|
||||
AllOffersStateUM.Loading -> AllOffersContentLoading()
|
||||
is AllOffersStateUM.Error -> AllOffersError(state.errorNotification)
|
||||
is AllOffersStateUM.Content -> {
|
||||
if (shouldShowOffersScreen) {
|
||||
state.currentMethod?.let {
|
||||
OffersBasedOnPaymentMethodContent(offers = it.offers)
|
||||
}
|
||||
} else {
|
||||
PaymentMethodsContent(methods = state.methods)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProviderTitle(onBackClick: () -> Unit, onCloseClick: () -> Unit) {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = TextReference.Res(R.string.onramp_all_offers_button_title),
|
||||
subtitle = TextReference.Res(R.string.express_choose_providers_title),
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onBackClick,
|
||||
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
|
||||
onEndClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentMethodTitle(onCloseClick: () -> Unit) {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = TextReference.Res(R.string.onramp_all_offers_button_title),
|
||||
subtitle = TextReference.Res(R.string.onramp_payment_method_subtitle),
|
||||
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
|
||||
onEndClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OffersBasedOnPaymentMethodContent(offers: ImmutableList<OnrampOfferUM>) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
offers.fastForEach { offer ->
|
||||
key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") {
|
||||
Offer(offer)
|
||||
SpacerH(8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AllOffersContentLoading() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.height(96.dp)
|
||||
.size(width = 76.dp, height = 20.dp),
|
||||
radius = 14.dp,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.height(96.dp)
|
||||
.size(width = 76.dp, height = 20.dp),
|
||||
radius = 14.dp,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.height(96.dp)
|
||||
.size(width = 76.dp, height = 20.dp),
|
||||
radius = 14.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AllOffersError(errorNotification: NotificationUM) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Notification(config = errorNotification.config)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun AllOffersContentSheetPaymentPreview() {
|
||||
val method = AllOffersPaymentMethodUM(
|
||||
offers = persistentListOf(
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||
paymentMethod = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
providerId = "providerId1",
|
||||
providerName = "Simplex",
|
||||
rate = "0,0245334 BTC",
|
||||
diff = null,
|
||||
onBuyClicked = {},
|
||||
),
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.Default,
|
||||
paymentMethod = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
providerId = "providerId2",
|
||||
providerName = "Simplex",
|
||||
rate = "0,00145334 BTC",
|
||||
diff = stringReference("–0.07%"),
|
||||
onBuyClicked = {},
|
||||
),
|
||||
),
|
||||
methodConfig = OnrampPaymentMethodConfig(
|
||||
method = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
diff = null,
|
||||
rate = "0,0245334 BTC",
|
||||
providersCount = 2,
|
||||
isBestRate = true,
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
AllOffersContentSheet(
|
||||
state = AllOffersStateUM.Content(
|
||||
methods = persistentListOf(),
|
||||
currentMethod = method,
|
||||
onBackClicked = {},
|
||||
),
|
||||
onCloseClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun AllOffersContentSheetOffersPreview() {
|
||||
val methods = List(5) {
|
||||
AllOffersPaymentMethodUM(
|
||||
offers = persistentListOf(
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||
paymentMethod = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl =
|
||||
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
providerId = "providerId1",
|
||||
providerName = "Simplex",
|
||||
rate = "0,0245334 BTC",
|
||||
diff = null,
|
||||
onBuyClicked = {},
|
||||
),
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.Default,
|
||||
paymentMethod = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl =
|
||||
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
providerId = "providerId2",
|
||||
providerName = "Simplex",
|
||||
rate = "0,00145334 BTC",
|
||||
diff = stringReference("–0.07%"),
|
||||
onBuyClicked = {},
|
||||
),
|
||||
),
|
||||
methodConfig = OnrampPaymentMethodConfig(
|
||||
method = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
diff = null,
|
||||
rate = "0,0245334 BTC",
|
||||
providersCount = 2,
|
||||
isBestRate = true,
|
||||
)
|
||||
}
|
||||
|
||||
TangemThemePreview {
|
||||
AllOffersContentSheet(
|
||||
state = AllOffersStateUM.Content(
|
||||
methods = methods.toPersistentList(),
|
||||
currentMethod = null,
|
||||
onBackClicked = {},
|
||||
),
|
||||
onCloseClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
package com.tangem.features.onramp.alloffers.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.mainv2.ui.TimingBlock
|
||||
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun PaymentMethodsContent(methods: ImmutableList<AllOffersPaymentMethodUM>) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
methods.fastForEach { method ->
|
||||
key(method.methodConfig.method.id) {
|
||||
PaymentMethod(methodUM = method)
|
||||
SpacerH(8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
)
|
||||
.clickable(
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = methodUM.methodConfig.onClick,
|
||||
)
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 14.dp,
|
||||
bottom = 12.dp,
|
||||
),
|
||||
) {
|
||||
PaymentMethodIcon(
|
||||
modifier = Modifier.size(36.dp),
|
||||
imageUrl = methodUM.methodConfig.method.imageUrl,
|
||||
)
|
||||
|
||||
SpacerW(12.dp)
|
||||
|
||||
Column {
|
||||
PaymentMethodInfoBlock(
|
||||
paymentMethodName = methodUM.methodConfig.method.name,
|
||||
rate = methodUM.rate,
|
||||
diff = methodUM.diff,
|
||||
isBestRate = methodUM.isBestRate,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ProvidersCountBlockInfo(providersCount = methodUM.providersCount)
|
||||
SpacerW(8.dp)
|
||||
TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentMethodInfoBlock(
|
||||
paymentMethodName: String,
|
||||
rate: String,
|
||||
diff: TextReference?,
|
||||
isBestRate: Boolean,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(bottom = 14.dp)) {
|
||||
Text(
|
||||
text = paymentMethodName,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH(2.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_up_to_rate),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = rate,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
when {
|
||||
isBestRate -> {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_12),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
diff != null -> {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.text.warning.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
)
|
||||
.padding(horizontal = 4.dp),
|
||||
text = diff.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProvidersCountBlockInfo(providersCount: Int) {
|
||||
BorderedRow {
|
||||
Icon(
|
||||
modifier = Modifier.size(10.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = pluralStringResourceSafe(
|
||||
id = R.plurals.onramp_providers_count,
|
||||
count = providersCount,
|
||||
providersCount,
|
||||
),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimingBlockInfo(speed: PaymentMethodType.PaymentSpeed) {
|
||||
BorderedRow {
|
||||
Icon(
|
||||
modifier = Modifier.size(10.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
TimingBlock(speed)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BorderedRow(content: @Composable RowScope.() -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
)
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PaymentMethodsContentPreview() {
|
||||
val method = AllOffersPaymentMethodUM(
|
||||
offers = persistentListOf(
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||
paymentMethod = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
providerId = "providerId1",
|
||||
providerName = "Simplex",
|
||||
rate = "0,0245334 BTC",
|
||||
diff = null,
|
||||
onBuyClicked = {},
|
||||
),
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.Default,
|
||||
paymentMethod = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
providerId = "providerId2",
|
||||
providerName = "Simplex",
|
||||
rate = "0,00145334 BTC",
|
||||
diff = stringReference("–0.07%"),
|
||||
onBuyClicked = {},
|
||||
),
|
||||
),
|
||||
methodConfig = OnrampPaymentMethodConfig(
|
||||
method = OnrampPaymentMethod(
|
||||
id = "card",
|
||||
name = "Card",
|
||||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
diff = null,
|
||||
rate = "0,0245334 BTC",
|
||||
providersCount = 2,
|
||||
isBestRate = true,
|
||||
)
|
||||
TangemThemePreview {
|
||||
PaymentMethodsContent(persistentListOf(method))
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ internal interface OnrampMainComponent : ComposableContentComponent {
|
|||
val source: OnrampSource,
|
||||
val openSettings: () -> Unit,
|
||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||
val launchSepa: Boolean,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnrampMainComponent>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue