Updated on 2026-08-14
This commit is contained in:
commit
a651da10e5
143 changed files with 3655 additions and 899 deletions
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.tap.common.redux.global.GlobalMiddleware
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
|
||||
import com.tangem.tap.features.details.redux.DetailsMiddleware
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
|
||||
|
|
@ -20,12 +18,10 @@ data class AppState(
|
|||
fun getMiddleware(): List<Middleware<AppState>> {
|
||||
return listOf(
|
||||
logMiddleware,
|
||||
GlobalMiddleware.handler,
|
||||
DetailsMiddleware().detailsMiddleware,
|
||||
LockUserWalletsTimerMiddleware().middleware,
|
||||
AccessCodeRequestPolicyMiddleware().middleware,
|
||||
DaggerGraphMiddleware.daggerGraphMiddleware,
|
||||
LegacyMiddleware.legacyMiddleware,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -8,10 +7,5 @@ sealed class GlobalAction : Action {
|
|||
|
||||
data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
|
||||
|
||||
data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction()
|
||||
object RestoreAppCurrency : GlobalAction() {
|
||||
data class Success(val appCurrency: AppCurrency) : GlobalAction()
|
||||
}
|
||||
|
||||
data class IsSignWithRing(val isSignWithRing: Boolean) : GlobalAction()
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
object GlobalMiddleware {
|
||||
val handler = globalMiddlewareHandler
|
||||
}
|
||||
|
||||
private val globalMiddlewareHandler: Middleware<AppState> = { _, _ ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
handleAction(action)
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(action: Action) {
|
||||
when (action) {
|
||||
is GlobalAction.RestoreAppCurrency -> restoreAppCurrency()
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreAppCurrency() {
|
||||
scope.launch {
|
||||
val currency = store.inject(DaggerGraphState::appCurrencyRepository)
|
||||
.getSelectedAppCurrency()
|
||||
.firstOrNull()
|
||||
?: AppCurrency.Default
|
||||
|
||||
store.dispatchWithMain(GlobalAction.RestoreAppCurrency.Success(currency))
|
||||
}
|
||||
}
|
||||
|
|
@ -13,13 +13,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
is GlobalAction.SaveScanResponse -> {
|
||||
globalState.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is GlobalAction.ChangeAppCurrency -> {
|
||||
globalState.copy(appCurrency = action.appCurrency)
|
||||
}
|
||||
is GlobalAction.RestoreAppCurrency.Success -> {
|
||||
globalState.copy(appCurrency = action.appCurrency)
|
||||
}
|
||||
is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing)
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.TapWalletManager
|
||||
import org.rekotlin.StateType
|
||||
|
|
@ -9,7 +8,6 @@ data class GlobalState(
|
|||
@Deprecated("Use scan response from selected user wallet")
|
||||
val scanResponse: ScanResponse? = null,
|
||||
val tapWalletManager: TapWalletManager = TapWalletManager(),
|
||||
val appCurrency: AppCurrency = AppCurrency.Default,
|
||||
val isLastSignWithRing: Boolean = false,
|
||||
) : StateType
|
||||
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
package com.tangem.tap.common.redux.legacy
|
||||
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.redux.AppSettingsState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
@Suppress("MemberNameEqualsClassName")
|
||||
internal object LegacyMiddleware {
|
||||
private val prepareDetailsScreenJobHolder = JobHolder()
|
||||
|
||||
val legacyMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is LegacyAction.PrepareDetailsScreen -> {
|
||||
selectedUserWallet()
|
||||
.distinctUntilChanged { old, new ->
|
||||
if (old is UserWallet.Cold && new is UserWallet.Cold) {
|
||||
old.walletId == new.walletId &&
|
||||
old.scanResponse == new.scanResponse
|
||||
} else {
|
||||
old.walletId == new.walletId
|
||||
}
|
||||
}
|
||||
.onEach { selectedUserWallet ->
|
||||
val initializedAppSettingsStateContent = initializeAppSettingsState()
|
||||
store.dispatchWithMain(
|
||||
DetailsAction.PrepareScreen(
|
||||
scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse,
|
||||
initializedAppSettingsState = initializedAppSettingsStateContent,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.launchIn(scope)
|
||||
.saveIn(prepareDetailsScreenJobHolder)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectedUserWallet(): Flow<UserWallet> {
|
||||
return store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking
|
||||
* previously it was initialized in runBlocking and blocked details screen
|
||||
*/
|
||||
private suspend fun initializeAppSettingsState(): AppSettingsState {
|
||||
return AppSettingsState(
|
||||
selectedAppCurrency = store.state.globalState.appCurrency,
|
||||
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
|
||||
?: AppThemeMode.DEFAULT,
|
||||
requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(),
|
||||
useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(),
|
||||
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
|
||||
.getBalanceHidingSettings().isHidingEnabledInSettings,
|
||||
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
|
||||
hasSecuredWallets = store.inject(DaggerGraphState::userWalletsListRepository).hasSecuredWallets(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase
|
||||
import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase
|
||||
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
|
||||
import com.tangem.domain.dynamicaddresses.GetDynamicAddressesStatusUseCase
|
||||
import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase
|
||||
import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase
|
||||
import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase
|
||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object DynamicAddressesDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEnableDynamicAddressesUseCase(
|
||||
dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
): EnableDynamicAddressesUseCase {
|
||||
return EnableDynamicAddressesUseCase(dynamicAddressesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDisableDynamicAddressesUseCase(
|
||||
dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
): DisableDynamicAddressesUseCase {
|
||||
return DisableDynamicAddressesUseCase(dynamicAddressesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetDynamicAddressesStatusUseCase(
|
||||
dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
): GetDynamicAddressesStatusUseCase {
|
||||
return GetDynamicAddressesStatusUseCase(dynamicAddressesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetDynamicReceiveAddressUseCase(
|
||||
dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
): GetDynamicReceiveAddressUseCase {
|
||||
return GetDynamicReceiveAddressUseCase(dynamicAddressesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCreateConsolidationTransactionUseCase(
|
||||
consolidationRepository: ConsolidationRepository,
|
||||
): CreateConsolidationTransactionUseCase {
|
||||
return CreateConsolidationTransactionUseCase(consolidationRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsXpubSupportedUseCase(walletManagersFacade: WalletManagersFacade): IsXpubSupportedUseCase {
|
||||
return IsXpubSupportedUseCase(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsXpubDerivedUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
derivationsRepository: DerivationsRepository,
|
||||
): IsXpubDerivedUseCase {
|
||||
return IsXpubDerivedUseCase(walletManagersFacade, derivationsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.staking.*
|
||||
import com.tangem.domain.staking.repositories.*
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
||||
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
@ -226,8 +227,14 @@ internal object StakingDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory {
|
||||
return StakingIdFactory(walletManagersFacade = walletManagersFacade)
|
||||
fun provideStakingIdFactory(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
): StakingIdFactory {
|
||||
return StakingIdFactory(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class FinalizeTwinTask(
|
|||
visaCardScanHandler = null,
|
||||
visaCoroutineScope = null,
|
||||
shouldCheckIsAlreadyActivated = false,
|
||||
isDynamicAddressesEnabled = false,
|
||||
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
||||
onboardingV2FeatureToggles = null,
|
||||
).run(session, callback)
|
||||
is CompletionResult.Failure ->
|
||||
|
|
|
|||
|
|
@ -2,18 +2,12 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.rekotlin.Action
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
sealed class DetailsAction : Action {
|
||||
|
||||
data class PrepareScreen(
|
||||
val scanResponse: ScanResponse?,
|
||||
val initializedAppSettingsState: AppSettingsState,
|
||||
) : DetailsAction()
|
||||
|
||||
sealed class AppSettings : DetailsAction() {
|
||||
data class SwitchPrivacySetting(
|
||||
val enable: Boolean,
|
||||
|
|
@ -50,6 +44,4 @@ sealed class DetailsAction : Action {
|
|||
|
||||
data class Prepare(val state: AppSettingsState) : AppSettings()
|
||||
}
|
||||
|
||||
data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction()
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import com.tangem.tap.common.analytics.events.Settings
|
|||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
|
|
@ -78,10 +77,7 @@ class DetailsMiddleware {
|
|||
is DetailsAction.AppSettings.ChangeBalanceHiding -> {
|
||||
changeBalanceHiding(action.shouldHideBalance)
|
||||
}
|
||||
is DetailsAction.AppSettings.ChangeAppCurrency -> {
|
||||
store.dispatch(GlobalAction.ChangeAppCurrency(action.currency))
|
||||
store.dispatch(DetailsAction.ChangeAppCurrency(action.currency))
|
||||
}
|
||||
is DetailsAction.AppSettings.ChangeAppCurrency,
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
|
||||
is DetailsAction.AppSettings.BiometricsStatusChanged,
|
||||
|
|
|
|||
|
|
@ -12,27 +12,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
|||
if (action !is DetailsAction) return state.detailsState
|
||||
val detailsState = state.detailsState
|
||||
return when (action) {
|
||||
is DetailsAction.PrepareScreen -> {
|
||||
handlePrepareScreen(action)
|
||||
}
|
||||
is DetailsAction.AppSettings -> {
|
||||
handlePrivacyAction(action, detailsState)
|
||||
}
|
||||
is DetailsAction.ChangeAppCurrency -> detailsState.copy(
|
||||
appSettingsState = detailsState.appSettingsState.copy(
|
||||
selectedAppCurrency = action.currency,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
|
||||
return DetailsState(
|
||||
scanResponse = action.scanResponse,
|
||||
appSettingsState = action.initializedAppSettingsState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
|
||||
return when (action) {
|
||||
|
|
@ -94,6 +79,7 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
|||
useBiometricAuthentication = action.state.useBiometricAuthentication,
|
||||
requireAccessCode = action.state.requireAccessCode,
|
||||
hasSecuredWallets = action.state.hasSecuredWallets,
|
||||
needEnrollBiometrics = action.state.needEnrollBiometrics,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.EnrollBiometrics,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class DetailsState(
|
||||
@Deprecated("Delete after onboarding refactoring")
|
||||
val scanResponse: ScanResponse? = null,
|
||||
val appSettingsState: AppSettingsState = AppSettingsState(),
|
||||
) : StateType
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
|
|||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
|
|
@ -54,6 +55,7 @@ internal class AppSettingsModel @Inject constructor(
|
|||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appThemeModeRepository: AppThemeModeRepository,
|
||||
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model(), StoreSubscriber<DetailsState> {
|
||||
|
||||
|
|
@ -251,9 +253,8 @@ internal class AppSettingsModel @Inject constructor(
|
|||
private fun bootstrapAppCurrencyUpdates() {
|
||||
appCurrencyRepository
|
||||
.getSelectedAppCurrency()
|
||||
.distinctUntilChanged()
|
||||
.onEach { appCurrency ->
|
||||
if (appCurrency.code == store.state.globalState.appCurrency.code) return@onEach
|
||||
|
||||
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(appCurrency))
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
|
@ -267,6 +268,7 @@ internal class AppSettingsModel @Inject constructor(
|
|||
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
|
||||
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
|
||||
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,
|
||||
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
|
||||
hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ class AccountCryptoPortfolioItemStateConverter(
|
|||
private val priceChangeLce: Lce<Unit, PriceChange>? = null,
|
||||
private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null,
|
||||
private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null,
|
||||
private val fiatAmountStateProvider: ((TotalFiatBalance) -> FiatAmountState?)? = null,
|
||||
private val subtitle2StateProvider: ((Lce<Unit, PriceChange>) -> Subtitle2State?)? = null,
|
||||
) : Converter<TotalFiatBalance, TokenItemState> {
|
||||
|
||||
override fun convert(value: TotalFiatBalance): TokenItemState {
|
||||
|
|
@ -40,11 +42,11 @@ class AccountCryptoPortfolioItemStateConverter(
|
|||
private fun Account.CryptoPortfolio.mapToContentState(
|
||||
fiatBalance: TotalFiatBalance.Loaded,
|
||||
): TokenItemState.Content {
|
||||
val subtitle2State = priceChangeLce?.fold(
|
||||
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
|
||||
ifError = { null },
|
||||
ifContent = { priceChange -> priceChange.toSubtitle2State() },
|
||||
)
|
||||
val subtitle2State = if (priceChangeLce != null && subtitle2StateProvider != null) {
|
||||
subtitle2StateProvider(priceChangeLce)
|
||||
} else {
|
||||
createSubtitle2State(priceChangeLce)
|
||||
}
|
||||
return TokenItemState.Content(
|
||||
id = account.accountId.toItemId(),
|
||||
iconState = AccountIconItemStateConverter().convert(this),
|
||||
|
|
@ -59,11 +61,8 @@ class AccountCryptoPortfolioItemStateConverter(
|
|||
),
|
||||
isAvailable = false,
|
||||
),
|
||||
fiatAmountState = FiatAmountState.Content(
|
||||
text = fiatBalance.amount
|
||||
.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
|
||||
isFlickering = fiatBalance.source == StatusSource.CACHE,
|
||||
),
|
||||
fiatAmountState = fiatAmountStateProvider?.invoke(fiatBalance)
|
||||
?: createFiatAmountState(fiatBalance, appCurrency),
|
||||
subtitle2State = subtitle2State,
|
||||
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
|
||||
onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } },
|
||||
|
|
@ -127,4 +126,30 @@ class AccountCryptoPortfolioItemStateConverter(
|
|||
type = this.value.getPriceChangeType(),
|
||||
isFlickering = this.source.isFlickering(),
|
||||
)
|
||||
|
||||
private fun createFiatAmountState(fiatBalance: TotalFiatBalance, appCurrency: AppCurrency): FiatAmountState {
|
||||
return when (fiatBalance) {
|
||||
TotalFiatBalance.Failed,
|
||||
TotalFiatBalance.Loading,
|
||||
-> FiatAmountState.Empty
|
||||
|
||||
is TotalFiatBalance.Loaded -> FiatAmountState.Content(
|
||||
text = fiatBalance.amount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
},
|
||||
isFlickering = fiatBalance.source == StatusSource.CACHE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSubtitle2State(priceChangeLce: Lce<Unit, PriceChange>?): Subtitle2State? {
|
||||
return priceChangeLce?.fold(
|
||||
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
|
||||
ifError = { null },
|
||||
ifContent = { priceChange -> priceChange.toSubtitle2State() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.common.ui.navigationButtons
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
sealed class NavigationButtonsState {
|
||||
data object Empty : NavigationButtonsState()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.account.AccountCharIcon
|
||||
import com.tangem.core.ui.components.account.AccountResIcon
|
||||
import com.tangem.core.ui.components.account.PaymentAccountIcon
|
||||
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -61,6 +62,7 @@ internal fun ContentIcon(
|
|||
background = icon.background,
|
||||
alpha = alpha,
|
||||
)
|
||||
is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(modifier = modifier, size = icon.size)
|
||||
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
|
||||
modifier = modifier,
|
||||
resId = icon.resId,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ fun CurrencyIcon(
|
|||
is CurrencyIconState.FiatIcon,
|
||||
is CurrencyIconState.CustomTokenIcon,
|
||||
is CurrencyIconState.TokenIcon,
|
||||
is CurrencyIconState.PaymentAccount,
|
||||
is CurrencyIconState.CryptoPortfolio.Icon,
|
||||
is CurrencyIconState.CryptoPortfolio.Letter,
|
||||
-> {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@ sealed class CurrencyIconState {
|
|||
override val topBadgeIconResId: Int? = null
|
||||
}
|
||||
|
||||
data class PaymentAccount(val size: AccountIconSize = AccountIconSize.Default) : CurrencyIconState() {
|
||||
override val isGrayscale: Boolean = false
|
||||
override val shouldShowCustomBadge: Boolean = false
|
||||
override val topBadgeIconResId: Int? = null
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class CryptoPortfolio : CurrencyIconState() {
|
||||
override val shouldShowCustomBadge: Boolean = false
|
||||
|
|
@ -155,6 +161,7 @@ sealed class CurrencyIconState {
|
|||
is CryptoPortfolio.Letter -> copy(
|
||||
isGrayscale = isGrayscale,
|
||||
)
|
||||
is PaymentAccount,
|
||||
is Loading,
|
||||
is Locked,
|
||||
is Empty,
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ data class SearchBarUM(
|
|||
val isActive: Boolean,
|
||||
val onActiveChange: (Boolean) -> Unit,
|
||||
val onClearClick: () -> Unit = {},
|
||||
val onCancelClick: (() -> Unit)? = null,
|
||||
)
|
||||
|
|
@ -2,7 +2,8 @@ package com.tangem.core.ui.components.tokenlist
|
|||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -24,6 +25,7 @@ import com.tangem.core.ui.components.TextShimmer
|
|||
import com.tangem.core.ui.components.account.AccountCharIcon
|
||||
import com.tangem.core.ui.components.account.AccountIconSize
|
||||
import com.tangem.core.ui.components.account.AccountResIcon
|
||||
import com.tangem.core.ui.components.account.PaymentAccountIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
|
|
@ -98,6 +100,9 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
|
|||
icon.copy(
|
||||
size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default,
|
||||
)
|
||||
is CurrencyIconState.PaymentAccount -> icon.copy(
|
||||
size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default,
|
||||
)
|
||||
else -> icon
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +188,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun ExpandedPortfolioHeader(
|
||||
state: TokenItemState,
|
||||
|
|
@ -209,6 +214,7 @@ fun ExpandedPortfolioHeader(
|
|||
composables.icon.invoke(Modifier)
|
||||
} else {
|
||||
when (val icon = state.iconState) {
|
||||
is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(size = icon.size)
|
||||
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
|
||||
resId = icon.resId,
|
||||
color = icon.color,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ sealed interface TokensListItemUM {
|
|||
is PortfolioItemContentUM.Tokens -> content.tokens
|
||||
is PortfolioItemContentUM.Empty -> persistentListOf()
|
||||
}
|
||||
|
||||
val tokensItemsList: List<Token>
|
||||
get() = when (content) {
|
||||
is PortfolioItemContentUM.Tokens -> content.tokens.filterIsInstance<Token>()
|
||||
is PortfolioItemContentUM.Empty -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
data class Text(override val id: Any, val text: TextReference) : TokensListItemUM
|
||||
|
|
|
|||
|
|
@ -289,8 +289,13 @@ private fun CancelButton(
|
|||
state.onQueryChange("")
|
||||
}
|
||||
keyboardController?.hide()
|
||||
state.onClearClick()
|
||||
focusManager.clearFocus()
|
||||
val onCancel = state.onCancelClick
|
||||
if (onCancel != null) {
|
||||
onCancel()
|
||||
} else {
|
||||
state.onClearClick()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurR
|
|||
blurRadius = blurRadius,
|
||||
)
|
||||
}
|
||||
is CurrencyIconState.PaymentAccount -> Unit
|
||||
CurrencyIconState.Loading -> Unit
|
||||
CurrencyIconState.Locked -> Unit
|
||||
}
|
||||
|
|
@ -193,7 +194,7 @@ private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) {
|
|||
AsyncImage(
|
||||
model = imageRequest,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.scale(SCALE_FACTOR)
|
||||
|
|
@ -207,7 +208,7 @@ private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) {
|
|||
Image(
|
||||
painter = painterResource(res),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.scale(SCALE_FACTOR)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11.999,1.25C13.68,1.25 15.047,1.818 16.24,2.436C16.6,2.622 16.935,2.807 17.255,2.983C18.038,3.415 18.733,3.797 19.495,4.04C19.942,4.183 20.338,4.31 20.619,4.42C20.883,4.523 21.198,4.665 21.42,4.913C21.621,5.136 21.723,5.385 21.792,5.624C21.854,5.841 21.907,6.135 21.958,6.417C23.196,13.282 20.492,19.787 13.881,22.318C13.232,22.567 12.753,22.75 12.002,22.75C11.251,22.75 10.772,22.567 10.123,22.318C3.512,19.788 0.806,13.283 2.043,6.417C2.094,6.135 2.147,5.841 2.209,5.624C2.277,5.385 2.38,5.135 2.58,4.913C2.802,4.665 3.117,4.524 3.381,4.42C3.662,4.31 4.059,4.183 4.506,4.04C5.268,3.797 5.961,3.415 6.744,2.983C7.063,2.807 7.398,2.622 7.758,2.436C8.95,1.818 10.317,1.25 11.999,1.25ZM17.942,8.666C17.757,8.146 17.186,7.873 16.666,8.058C15.788,8.369 14.948,8.961 14.212,9.607C13.465,10.262 12.766,11.023 12.172,11.734C11.733,12.261 11.345,12.769 11.03,13.199C10.741,12.854 10.453,12.601 10.179,12.418C9.905,12.235 9.552,12 9,12C8.447,12 8,12.448 8,13C8,13.521 8.397,13.949 8.906,13.996C9.11,14.015 9.664,14.566 10.105,15.447C10.266,15.77 10.589,15.981 10.949,15.999C11.309,16.017 11.65,15.84 11.843,15.536C11.849,15.528 12.193,15.012 12.357,14.78C12.688,14.311 13.157,13.677 13.708,13.016C14.262,12.352 14.887,11.675 15.53,11.111C16.183,10.539 16.8,10.132 17.333,9.943C17.854,9.759 18.126,9.187 17.942,8.666Z"
|
||||
android:fillColor="#0099FF"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="72dp"
|
||||
android:height="72dp"
|
||||
android:viewportWidth="72"
|
||||
android:viewportHeight="72">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M36,0L36,0A36,36 0,0 1,72 36L72,36A36,36 0,0 1,36 72L36,72A36,36 0,0 1,0 36L0,36A36,36 0,0 1,36 0z"/>
|
||||
<path
|
||||
android:pathData="M0,0h72v72h-72z"
|
||||
android:strokeAlpha="0.1"
|
||||
android:fillColor="#0099FF"
|
||||
android:fillAlpha="0.1"/>
|
||||
<path
|
||||
android:pathData="M24.607,31.142C23.778,32.206 22.682,31.815 22.291,30.454L20.319,23.678C20.006,22.598 20.663,21.753 21.79,21.8L28.848,22.035C30.256,22.081 30.882,23.067 30.069,24.116L28.66,25.915L28.723,25.947C31.602,27.856 35.123,31.831 35.968,34.46H36.031C36.86,31.846 40.397,27.856 43.276,25.947L43.354,25.9L41.915,23.912C41.132,22.833 41.774,21.862 43.198,21.878L50.256,21.847C51.382,21.847 52.008,22.723 51.664,23.803L49.473,30.501C49.051,31.846 47.924,32.206 47.126,31.095L45.608,28.998L45.264,29.217C42.118,31.22 37.893,36.15 37.893,39.734V48.106C37.893,49.467 37.22,50.203 36,50.203C34.779,50.203 34.122,49.467 34.122,48.106V39.734C34.122,36.15 29.865,31.22 26.735,29.217L26.313,28.951L24.607,31.142Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M28,0L28,0A28,28 0,0 1,56 28L56,28A28,28 0,0 1,28 56L28,56A28,28 0,0 1,0 28L0,28A28,28 0,0 1,28 0z"/>
|
||||
<path
|
||||
android:pathData="M0,0h56v56h-56z"
|
||||
android:strokeAlpha="0.1"
|
||||
android:fillColor="#FFB71B"
|
||||
android:fillAlpha="0.1"/>
|
||||
<path
|
||||
android:pathData="M41.646,35.377C41.646,37.37 40.305,38.906 38.091,38.906H17.909C15.696,38.906 14.354,37.37 14.354,35.377C14.354,34.765 14.511,34.14 14.862,33.555L24.966,15.95C25.631,14.766 26.802,14.167 28,14.167C29.198,14.167 30.357,14.766 31.034,15.95L41.138,33.555C41.464,34.127 41.646,34.765 41.646,35.377ZM28.013,30.208C27.336,30.208 26.959,29.817 26.946,29.127L26.776,22.044C26.763,21.354 27.271,20.859 28,20.859C28.703,20.859 29.25,21.367 29.237,22.057L29.042,29.127C29.029,29.831 28.651,30.208 28.013,30.208ZM28.013,34.57C27.232,34.57 26.555,33.945 26.555,33.177C26.555,32.396 27.219,31.771 28.013,31.771C28.795,31.771 29.459,32.383 29.459,33.177C29.459,33.958 28.782,34.57 28.013,34.57Z"
|
||||
android:fillColor="#FFB71B"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13.608,1.147C14.314,1.448 14.797,2.163 14.797,3.017L14.798,9.785C14.798,9.895 14.887,9.985 14.998,9.985H18.099C18.985,9.985 19.595,10.583 19.847,11.21C20.097,11.837 20.064,12.642 19.563,13.285L12.564,22.268C12.003,22.988 11.12,23.163 10.392,22.852C9.685,22.551 9.202,21.837 9.202,20.983L9.202,14.215C9.202,14.104 9.112,14.015 9.002,14.015H5.9C5.014,14.015 4.404,13.417 4.153,12.789C3.902,12.163 3.936,11.358 4.437,10.715L11.435,1.732C11.996,1.011 12.879,0.837 13.608,1.147Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
</vector>
|
||||
|
|
@ -30,6 +30,7 @@ dependencies {
|
|||
|
||||
// region Project - Libs
|
||||
implementation(tangemDeps.blockchain) { exclude(module = "joda-time") }
|
||||
implementation(tangemDeps.card.core)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.dynamicaddresses
|
||||
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
|
|
@ -10,6 +11,7 @@ import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
|||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -31,17 +33,20 @@ internal class DefaultDynamicAddressesRepository(
|
|||
.map { response ->
|
||||
val token = response.findToken(network)
|
||||
when {
|
||||
token?.dynamicAddressesEnabled == true -> DynamicAddressesStatus.ENABLED
|
||||
else -> DynamicAddressesStatus.DISABLED
|
||||
token?.dynamicAddressesEnabled != true -> DynamicAddressesStatus.DISABLED
|
||||
!isXpubAvailable(userWalletId, network) -> DynamicAddressesStatus.ENABLED_REQUIRES_SETUP
|
||||
else -> DynamicAddressesStatus.ENABLED
|
||||
}
|
||||
// TODO handle ENABLED_REQUIRES_SETUP when XPUB is not derived locally
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
override suspend fun enable(userWalletId: UserWalletId, network: Network, xpub: String) {
|
||||
withContext(dispatchers.io) {
|
||||
walletManagersFacade.enableXpubMode(userWalletId, network, xpub)
|
||||
val result = walletManagersFacade.enableXpubMode(userWalletId, network, xpub)
|
||||
if (result is SimpleResult.Failure) {
|
||||
error("Failed to enable xpub mode for $userWalletId / ${network.id}: ${result.error}")
|
||||
}
|
||||
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true)
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||
.onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) }
|
||||
|
|
@ -50,7 +55,10 @@ internal class DefaultDynamicAddressesRepository(
|
|||
|
||||
override suspend fun disable(userWalletId: UserWalletId, network: Network) {
|
||||
withContext(dispatchers.io) {
|
||||
walletManagersFacade.disableXpubMode(userWalletId, network)
|
||||
val result = walletManagersFacade.disableXpubMode(userWalletId, network)
|
||||
if (result is SimpleResult.Failure) {
|
||||
error("Failed to disable xpub mode for $userWalletId / ${network.id}: ${result.error}")
|
||||
}
|
||||
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false)
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||
.onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) }
|
||||
|
|
@ -70,6 +78,45 @@ internal class DefaultDynamicAddressesRepository(
|
|||
return walletManagersFacade.hasDynamicAddressesNonBaseBalances(userWalletId, network)
|
||||
}
|
||||
|
||||
override suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = walletAccountsFetcher.getSaved(userWalletId) ?: return@withContext false
|
||||
val baseDerivationPath = network.derivationPath.value ?: return@withContext false
|
||||
|
||||
response.accounts
|
||||
.flatMap { it.tokens.orEmpty() }
|
||||
.any { token ->
|
||||
val tokenDerivationPath = token.derivationPath ?: return@any false
|
||||
token.networkId == network.backendId &&
|
||||
tokenDerivationPath != baseDerivationPath &&
|
||||
hasNonZeroChangeOrIndex(tokenDerivationPath, baseDerivationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the token's derivation path has the same first 3 nodes (purpose/coin/account)
|
||||
* as the base path but different change/index nodes (not both 0).
|
||||
*/
|
||||
private fun hasNonZeroChangeOrIndex(tokenPath: String, basePath: String): Boolean {
|
||||
val tokenNodes = runCatching { DerivationPath(tokenPath).nodes }.getOrNull() ?: return false
|
||||
val baseNodes = runCatching { DerivationPath(basePath).nodes }.getOrNull() ?: return false
|
||||
|
||||
if (tokenNodes.size < DERIVATION_NODE_COUNT || baseNodes.size < DERIVATION_NODE_COUNT) return false
|
||||
|
||||
// First 3 nodes must match (purpose/coin/account) by value, ignoring hardening
|
||||
val isSameAccount = (0 until ACCOUNT_NODE_COUNT).all { i ->
|
||||
tokenNodes[i].getIndex(includeHardened = false) == baseNodes[i].getIndex(includeHardened = false)
|
||||
}
|
||||
if (!isSameAccount) return false
|
||||
|
||||
// Check if change or index ≠ 0
|
||||
val change = tokenNodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false)
|
||||
val index = tokenNodes[INDEX_NODE_INDEX].getIndex(includeHardened = false)
|
||||
|
||||
return change != 0L || index != 0L
|
||||
}
|
||||
|
||||
private suspend fun updateTokenDynamicAddressesFlag(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
@ -92,6 +139,11 @@ internal class DefaultDynamicAddressesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun isXpubAvailable(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
// Check if WalletManager is already in XPUB mode (DA was previously enabled on this device)
|
||||
return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network) != null
|
||||
}
|
||||
|
||||
private fun GetWalletAccountsResponse.findToken(network: Network): UserTokensResponse.Token? {
|
||||
return accounts
|
||||
.flatMap { it.tokens.orEmpty() }
|
||||
|
|
@ -103,4 +155,11 @@ internal class DefaultDynamicAddressesRepository(
|
|||
derivationPath == network.derivationPath.value &&
|
||||
contractAddress == null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DERIVATION_NODE_COUNT = 5
|
||||
const val ACCOUNT_NODE_COUNT = 3
|
||||
const val CHANGE_NODE_INDEX = 3
|
||||
const val INDEX_NODE_INDEX = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
|||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -65,7 +66,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
}
|
||||
|
||||
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
|
||||
val vaults = if (stakingFeatureToggles.isEthStakingEnabled) {
|
||||
val vaults = if (stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)) {
|
||||
getVaults(network).getOrElse { error ->
|
||||
TangemLogger.e("Error fetching P2PEthPool vaults: $error")
|
||||
emptyList()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.staking.store.StakeKitBalancesStore
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -22,14 +20,13 @@ import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
internal class DefaultStakingRepository(
|
||||
private val stakeKitRepository: StakeKitRepository,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
private val stakingBalanceStoreV2: StakeKitBalancesStore,
|
||||
private val stakeKitBalancesStore: StakeKitBalancesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
|
|
@ -40,7 +37,8 @@ internal class DefaultStakingRepository(
|
|||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<StakingAvailability> {
|
||||
return channelFlow {
|
||||
if (!checkFeatureToggleEnabled(cryptoCurrency)) {
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
if (stakingIntegration == null || !stakingFeatureToggles.isIntegrationEnabled(stakingIntegration)) {
|
||||
send(StakingAvailability.Unavailable)
|
||||
return@channelFlow
|
||||
}
|
||||
|
|
@ -56,8 +54,6 @@ internal class DefaultStakingRepository(
|
|||
return@channelFlow
|
||||
}
|
||||
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
|
||||
val availabilityFlow = when (stakingIntegration) {
|
||||
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
|
||||
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
|
||||
|
|
@ -65,7 +61,6 @@ internal class DefaultStakingRepository(
|
|||
rawCurrencyId,
|
||||
cryptoCurrency.symbol,
|
||||
)
|
||||
null -> flowOf(StakingAvailability.Unavailable)
|
||||
}
|
||||
|
||||
availabilityFlow.collect { send(it) }
|
||||
|
|
@ -76,20 +71,15 @@ internal class DefaultStakingRepository(
|
|||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): StakingAvailability {
|
||||
if (!checkFeatureToggleEnabled(cryptoCurrency)) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
?.takeIf(stakingFeatureToggles::isIntegrationEnabled)
|
||||
?: return StakingAvailability.Unavailable
|
||||
|
||||
if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId
|
||||
if (rawCurrencyId == null) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
?: return StakingAvailability.Unavailable
|
||||
|
||||
return when (stakingIntegration) {
|
||||
|
|
@ -104,7 +94,7 @@ internal class DefaultStakingRepository(
|
|||
|
||||
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
|
||||
return withContext(dispatchers.default) {
|
||||
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
|
||||
val balances = stakeKitBalancesStore.getAllSyncOrNull(userWalletId) ?: return@withContext false
|
||||
|
||||
val hasDataStakingBalance by lazy {
|
||||
balances.any { stakingBalance ->
|
||||
|
|
@ -116,18 +106,6 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkFeatureToggleEnabled(cryptoCurrency: CryptoCurrency): Boolean {
|
||||
return when (cryptoCurrency.network.id.toBlockchain()) {
|
||||
Blockchain.Ethereum -> {
|
||||
when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> stakingFeatureToggles.isEthStakingEnabled
|
||||
is CryptoCurrency.Token -> true
|
||||
}
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForInvalidCardBatch(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
|
||||
error("Failed to get user wallet")
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import com.tangem.data.staking.store.StakeKitBalancesStore
|
|||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -54,7 +54,7 @@ internal object StakingBalanceSupplierModule {
|
|||
fun provideSingleStakingBalanceSupplier(
|
||||
factory: SingleStakingBalanceProducer.Factory,
|
||||
): SingleStakingBalanceSupplier {
|
||||
return object : SingleStakingBalanceSupplier(
|
||||
return SingleStakingBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { params ->
|
||||
listOf(
|
||||
|
|
@ -65,15 +65,15 @@ internal object StakingBalanceSupplierModule {
|
|||
)
|
||||
.joinToString(separator = "_")
|
||||
},
|
||||
) {}
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMultiStakingBalanceSupplier(factory: MultiStakingBalanceProducer.Factory): MultiStakingBalanceSupplier {
|
||||
return object : MultiStakingBalanceSupplier(
|
||||
return MultiStakingBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "multi_staking_balances_${it.userWalletId.stringValue}" },
|
||||
) {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ internal object StakingDataModule {
|
|||
return DefaultStakingRepository(
|
||||
stakeKitRepository = stakeKitRepository,
|
||||
p2pEthPoolRepository = p2pEthPoolRepository,
|
||||
stakingBalanceStoreV2 = stakeKitBalancesStore,
|
||||
stakeKitBalancesStore = stakeKitBalancesStore,
|
||||
dispatchers = dispatchers,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,35 @@ package com.tangem.data.staking.toggles
|
|||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
|
||||
internal class DefaultStakingFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : StakingFeatureToggles {
|
||||
|
||||
override val isEthStakingEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED)
|
||||
override fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean {
|
||||
val toggle = integrationId.getFeatureToggle() ?: return true
|
||||
return featureTogglesManager.isFeatureEnabled(toggle)
|
||||
}
|
||||
|
||||
private fun StakingIntegrationID.getFeatureToggle(): FeatureToggles? = when (this) {
|
||||
is StakingIntegrationID.P2PEthPool -> FeatureToggles.STAKING_ETH_ENABLED
|
||||
is StakingIntegrationID.StakeKit -> this.getStakeKitFeatureToggle()
|
||||
}
|
||||
|
||||
private fun StakingIntegrationID.StakeKit.getStakeKitFeatureToggle(): FeatureToggles? = when (this) {
|
||||
is StakingIntegrationID.StakeKit.Coin -> when (this) {
|
||||
StakingIntegrationID.StakeKit.Coin.Ton,
|
||||
StakingIntegrationID.StakeKit.Coin.Solana,
|
||||
StakingIntegrationID.StakeKit.Coin.Cosmos,
|
||||
StakingIntegrationID.StakeKit.Coin.Tron,
|
||||
StakingIntegrationID.StakeKit.Coin.BSC,
|
||||
StakingIntegrationID.StakeKit.Coin.Cardano,
|
||||
-> null
|
||||
}
|
||||
is StakingIntegrationID.StakeKit.EthereumToken -> when (this) {
|
||||
StakingIntegrationID.StakeKit.EthereumToken.Polygon -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.data.staking.toggles
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultStakingFeatureTogglesTest {
|
||||
|
||||
private val featureTogglesManager: FeatureTogglesManager = mockk()
|
||||
private val toggles = DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(featureTogglesManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `P2PEthPool returns true when STAKING_ETH_ENABLED is enabled`() {
|
||||
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns true
|
||||
|
||||
assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isTrue()
|
||||
|
||||
verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `P2PEthPool returns false when STAKING_ETH_ENABLED is disabled`() {
|
||||
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns false
|
||||
|
||||
assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isFalse()
|
||||
|
||||
verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existing StakeKit Coin integrations are always enabled`() {
|
||||
StakingIntegrationID.StakeKit.Coin.entries.forEach { coin ->
|
||||
assertThat(toggles.isIntegrationEnabled(coin)).isTrue()
|
||||
}
|
||||
|
||||
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existing StakeKit EthereumToken integrations are always enabled`() {
|
||||
StakingIntegrationID.StakeKit.EthereumToken.entries.forEach { token ->
|
||||
assertThat(toggles.isIntegrationEnabled(token)).isTrue()
|
||||
}
|
||||
|
||||
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.util.TangemPayErrorConverter
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
|
|
@ -16,14 +16,8 @@ import com.tangem.utils.logging.TangemLogger
|
|||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory"
|
||||
/**
|
||||
* Custom token parameters. Will be used only for F&F.
|
||||
*/
|
||||
private const val TOKEN_ID = "usd-coin"
|
||||
private const val TOKEN_NAME = "USDC"
|
||||
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
private const val TOKEN_DECIMALS = 6
|
||||
|
||||
@Deprecated("Use TangemPayCurrencyFactory instead")
|
||||
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
private val errorConverter: TangemPayErrorConverter,
|
||||
|
|
@ -47,32 +41,11 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
|
|||
)
|
||||
cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
)
|
||||
}.mapLeft { exception ->
|
||||
TangemLogger.withTag(TAG).e("Error", exception)
|
||||
errorConverter.convert(exception)
|
||||
}
|
||||
}
|
||||
|
||||
override fun create(userWallet: UserWallet): Either<UniversalError, CryptoCurrency.Token> {
|
||||
return catch {
|
||||
val network = networkFactory.create(
|
||||
blockchain = VisaUtilities.visaBlockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID),
|
||||
name = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
|
||||
)
|
||||
}.mapLeft { exception ->
|
||||
TangemLogger.withTag(TAG).e("Error", exception)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM].
|
||||
|
|
@ -15,10 +16,12 @@ import com.tangem.utils.converter.TwoWayConverter
|
|||
*
|
||||
* [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source.
|
||||
*/
|
||||
internal object PaymentAccountStatusValueDMConverter :
|
||||
TwoWayConverter<PaymentAccountStatusValue, PaymentAccountStatusValueDM?> {
|
||||
@Singleton
|
||||
internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
||||
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
|
||||
) {
|
||||
|
||||
override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? {
|
||||
fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? {
|
||||
return when (value) {
|
||||
is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated()
|
||||
is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview(
|
||||
|
|
@ -60,7 +63,7 @@ internal object PaymentAccountStatusValueDMConverter :
|
|||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
|
||||
fun convertBack(userWalletId: UserWalletId, value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
|
||||
return when (value) {
|
||||
is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated
|
||||
is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed(
|
||||
|
|
@ -80,6 +83,7 @@ internal object PaymentAccountStatusValueDMConverter :
|
|||
isPinSet = value.isPinSet,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
)
|
||||
} else {
|
||||
PaymentAccountStatusValue.Loaded(
|
||||
|
|
@ -92,6 +96,7 @@ internal object PaymentAccountStatusValueDMConverter :
|
|||
isPinSet = value.isPinSet,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
)
|
||||
}
|
||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.datastore.dataStoreFile
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
|
||||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer
|
||||
import com.tangem.data.pay.repository.*
|
||||
|
|
@ -24,6 +25,7 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
|||
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
|
|
@ -112,6 +114,7 @@ internal interface TangemPayDataModule {
|
|||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
scope: AppCoroutineScope,
|
||||
converter: PaymentAccountStatusValueDMConverter,
|
||||
): PaymentAccountStatusesStore {
|
||||
return PaymentAccountStatusesStore(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
|
|
@ -124,6 +127,7 @@ internal interface TangemPayDataModule {
|
|||
produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") },
|
||||
scope = scope,
|
||||
),
|
||||
converter = converter,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
|
|
@ -139,6 +143,14 @@ internal interface TangemPayDataModule {
|
|||
) {}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTangemPayCryptoCurrencyStatusUseCase(
|
||||
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
): GetPaymentAccountCryptoCurrencyStatusUseCase {
|
||||
return GetPaymentAccountCryptoCurrencyStatusUseCase(paymentAccountStatusSupplier)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayMainScreenCustomerInfoUseCase(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.data.pay.entity
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.requireUserWalletsSync
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class TangemPayCurrencyFactory @Inject constructor(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val networkFactory: NetworkFactory,
|
||||
) {
|
||||
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyFactory(excludedBlockchains)
|
||||
}
|
||||
|
||||
fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
|
||||
val userWallet = userWalletsListRepository.requireUserWalletsSync()
|
||||
.firstOrNull { it.walletId == userWalletId }
|
||||
?: error("User wallet with id $userWalletId not found")
|
||||
val network = networkFactory.create(
|
||||
blockchain = VisaUtilities.visaBlockchain,
|
||||
userWallet = userWallet,
|
||||
extraDerivationPath = null,
|
||||
)
|
||||
return cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val TOKEN_ID = "usd-coin"
|
||||
internal const val TOKEN_NAME = "USDC"
|
||||
internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
internal const val TOKEN_DECIMALS = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.pay.flow
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.models.StatusSource
|
||||
|
|
@ -8,6 +9,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
|
|
@ -29,6 +31,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val customerOrderRepository: CustomerOrderRepository,
|
||||
private val deviceSecurity: DeviceSecurityInfoProvider,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
|
@ -132,7 +135,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
},
|
||||
ifRight = { customerInfo ->
|
||||
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
|
||||
val status = customerInfo.mapToPaymentAccountStatus()
|
||||
val status = customerInfo.mapToPaymentAccountStatus(account.userWalletId)
|
||||
if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
onboardingRepository.createOrder(account.userWalletId)
|
||||
|
|
@ -167,7 +170,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
},
|
||||
ifRight = { customerInfo ->
|
||||
if (customerInfo.kycStatus == KycStatus.REJECTED) {
|
||||
customerInfo.mapToPaymentAccountStatus()
|
||||
customerInfo.mapToPaymentAccountStatus(account.userWalletId)
|
||||
} else {
|
||||
PaymentAccountStatusValue.Error.CardIssueFailed(
|
||||
customerId = orderData.customerId,
|
||||
|
|
@ -182,7 +185,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
|
||||
.fold(
|
||||
ifLeft = { it.mapToPaymentAccountStatus() },
|
||||
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() },
|
||||
ifRight = { it.mapToPaymentAccountStatus(account.userWalletId) },
|
||||
)
|
||||
}
|
||||
OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable
|
||||
|
|
@ -191,7 +194,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue {
|
||||
private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
|
||||
val cardInfo = this.cardInfo
|
||||
val productInstance = this.productInstance
|
||||
return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) {
|
||||
|
|
@ -202,6 +205,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
)
|
||||
} else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) {
|
||||
convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
productInstance = productInstance,
|
||||
cardInfo = cardInfo,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
|
|
@ -212,10 +216,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
}
|
||||
|
||||
private fun convertToContentState(
|
||||
userWalletId: UserWalletId,
|
||||
productInstance: CustomerInfo.ProductInstance,
|
||||
cardInfo: CustomerInfo.CardInfo,
|
||||
customerId: String,
|
||||
): PaymentAccountStatusValue {
|
||||
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
|
||||
return when (productInstance.frozenState) {
|
||||
TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked(
|
||||
source = StatusSource.ACTUAL,
|
||||
|
|
@ -227,6 +233,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
isPinSet = cardInfo.isPinSet,
|
||||
fiatBalance = cardInfo.fiatBalance,
|
||||
cryptoBalance = cardInfo.cryptoBalance,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
else -> PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
|
|
@ -238,6 +245,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
isPinSet = cardInfo.isPinSet,
|
||||
fiatBalance = cardInfo.fiatBalance,
|
||||
cryptoBalance = cardInfo.cryptoBalance,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatu
|
|||
internal class PaymentAccountStatusesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithPaymentStatus>,
|
||||
private val persistenceDataStore: DataStore<WalletIdWithPaymentStatusDM>,
|
||||
private val converter: PaymentAccountStatusValueDMConverter,
|
||||
scope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ internal class PaymentAccountStatusesStore(
|
|||
runtimeStore.store(
|
||||
value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) ->
|
||||
val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId))
|
||||
val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM)
|
||||
val statusValue = converter.convertBack(userWalletId = account.userWalletId, value = statusDM)
|
||||
AccountStatus.Payment(account = account, value = statusValue)
|
||||
},
|
||||
)
|
||||
|
|
@ -87,7 +88,7 @@ internal class PaymentAccountStatusesStore(
|
|||
}
|
||||
|
||||
private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) {
|
||||
val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return
|
||||
val statusDM = converter.convert(value = status) ?: return
|
||||
persistenceDataStore.updateData { storedStatuses ->
|
||||
storedStatuses.toMutableMap().apply {
|
||||
put(key = userWalletId.stringValue, value = statusDM)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.wallets.derivations
|
|||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncStrict
|
||||
|
|
@ -76,6 +77,23 @@ internal class DefaultDerivationsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getExistingDerivedKeys(
|
||||
userWalletId: UserWalletId,
|
||||
seedKey: ByteArrayKey,
|
||||
): ExtendedPublicKeysMap {
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
return userWallet.getExistingDerivedKeys()[seedKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
}
|
||||
|
||||
private fun UserWallet.getExistingDerivedKeys(): Map<ByteArrayKey, ExtendedPublicKeysMap> {
|
||||
return when (this) {
|
||||
is UserWallet.Cold -> scanResponse.derivedKeys
|
||||
is UserWallet.Hot -> wallets
|
||||
?.associate { it.publicKey.toMapKey() to ExtendedPublicKeysMap(it.derivedKeys) }
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@ dependencies {
|
|||
api(projects.domain.dynamicAddresses.models)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(tangemDeps.card.core)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
||||
/**
|
||||
* List of blockchains that support Dynamic Addresses (XPUB-based multi-address mode).
|
||||
* Must match [Blockchain.isBip44DerivationStyleXPUB] from blockchain-sdk minus Kaspa (deferred).
|
||||
*
|
||||
* Per ASMPT-005: DA is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses.
|
||||
* Only the default derivation style per blockchain is supported.
|
||||
*/
|
||||
object DynamicAddressesSupportedBlockchains {
|
||||
|
||||
private const val BIP44_PURPOSE = 44L
|
||||
private const val BIP84_PURPOSE = 84L
|
||||
|
||||
private val supported = setOf(
|
||||
Blockchain.Bitcoin,
|
||||
Blockchain.BitcoinTestnet,
|
||||
Blockchain.BitcoinCash,
|
||||
Blockchain.BitcoinCashTestnet,
|
||||
Blockchain.Litecoin,
|
||||
Blockchain.Dogecoin,
|
||||
Blockchain.Dash,
|
||||
Blockchain.Ravencoin,
|
||||
Blockchain.RavencoinTestnet,
|
||||
)
|
||||
|
||||
private val supportedNetworkIds = supported.map { it.id }.toSet()
|
||||
|
||||
/**
|
||||
* Allowed BIP purpose nodes per network ID.
|
||||
* BTC/LTC use BIP-84 (SegWit), others use BIP-44 (Legacy P2PKH).
|
||||
*/
|
||||
private val allowedPurposeByNetworkId: Map<String, Long> = buildMap {
|
||||
put(Blockchain.Bitcoin.id, BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinTestnet.id, BIP84_PURPOSE)
|
||||
put(Blockchain.Litecoin.id, BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinCash.id, BIP44_PURPOSE)
|
||||
put(Blockchain.BitcoinCashTestnet.id, BIP44_PURPOSE)
|
||||
put(Blockchain.Dogecoin.id, BIP44_PURPOSE)
|
||||
put(Blockchain.Dash.id, BIP44_PURPOSE)
|
||||
put(Blockchain.Ravencoin.id, BIP44_PURPOSE)
|
||||
put(Blockchain.RavencoinTestnet.id, BIP44_PURPOSE)
|
||||
}
|
||||
|
||||
fun isSupported(blockchain: Blockchain): Boolean = blockchain in supported
|
||||
|
||||
fun isSupportedByNetworkId(networkId: String): Boolean = networkId in supportedNetworkIds
|
||||
|
||||
/** Returns the allowed BIP purpose node for the given network, or null if not supported */
|
||||
fun getAllowedPurpose(networkId: String): Long? = allowedPurposeByNetworkId[networkId]
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
sealed class EnableDynamicAddressesError {
|
||||
|
||||
data object ConflictingCustomTokens : EnableDynamicAddressesError()
|
||||
|
||||
data class ServiceError(val cause: Throwable) : EnableDynamicAddressesError()
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -9,8 +11,19 @@ class EnableDynamicAddressesUseCase(
|
|||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network, xpub: String): Either<Throwable, Unit> =
|
||||
Either.catch {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
xpub: String,
|
||||
): Either<EnableDynamicAddressesError, Unit> {
|
||||
return try {
|
||||
if (dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network)) {
|
||||
return EnableDynamicAddressesError.ConflictingCustomTokens.left()
|
||||
}
|
||||
dynamicAddressesRepository.enable(userWalletId, network, xpub)
|
||||
Unit.right()
|
||||
} catch (e: Throwable) {
|
||||
EnableDynamicAddressesError.ServiceError(e).left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
|
||||
/**
|
||||
* Checks if the account-level XPUB key is already derived (no card scan needed).
|
||||
*/
|
||||
class IsXpubDerivedUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val blockchain = network.toBlockchain()
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false
|
||||
if (!blockchain.isBip44DerivationStyleXPUB()) return false
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false
|
||||
val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return false
|
||||
if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return false
|
||||
val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT))
|
||||
|
||||
val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey)
|
||||
val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey)
|
||||
return existingKeys[accountPath] != null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACCOUNT_PATH_DROP_COUNT = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
||||
/**
|
||||
* Checks if XPUB generation is supported for the given wallet and network (hardware capability check).
|
||||
*/
|
||||
class IsXpubSupportedUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val blockchain = network.toBlockchain()
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false
|
||||
return walletManager.wallet.publicKey.derivationType?.hdKey != null
|
||||
}
|
||||
}
|
||||
|
|
@ -19,4 +19,7 @@ interface DynamicAddressesRepository {
|
|||
suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String?
|
||||
|
||||
suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean
|
||||
|
||||
/** Returns true if there are custom tokens with change/index ≠ 0 that conflict with DA */
|
||||
suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed interface LegacyAction : Action {
|
||||
|
||||
data object PrepareDetailsScreen : LegacyAction
|
||||
}
|
||||
|
|
@ -2,9 +2,13 @@ package com.tangem.domain.models.account
|
|||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Represents the various states a payment account can have, encapsulating different information based on the state.
|
||||
|
|
@ -104,7 +108,29 @@ sealed class PaymentAccountStatusValue {
|
|||
val isPinSet: Boolean,
|
||||
val fiatBalance: FiatBalance,
|
||||
val cryptoBalance: CryptoBalance,
|
||||
) : PaymentAccountStatusValue()
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
amount = cryptoBalance.balance,
|
||||
fiatAmount = fiatBalance.availableBalance,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = cryptoBalance.depositAddress,
|
||||
),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state where the payment account is successfully loaded with complete information.
|
||||
|
|
@ -130,7 +156,29 @@ sealed class PaymentAccountStatusValue {
|
|||
val isPinSet: Boolean,
|
||||
val fiatBalance: FiatBalance,
|
||||
val cryptoBalance: CryptoBalance,
|
||||
) : PaymentAccountStatusValue()
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
amount = cryptoBalance.balance,
|
||||
fiatAmount = fiatBalance.availableBalance,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = cryptoBalance.depositAddress,
|
||||
),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** Represents an error state for the payment account status. */
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ package com.tangem.domain.search.model
|
|||
data class SearchResult(
|
||||
val textHints: List<SearchTextHint>,
|
||||
val recentTokens: List<RecentSearchToken>,
|
||||
val userAssets: List<UserAssetSearchEntry>,
|
||||
val userAssets: List<UserAssetSearchItem>,
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.search.model
|
|||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
|
|
@ -10,5 +11,6 @@ data class UserAssetSearchEntry(
|
|||
val userWalletName: String,
|
||||
val accountId: AccountId,
|
||||
val accountName: AccountName,
|
||||
val accountIcon: CryptoPortfolioIcon,
|
||||
val currencyStatus: CryptoCurrencyStatus,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.domain.search.model
|
||||
|
||||
sealed interface UserAssetSearchItem {
|
||||
|
||||
data class Single(val entry: UserAssetSearchEntry) : UserAssetSearchItem
|
||||
|
||||
data class Grouped(
|
||||
val tokenName: String,
|
||||
val tokenSymbol: String,
|
||||
val tokenIconUrl: String?,
|
||||
val entries: List<UserAssetSearchEntry>,
|
||||
) : UserAssetSearchItem
|
||||
}
|
||||
|
|
@ -9,10 +9,12 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.search.model.SearchResult
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.search.model.UserAssetSearchItem
|
||||
import com.tangem.domain.search.repository.SearchRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Primary search use case that produces [SearchResult] based on the current query.
|
||||
|
|
@ -70,9 +72,12 @@ class GetSearchResultsUseCase(
|
|||
|
||||
if (unlockedWallets.isEmpty()) return@combine emptyList()
|
||||
|
||||
statusLists
|
||||
val entries = statusLists
|
||||
.filter { it.userWalletId in unlockedWallets }
|
||||
.flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) }
|
||||
|
||||
val shouldGroup = needsGrouping(unlockedWallets.values, statusLists)
|
||||
groupAndSort(entries, shouldGroup)
|
||||
}.map { userAssets ->
|
||||
SearchResult(
|
||||
textHints = emptyList(),
|
||||
|
|
@ -82,6 +87,44 @@ class GetSearchResultsUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
private fun needsGrouping(unlockedWallets: Collection<UserWallet>, statusLists: List<AccountStatusList>): Boolean {
|
||||
if (unlockedWallets.size > 1) return true
|
||||
|
||||
val totalAccounts = statusLists
|
||||
.filter { sl -> unlockedWallets.any { it.walletId == sl.userWalletId } }
|
||||
.sumOf { it.accountStatuses.filterCryptoPortfolio().size }
|
||||
|
||||
return totalAccounts > 1
|
||||
}
|
||||
|
||||
private fun groupAndSort(entries: List<UserAssetSearchEntry>, shouldGroup: Boolean): List<UserAssetSearchItem> {
|
||||
if (!shouldGroup) {
|
||||
return entries
|
||||
.map { UserAssetSearchItem.Single(it) }
|
||||
.sortedByDescending { it.entry.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
}
|
||||
|
||||
val grouped = entries.groupBy { entry ->
|
||||
val rawId = entry.currencyStatus.currency.id.rawCurrencyId
|
||||
rawId?.value ?: "${entry.currencyStatus.currency.name}|${entry.currencyStatus.currency.symbol}"
|
||||
}
|
||||
|
||||
return grouped.map { (_, groupEntries) ->
|
||||
val assetInfo = groupEntries.first()
|
||||
UserAssetSearchItem.Grouped(
|
||||
tokenName = assetInfo.currencyStatus.currency.name,
|
||||
tokenSymbol = assetInfo.currencyStatus.currency.symbol,
|
||||
tokenIconUrl = assetInfo.currencyStatus.currency.iconUrl,
|
||||
entries = groupEntries,
|
||||
)
|
||||
}.sortedByDescending { item ->
|
||||
when (item) {
|
||||
is UserAssetSearchItem.Grouped ->
|
||||
item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractMatchingAssets(
|
||||
statusList: AccountStatusList,
|
||||
wallets: Map<UserWalletId, UserWallet>,
|
||||
|
|
@ -103,6 +146,7 @@ class GetSearchResultsUseCase(
|
|||
userWalletName = wallet.name,
|
||||
accountId = accountStatus.accountId,
|
||||
accountName = accountStatus.account.accountName,
|
||||
accountIcon = accountStatus.account.icon,
|
||||
currencyStatus = currencyStatus,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either }</ID>
|
||||
<ID>MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending && action.amount < it.amount && it.type == BalanceType.STAKED && it.validatorAddress == action.validatorAddress }</ID>
|
||||
<ID>NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId)</ID>
|
||||
<ID>UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier</ID>
|
||||
<ID>UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier</ID>
|
||||
<ID>UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf()</ID>
|
||||
<ID>UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: ""</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -23,9 +23,9 @@ class FetchStakingYieldBalanceUseCase(
|
|||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
.getOrElse {
|
||||
when (it) {
|
||||
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it"))
|
||||
.getOrElse { error ->
|
||||
when (error) {
|
||||
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$error"))
|
||||
StakingIdFactory.Error.UnsupportedCurrency -> Unit.right()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,12 @@ class GetConstructedStakingTransactionUseCase(
|
|||
amount: Amount,
|
||||
transactionId: String,
|
||||
): Either<StakingError, Pair<StakingTransaction, TransactionData.Compiled>> = Either.catch {
|
||||
stakeKitRepository.constructTransaction(networkId, fee, amount, transactionId)
|
||||
stakeKitRepository.constructTransaction(
|
||||
networkId = networkId,
|
||||
fee = fee,
|
||||
amount = amount,
|
||||
transactionId = transactionId,
|
||||
)
|
||||
}.mapLeft {
|
||||
stakingErrorResolver.resolve(it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class InvalidatePendingTransactionsUseCase(
|
|||
type = BalanceType.STAKED,
|
||||
amount = action.amount,
|
||||
rawCurrencyId = null,
|
||||
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "",
|
||||
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0).orEmpty(),
|
||||
date = null,
|
||||
pendingActions = emptyList(),
|
||||
pendingActionsConstraints = emptyList(),
|
||||
|
|
@ -149,10 +149,10 @@ class InvalidatePendingTransactionsUseCase(
|
|||
}
|
||||
|
||||
private fun findPartialUnstake(balances: MutableList<BalanceItem>, action: StakingAction): Pair<Int, BigDecimal> {
|
||||
val index = balances.indexOfFirst {
|
||||
!it.isPending && action.amount < it.amount &&
|
||||
it.type == BalanceType.STAKED &&
|
||||
it.validatorAddress == action.validatorAddress
|
||||
val index = balances.indexOfFirst { balance ->
|
||||
!balance.isPending && action.amount < balance.amount &&
|
||||
balance.type == BalanceType.STAKED &&
|
||||
balance.validatorAddress == action.validatorAddress
|
||||
}
|
||||
return index to action.amount
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,27 @@ package com.tangem.domain.staking
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
||||
/**
|
||||
* Factory class for creating instances of [StakingID]
|
||||
*
|
||||
* @property walletManagersFacade wallet manager facade
|
||||
* @property walletManagersFacade wallet manager facade
|
||||
* @property stakingFeatureToggles staking feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class StakingIdFactory(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -72,6 +76,8 @@ class StakingIdFactory(
|
|||
|
||||
ensureNotNull(integrationId) { Error.UnsupportedCurrency }
|
||||
|
||||
ensure(stakingFeatureToggles.isIntegrationEnabled(integrationId)) { Error.UnsupportedCurrency }
|
||||
|
||||
val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() }
|
||||
|
||||
ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.models.staking.action.StakingActionType
|
|||
|
||||
sealed class StakingAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(
|
||||
category = "Staking",
|
||||
event = event,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class MultiStakingBalanceSupplier(
|
||||
open class MultiStakingBalanceSupplier(
|
||||
override val factory: FlowProducer.Factory<MultiStakingBalanceProducer.Params, MultiStakingBalanceProducer>,
|
||||
override val keyCreator: (MultiStakingBalanceProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<MultiStakingBalanceProducer, MultiStakingBalanceProducer.Params, Set<StakingBalance>>()
|
||||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class SingleStakingBalanceSupplier(
|
||||
open class SingleStakingBalanceSupplier(
|
||||
override val factory: FlowProducer.Factory<SingleStakingBalanceProducer.Params, SingleStakingBalanceProducer>,
|
||||
override val keyCreator: (SingleStakingBalanceProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<SingleStakingBalanceProducer, SingleStakingBalanceProducer.Params, StakingBalance>()
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.domain.staking.toggles
|
||||
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
|
||||
interface StakingFeatureToggles {
|
||||
val isEthStakingEnabled: Boolean
|
||||
|
||||
fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean
|
||||
}
|
||||
|
|
@ -10,11 +10,13 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
|
@ -30,11 +32,16 @@ import org.junit.jupiter.params.ParameterizedTest
|
|||
internal class StakingIdFactoryTest {
|
||||
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade)
|
||||
private val stakingFeatureToggles: StakingFeatureToggles = mockk()
|
||||
private val factory = StakingIdFactory(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(walletManagersFacade)
|
||||
clearMocks(walletManagersFacade, stakingFeatureToggles)
|
||||
every { stakingFeatureToggles.isIntegrationEnabled(any()) } returns true
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
|
@ -66,6 +73,33 @@ internal class StakingIdFactoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create returns UnsupportedCurrency if integration is disabled by toggle`() = runTest {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
|
||||
|
||||
every {
|
||||
stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.StakeKit.Coin.Ton)
|
||||
} returns false
|
||||
|
||||
// Act
|
||||
val actual = factory.create(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = currency.id,
|
||||
network = currency.network,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = StakingIdFactory.Error.UnsupportedCurrency
|
||||
|
||||
Truth.assertThat(actual.leftOrNull()).isEqualTo(expected)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
walletManagersFacade.getDefaultAddress(userWalletId = any(), network = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create returns UnableToGetAddress if address is null`() = runTest {
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.tangem.core.error.UniversalError
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
@Deprecated("TangemPayCurrencyFactory")
|
||||
interface TangemPayCryptoCurrencyFactory {
|
||||
|
||||
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
|
||||
fun create(userWallet: UserWallet): Either<UniversalError, CryptoCurrency.Token>
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.none
|
||||
import arrow.core.some
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
class GetPaymentAccountCryptoCurrencyStatusUseCase(
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Option<Pair<Account.Payment, CryptoCurrencyStatus>> {
|
||||
val accountStatus = paymentAccountStatusSupplier.invoke(userWalletId).firstOrNull() ?: return none()
|
||||
val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) {
|
||||
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
|
||||
is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus
|
||||
else -> return none()
|
||||
}
|
||||
return if (cryptoCurrencyStatus.currency == cryptoCurrency) {
|
||||
(accountStatus.account to cryptoCurrencyStatus).some()
|
||||
} else {
|
||||
none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,9 @@ interface DerivationsRepository {
|
|||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap>
|
||||
|
||||
/** Returns already derived extended public keys for the given [seedKey] */
|
||||
suspend fun getExistingDerivedKeys(userWalletId: UserWalletId, seedKey: ByteArrayKey): ExtendedPublicKeysMap
|
||||
|
||||
/** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */
|
||||
suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.wallets.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
|
|
@ -38,23 +37,37 @@ class GetExtendedPublicKeyForCurrencyUseCase(
|
|||
error("No derivation found")
|
||||
}
|
||||
|
||||
val seedKey = walletManager.wallet.publicKey.seedKey
|
||||
val existingKeys = derivationsRepository.getExistingDerivedKeys(
|
||||
userWalletId = userWalletId,
|
||||
seedKey = ByteArrayKey(seedKey),
|
||||
)
|
||||
|
||||
var childKey = makeChildKey(
|
||||
isBip44DerivationStyleXPUB = blockchain.isBip44DerivationStyleXPUB(),
|
||||
extendedPublicKey = hdKey.extendedPublicKey,
|
||||
derivationPath = hdKey.path,
|
||||
)
|
||||
|
||||
// Fill from already derived keys if available
|
||||
if (childKey.extendedPublicKey == null) {
|
||||
existingKeys[childKey.derivationPath]?.let {
|
||||
childKey = childKey.copy(extendedPublicKey = it)
|
||||
}
|
||||
}
|
||||
|
||||
val parentPath = childKey.derivationPath.dropLastNodes(1)
|
||||
var parentKey = Key(
|
||||
derivationPath = childKey.derivationPath.dropLastNodes(1),
|
||||
extendedPublicKey = null,
|
||||
derivationPath = parentPath,
|
||||
extendedPublicKey = existingKeys[parentPath],
|
||||
)
|
||||
|
||||
val pendingDerivations = getPendingDerivations(childKey, parentKey)
|
||||
val derivedKeys = deriveKeys(
|
||||
userWalletId = userWalletId,
|
||||
seedKey = walletManager.wallet.publicKey.seedKey,
|
||||
paths = pendingDerivations,
|
||||
)
|
||||
val derivedKeys = if (pendingDerivations.isNotEmpty()) {
|
||||
deriveKeys(userWalletId = userWalletId, seedKey = seedKey, paths = pendingDerivations)
|
||||
} else {
|
||||
ExtendedPublicKeysMap(emptyMap())
|
||||
}
|
||||
|
||||
if (childKey.extendedPublicKey == null) {
|
||||
childKey = childKey.copy(
|
||||
|
|
@ -72,22 +85,6 @@ class GetExtendedPublicKeyForCurrencyUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if xpub generation is supported, false otherwise
|
||||
*/
|
||||
suspend fun isSupported(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> = Either.catch {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
|
||||
?: error("Wallet not found for user wallet $userWalletId and network ${network.id}")
|
||||
|
||||
val blockchain = network.toBlockchain()
|
||||
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
|
||||
val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey
|
||||
|
||||
val isSupported = isSecp256k1Blockchain && isHdKey != null
|
||||
|
||||
return isSupported.right()
|
||||
}
|
||||
|
||||
private suspend fun deriveKeys(
|
||||
userWalletId: UserWalletId,
|
||||
seedKey: ByteArray,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package com.tangem.domain.wallets.usecase
|
|||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.LinkedHashMap
|
||||
|
||||
/**
|
||||
* Use case for getting list of user wallets
|
||||
|
|
@ -22,9 +22,13 @@ class GetWalletsUseCase(
|
|||
operator fun invoke(): Flow<List<UserWallet>> = userWalletsListRepository.userWallets.map { requireNotNull(it) }
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun invokeAsMap(): Flow<LinkedHashMap<UserWalletId, UserWallet>> = userWalletsListRepository.userWallets
|
||||
.map { requireNotNull(it) }
|
||||
.map { wallets ->
|
||||
fun invokeAsMap(isOnlyMultiCurrency: Boolean = true): Flow<LinkedHashMap<UserWalletId, UserWallet>> = invoke()
|
||||
.map { list ->
|
||||
val wallets = if (isOnlyMultiCurrency) {
|
||||
list.filter { wallet -> wallet.isMultiCurrency }
|
||||
} else {
|
||||
list
|
||||
}
|
||||
wallets.associateByTo(
|
||||
destination = linkedMapOf(),
|
||||
keySelector = { wallet -> wallet.walletId },
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
||||
|
|
@ -61,7 +59,6 @@ internal class DetailsModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val appStateHolder: ReduxStateHolder,
|
||||
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||
private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
|
|
@ -79,9 +76,6 @@ internal class DetailsModel @Inject constructor(
|
|||
val state: MutableStateFlow<DetailsUM>
|
||||
|
||||
init {
|
||||
// Use to save compatibility with screens that using Redux states
|
||||
bootstrapScreenState()
|
||||
|
||||
val isWalletConnectAvailable = runBlocking {
|
||||
// danger region, this works immediately, but will be refactored later with WC
|
||||
checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { throwable ->
|
||||
|
|
@ -122,10 +116,6 @@ internal class DetailsModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun bootstrapScreenState() {
|
||||
appStateHolder.dispatch(LegacyAction.PrepareDetailsScreen)
|
||||
}
|
||||
|
||||
private fun sendFeedback() {
|
||||
modelScope.launch {
|
||||
val userWallets = getWalletsUseCase.invokeSync()
|
||||
|
|
|
|||
|
|
@ -148,6 +148,13 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
appComponentContext = appComponentContext,
|
||||
params = DefaultSearchComponent.Params(
|
||||
onBackClick = onBackClicked,
|
||||
onMarketTokenClick = { token, currency ->
|
||||
feedEntryClickIntents.onMarketItemClick(
|
||||
token = token,
|
||||
appCurrency = currency,
|
||||
source = AnalyticsParam.ScreensSources.Market.value,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import com.tangem.core.ui.ds.field.search.TangemFieldShape
|
|||
import com.tangem.core.ui.ds.field.search.TangemSearchField
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.model.search.SearchModel
|
||||
import com.tangem.features.feed.ui.search.SearchContent
|
||||
import com.tangem.features.feed.ui.search.state.SearchCallbacks
|
||||
|
|
@ -74,6 +76,7 @@ internal class DefaultSearchComponent(
|
|||
onClearHintsClick = model::clearSearchHistory,
|
||||
onTextHintClick = model::onTextHintClick,
|
||||
onResultMarketTokenClick = model::onResultMarketTokenClick,
|
||||
onHistoryTokenClick = model::onHistoryTokenClick,
|
||||
)
|
||||
}
|
||||
SearchContent(
|
||||
|
|
@ -86,5 +89,6 @@ internal class DefaultSearchComponent(
|
|||
|
||||
data class Params(
|
||||
val onBackClick: () -> Unit,
|
||||
val onMarketTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
package com.tangem.features.feed.model.search
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
|
||||
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
|
||||
import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase
|
||||
|
|
@ -26,6 +28,7 @@ import com.tangem.features.feed.model.search.converter.MarketsListItemUMToRecent
|
|||
import com.tangem.features.feed.model.search.converter.MarketsListItemUMWithAppCurrency
|
||||
import com.tangem.features.feed.model.search.converter.RecentSearchTokenToMarketsListItemUMConverter
|
||||
import com.tangem.features.feed.model.search.converter.RecentSearchTokenWithAppCurrency
|
||||
import com.tangem.features.feed.model.search.converter.UserAssetSearchItemConverter
|
||||
import com.tangem.features.feed.model.search.state.SearchStateController
|
||||
import com.tangem.features.feed.model.search.state.transformers.*
|
||||
import com.tangem.features.feed.ui.search.state.*
|
||||
|
|
@ -35,13 +38,8 @@ import com.tangem.utils.coroutines.JobHolder
|
|||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
|
||||
|
|
@ -54,6 +52,7 @@ internal class SearchModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getSearchResultsUseCase: GetSearchResultsUseCase,
|
||||
private val saveSearchQueryUseCase: SaveSearchQueryUseCase,
|
||||
private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase,
|
||||
|
|
@ -77,6 +76,13 @@ internal class SearchModel @Inject constructor(
|
|||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val isBalanceHidden = getBalanceHidingSettingsUseCase.isBalanceHidden()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
private val marketsListItemToRecentSearchTokenConverter by lazy {
|
||||
MarketsListItemUMToRecentSearchTokenConverter()
|
||||
}
|
||||
|
|
@ -143,17 +149,38 @@ internal class SearchModel @Inject constructor(
|
|||
)
|
||||
saveRecentSearchTokenUseCase(marketsListItemToRecentSearchTokenConverter.convert(input))
|
||||
saveSearchQueryUseCase(stateController.value.searchBar.query)
|
||||
withContext(dispatchers.mainImmediate) {
|
||||
searchMarketsListManager.getTokenById(item.id)?.let { found ->
|
||||
params.onMarketTokenClick(found.toSerializableParam(), appCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onHistoryTokenClick(item: MarketsListItemUM) {
|
||||
val tokenMarketParams = TokenMarketParams(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
symbol = item.currencySymbol,
|
||||
tokenQuotes = TokenMarketParams.Quotes(
|
||||
currentPrice = item.price.fiatPrice,
|
||||
h24Percent = null,
|
||||
weekPercent = null,
|
||||
monthPercent = null,
|
||||
),
|
||||
imageUrl = item.iconUrl,
|
||||
)
|
||||
params.onMarketTokenClick(tokenMarketParams, currentAppCurrency.value)
|
||||
}
|
||||
|
||||
private fun initCallbacks() {
|
||||
stateController.update(object : SearchUMTransformer {
|
||||
override fun transform(prevState: SearchUM): SearchUM {
|
||||
return prevState.copy(
|
||||
searchBar = prevState.searchBar.copy(
|
||||
onQueryChange = ::onQueryChange,
|
||||
onActiveChange = ::onActiveChange,
|
||||
onClearClick = ::onClearClick,
|
||||
onCancelClick = params.onBackClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -164,10 +191,6 @@ internal class SearchModel @Inject constructor(
|
|||
stateController.update(UpdateSearchBarQueryTransformer(query))
|
||||
}
|
||||
|
||||
private fun onActiveChange(isActive: Boolean) {
|
||||
if (!isActive) params.onBackClick()
|
||||
}
|
||||
|
||||
private fun onClearClick() {
|
||||
stateController.update(UpdateSearchBarQueryTransformer(""))
|
||||
}
|
||||
|
|
@ -200,20 +223,19 @@ internal class SearchModel @Inject constructor(
|
|||
|
||||
private fun subscribeToSearchResults(query: String) {
|
||||
modelScope.launch {
|
||||
getSearchResultsUseCase(query = query).collectLatest { searchResult ->
|
||||
val userAssets = searchResult.userAssets.map { entry ->
|
||||
UserAssetItemUM(
|
||||
id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" +
|
||||
"_${entry.currencyStatus.currency.id.value}",
|
||||
tokenIconUrl = entry.currencyStatus.currency.iconUrl,
|
||||
tokenName = entry.currencyStatus.currency.name,
|
||||
tokenSymbol = entry.currencyStatus.currency.symbol,
|
||||
accountName = entry.accountName.toDisplayString(),
|
||||
onClick = {
|
||||
// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task.
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
combine(
|
||||
getSearchResultsUseCase(query = query),
|
||||
currentAppCurrency,
|
||||
isBalanceHidden,
|
||||
) { searchResult, appCurrency, balanceHidden ->
|
||||
val converter = UserAssetSearchItemConverter(
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = balanceHidden,
|
||||
)
|
||||
searchResult.userAssets
|
||||
.map(converter::convert)
|
||||
.toImmutableList()
|
||||
}.collectLatest { userAssets ->
|
||||
stateController.update(UpdateUserAssetsTransformer(userAssets))
|
||||
}
|
||||
}.saveIn(searchResultsJob)
|
||||
|
|
@ -334,13 +356,6 @@ internal class SearchModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun AccountName.toDisplayString(): String {
|
||||
return when (this) {
|
||||
is AccountName.DefaultMain -> "Main" // TODO [REDACTED_TASK_KEY] localize
|
||||
is AccountName.Custom -> value
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
|
||||
launch {
|
||||
while (true) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.feed.model.search.converter
|
||||
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.search.model.UserAssetSearchItem
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class UserAssetSearchItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
) : Converter<UserAssetSearchItem, UserAssetItemUM> {
|
||||
|
||||
override fun convert(value: UserAssetSearchItem): UserAssetItemUM {
|
||||
return when (value) {
|
||||
is UserAssetSearchItem.Single -> convertSingle(value.entry)
|
||||
is UserAssetSearchItem.Grouped -> convertGrouped(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertSingle(entry: UserAssetSearchEntry): UserAssetItemUM.Single {
|
||||
val currency = entry.currencyStatus.currency
|
||||
val value = entry.currencyStatus.value
|
||||
|
||||
return UserAssetItemUM.Single(
|
||||
id = "${entry.userWalletId.stringValue}_${entry.accountId.value}_${currency.id.value}",
|
||||
icon = TangemIconUM.Currency(
|
||||
currencyIconState = CryptoCurrencyToIconStateConverter().convert(entry.currencyStatus),
|
||||
),
|
||||
tokenName = currency.name,
|
||||
tokenSymbol = currency.symbol,
|
||||
fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) },
|
||||
cryptoBalance = formatCryptoAmount(value.amount, currency.symbol, currency.decimals),
|
||||
fiatBalance = formatFiatAmount(value.fiatAmount),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped {
|
||||
val totalFiat = item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
val totalCrypto = item.entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO }
|
||||
|
||||
val firstCurrency = item.entries.first().currencyStatus.currency
|
||||
val children = item.entries.map { entry ->
|
||||
UserAssetItemUM.GroupedChild(
|
||||
walletName = entry.userWalletName,
|
||||
accountName = entry.accountName.toUM(),
|
||||
accountIcon = entry.accountIcon.value,
|
||||
accountColor = entry.accountIcon.color,
|
||||
cryptoBalance = formatCryptoAmount(
|
||||
entry.currencyStatus.value.amount,
|
||||
entry.currencyStatus.currency.symbol,
|
||||
entry.currencyStatus.currency.decimals,
|
||||
),
|
||||
fiatBalance = formatFiatAmount(entry.currencyStatus.value.fiatAmount),
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
return UserAssetItemUM.Grouped(
|
||||
id = "grouped_${item.tokenName}_${item.tokenSymbol}",
|
||||
icon = TangemIconUM.Currency(
|
||||
currencyIconState = CryptoCurrencyToIconStateConverter().convert(item.entries.first().currencyStatus),
|
||||
),
|
||||
tokenName = item.tokenName,
|
||||
tokenSymbol = item.tokenSymbol,
|
||||
tokensCount = item.entries.size,
|
||||
totalCryptoBalance = formatCryptoAmount(totalCrypto, firstCurrency.symbol, firstCurrency.decimals),
|
||||
totalFiatBalance = formatFiatAmount(totalFiat),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
children = children,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String {
|
||||
return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN
|
||||
}
|
||||
|
||||
private fun formatFiatAmount(fiatAmount: BigDecimal?): String {
|
||||
return fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } ?: StringsSigns.DASH_SIGN
|
||||
}
|
||||
}
|
||||
|
|
@ -3,20 +3,16 @@ package com.tangem.features.feed.ui.search
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -32,7 +28,6 @@ import com.tangem.core.ui.components.SpacerW
|
|||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -54,6 +49,15 @@ internal fun SearchContent(
|
|||
val lazyListState = rememberLazyListState()
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
val contentStructureKey = when (content) {
|
||||
is SearchContentUM.InitialEmpty -> "empty"
|
||||
is SearchContentUM.History -> "history"
|
||||
is SearchContentUM.Results -> "results_${content.userAssets.isNotEmpty()}"
|
||||
}
|
||||
LaunchedEffect(contentStructureKey) {
|
||||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = modifier
|
||||
|
|
@ -72,6 +76,7 @@ internal fun SearchContent(
|
|||
history = content,
|
||||
onClearAllClick = searchCallbacks.onClearHintsClick,
|
||||
onHintClick = searchCallbacks.onTextHintClick,
|
||||
onHistoryTokenClick = searchCallbacks.onHistoryTokenClick,
|
||||
)
|
||||
is SearchContentUM.Results -> searchResultsItems(
|
||||
results = content,
|
||||
|
|
@ -98,6 +103,7 @@ private fun LazyListScope.searchHistoryItems(
|
|||
history: SearchContentUM.History,
|
||||
onClearAllClick: (() -> Unit),
|
||||
onHintClick: (String) -> Unit,
|
||||
onHistoryTokenClick: (MarketsListItemUM) -> Unit,
|
||||
) {
|
||||
if (!history.textHints.isEmpty() || !history.recentTokens.isEmpty()) {
|
||||
item(key = "recents") {
|
||||
|
|
@ -107,15 +113,20 @@ private fun LazyListScope.searchHistoryItems(
|
|||
)
|
||||
}
|
||||
}
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = history.textHints,
|
||||
key = { "hint_${it.text}" },
|
||||
) { hint ->
|
||||
key = { _, item -> "hint_${item.text}" },
|
||||
) { index, hint ->
|
||||
TextHintItem(hint = hint, onHintClick = { onHintClick(hint.text) })
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2),
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
)
|
||||
if (index < history.textHints.size - 1) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2),
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
SpacerH(TangemTheme.dimens2.x2)
|
||||
}
|
||||
items(
|
||||
items = history.recentTokens,
|
||||
|
|
@ -129,7 +140,7 @@ private fun LazyListScope.searchHistoryItems(
|
|||
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
|
||||
),
|
||||
model = token,
|
||||
onClick = {}, // TODO in [REDACTED_TASK_KEY]
|
||||
onClick = { onHistoryTokenClick(token) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -264,9 +275,16 @@ private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task.
|
||||
@Composable
|
||||
private fun UserAssetItem(asset: UserAssetItemUM) {
|
||||
when (asset) {
|
||||
is UserAssetItemUM.Single -> SingleUserAssetItem(asset)
|
||||
is UserAssetItemUM.Grouped -> GroupedUserAssetItem(asset)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SingleUserAssetItem(asset: UserAssetItemUM.Single) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -276,10 +294,8 @@ private fun UserAssetItem(asset: UserAssetItemUM) {
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TangemIcon(
|
||||
tangemIconUM = TangemIconUM.Url(asset.tokenIconUrl, fallbackRes = R.drawable.ic_custom_token_44),
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape),
|
||||
modifier = Modifier.size(40.dp),
|
||||
tangemIconUM = asset.icon,
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
|
|
@ -289,12 +305,79 @@ private fun UserAssetItem(asset: UserAssetItemUM) {
|
|||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = "${asset.tokenSymbol} · ${asset.accountName}",
|
||||
text = asset.tokenSymbol,
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (!asset.isBalanceHidden) {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = asset.fiatBalance,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = asset.cryptoBalance,
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [REDACTED_JIRA] update ui item to Portfolio block item
|
||||
@Composable
|
||||
private fun GroupedUserAssetItem(asset: UserAssetItemUM.Grouped) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = asset.onClick)
|
||||
.padding(horizontal = 12.dp, vertical = 14.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TangemIcon(
|
||||
modifier = Modifier.size(40.dp),
|
||||
tangemIconUM = asset.icon,
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = asset.tokenName,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = "${asset.tokenSymbol} · ${asset.tokensCount}",
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (!asset.isBalanceHidden) {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = asset.totalFiatBalance,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = asset.totalCryptoBalance,
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ package com.tangem.features.feed.ui.search.preview
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -15,7 +11,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
|
@ -206,18 +204,22 @@ internal object SearchContentPreviewFixtures {
|
|||
updateTimestamp = updateTimestamp,
|
||||
)
|
||||
|
||||
private fun userAsset(
|
||||
id: String,
|
||||
name: String,
|
||||
symbol: String,
|
||||
accountName: String,
|
||||
iconUrl: String? = null,
|
||||
): UserAssetItemUM = UserAssetItemUM(
|
||||
private fun userAsset(id: String, name: String, symbol: String): UserAssetItemUM = UserAssetItemUM.Single(
|
||||
id = id,
|
||||
tokenIconUrl = iconUrl,
|
||||
icon = TangemIconUM.Currency(
|
||||
CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = com.tangem.core.ui.R.drawable.ic_ethereumpow_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
tokenName = name,
|
||||
tokenSymbol = symbol,
|
||||
accountName = accountName,
|
||||
fiatRate = "$98,765.43",
|
||||
cryptoBalance = "1.234 $symbol",
|
||||
fiatBalance = "$121,876.50",
|
||||
isBalanceHidden = false,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
|
|
@ -251,13 +253,8 @@ internal object SearchContentPreviewFixtures {
|
|||
)
|
||||
|
||||
private fun portfolioTwo(): ImmutableList<UserAssetItemUM> = persistentListOf(
|
||||
userAsset(id = "p1", name = "Ethereum", symbol = "ETH", accountName = "Main wallet"),
|
||||
userAsset(
|
||||
id = "p2",
|
||||
name = "Polygon",
|
||||
symbol = "POL",
|
||||
accountName = "Account with a long label for preview",
|
||||
),
|
||||
userAsset(id = "p1", name = "Ethereum", symbol = "ETH"),
|
||||
userAsset(id = "p2", name = "Polygon", symbol = "POL"),
|
||||
)
|
||||
|
||||
private fun marketListShort(): ImmutableList<MarketsListItemUM> = persistentListOf(
|
||||
|
|
@ -390,6 +387,7 @@ private val SearchContentPreviewCallbacks = SearchCallbacks(
|
|||
onClearHintsClick = {},
|
||||
onTextHintClick = { _ -> },
|
||||
onResultMarketTokenClick = { _ -> },
|
||||
onHistoryTokenClick = { _ -> },
|
||||
)
|
||||
|
||||
/** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ internal data class SearchCallbacks(
|
|||
val onClearHintsClick: () -> Unit,
|
||||
val onTextHintClick: (hint: String) -> Unit,
|
||||
val onResultMarketTokenClick: (MarketsListItemUM) -> Unit,
|
||||
val onHistoryTokenClick: (MarketsListItemUM) -> Unit,
|
||||
)
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.features.feed.ui.search.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class SearchUM(
|
||||
|
|
@ -42,11 +45,45 @@ sealed interface MarketSearchResultUM {
|
|||
|
||||
data class TextHintItemUM(val text: String)
|
||||
|
||||
data class UserAssetItemUM(
|
||||
val id: String,
|
||||
val tokenIconUrl: String?,
|
||||
val tokenName: String,
|
||||
val tokenSymbol: String,
|
||||
val accountName: String,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
@Immutable
|
||||
sealed interface UserAssetItemUM {
|
||||
val id: String
|
||||
val icon: TangemIconUM
|
||||
val tokenName: String
|
||||
val tokenSymbol: String
|
||||
val onClick: () -> Unit
|
||||
|
||||
data class Single(
|
||||
override val id: String,
|
||||
override val icon: TangemIconUM,
|
||||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val fiatRate: String?,
|
||||
val cryptoBalance: String,
|
||||
val fiatBalance: String,
|
||||
val isBalanceHidden: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : UserAssetItemUM
|
||||
|
||||
data class Grouped(
|
||||
override val id: String,
|
||||
override val icon: TangemIconUM,
|
||||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val tokensCount: Int,
|
||||
val totalCryptoBalance: String,
|
||||
val totalFiatBalance: String,
|
||||
val isBalanceHidden: Boolean,
|
||||
val children: ImmutableList<GroupedChild>,
|
||||
override val onClick: () -> Unit,
|
||||
) : UserAssetItemUM
|
||||
|
||||
data class GroupedChild(
|
||||
val walletName: String,
|
||||
val accountName: AccountNameUM,
|
||||
val accountIcon: CryptoPortfolioIcon.Icon,
|
||||
val accountColor: CryptoPortfolioIcon.Color,
|
||||
val cryptoBalance: String,
|
||||
val fiatBalance: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.network.CryptoCurrencyAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
|
|
@ -69,7 +68,6 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||
private val parseQrCodeUseCase: ParseQrCodeUseCase,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
) : Model(), SendDestinationClickIntents {
|
||||
|
|
@ -260,16 +258,16 @@ internal class SendDestinationModel @Inject constructor(
|
|||
|
||||
private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? {
|
||||
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null
|
||||
val address = when (val status = this.value) {
|
||||
is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress
|
||||
is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress
|
||||
val (paymentAccountAddress, currency) = when (val status = this.value) {
|
||||
is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress to status.cryptoCurrency
|
||||
is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress to status.cryptoCurrency
|
||||
else -> return null
|
||||
}
|
||||
val currency = tangemPayCryptoCurrencyFactory.create(wallet).getOrNull() ?: return null
|
||||
|
||||
return if (contractAddress.equals(currency.contractAddress, true)) {
|
||||
DestinationWalletUM(
|
||||
name = wallet.name,
|
||||
address = address,
|
||||
address = paymentAccountAddress,
|
||||
cryptoCurrency = currency,
|
||||
userWalletId = wallet.walletId,
|
||||
account = account,
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount > balance</ID>
|
||||
<ID>BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean</ID>
|
||||
<ID>CastNullableToNonNullableType:SetApprovalBottomSheetInProgressTransformer.kt$SetApprovalBottomSheetInProgressTransformer$as</ID>
|
||||
<ID>CastNullableToNonNullableType:SetApprovalBottomSheetTypeChangeTransformer.kt$SetApprovalBottomSheetTypeChangeTransformer$as</ID>
|
||||
<ID>MultilineLambdaItParameter:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer${ it is StakingNotification.Error || it is NotificationUM.Error || it is NotificationUM.Warning.NetworkFeeUnreachable || it is StakingNotification.Warning.TransactionInProgress || it is StakingNotification.Warning.InitializeTonAccount }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler${ val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true isNetwork && isCurrency }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingInfoNotificationsFactory.kt$StakingInfoNotificationsFactory${ it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || it.type == BalanceType.LOCKED }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingStateController.kt$StakingStateController${ it.copy( showColdWalletInteractionIcon = userWallet is UserWallet.Cold, ) }</ID>
|
||||
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId</ID>
|
||||
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:StakingFeeBlock.kt$FeeBlockPreviewProvider$contentState</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:StakingStateController.kt$StakingStateController$uiState</ID>
|
||||
<ID>UnnecessaryEventHandlerParameter:StakingInitialInfoContent.kt$onClick: (BalanceState) -> Unit</ID>
|
||||
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) }</ID>
|
||||
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -53,9 +53,9 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
params = MultiWalletCryptoCurrenciesProducer.Params(selectedUserWalletId),
|
||||
)
|
||||
.orEmpty()
|
||||
.firstOrNull {
|
||||
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
|
||||
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
|
||||
.firstOrNull { currency ->
|
||||
val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true)
|
||||
val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
|
||||
isNetwork && isCurrency
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +63,8 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
TangemLogger.e(
|
||||
"""
|
||||
Could not get crypto currency for
|
||||
|- $NETWORK_ID_KEY: $networkId
|
||||
|- $TOKEN_ID_KEY: $tokenId
|
||||
|- $NETWORK_ID_KEY: ${networkId.orEmpty()}
|
||||
|- $TOKEN_ID_KEY: ${tokenId.orEmpty()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
return@launch
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
|
|
@ -11,6 +12,7 @@ import java.math.BigDecimal
|
|||
|
||||
// TODO split this interface to click intents and other interaction events
|
||||
@Suppress("TooManyFunctions")
|
||||
@Immutable
|
||||
internal interface StakingClickIntents : AmountScreenClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
|
|
|||
|
|
@ -26,19 +26,19 @@ internal class StakingStateController @Inject constructor(
|
|||
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
|
||||
) {
|
||||
|
||||
val value: StakingUiState get() = uiState.value
|
||||
|
||||
private val mutableUiState: MutableStateFlow<StakingUiState> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
|
||||
|
||||
val value: StakingUiState get() = uiState.value
|
||||
|
||||
private val buttonsTransformer = SetButtonsStateTransformer(urlOpener)
|
||||
private val titleTransformer = SetTitleTransformer
|
||||
|
||||
fun initializeWithUserWallet(userWallet: UserWallet) {
|
||||
mutableUiState.update { state ->
|
||||
state.copy(
|
||||
showColdWalletInteractionIcon = userWallet.isColdWallet,
|
||||
isColdWalletInteractionIconVisible = userWallet.isColdWallet,
|
||||
shouldShowHoldToConfirmButton = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
|
||||
userWallet.isHotWallet,
|
||||
)
|
||||
|
|
@ -89,7 +89,7 @@ internal class StakingStateController @Inject constructor(
|
|||
actionType = StakingActionCommonType.Enter(skipEnterAmount = false),
|
||||
buttonsState = NavigationButtonsState.Empty,
|
||||
balanceState = null,
|
||||
showColdWalletInteractionIcon = true,
|
||||
isColdWalletInteractionIconVisible = true,
|
||||
shouldShowHoldToConfirmButton = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,22 +39,9 @@ internal data class StakingUiState(
|
|||
val actionType: StakingActionCommonType,
|
||||
val buttonsState: NavigationButtonsState,
|
||||
val balanceState: BalanceState?,
|
||||
val showColdWalletInteractionIcon: Boolean,
|
||||
val isColdWalletInteractionIconVisible: Boolean,
|
||||
val shouldShowHoldToConfirmButton: Boolean,
|
||||
) {
|
||||
|
||||
fun copyWrapped(
|
||||
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
|
||||
amountState: AmountState = this.amountState,
|
||||
confirmationState: StakingStates.ConfirmationState = this.confirmationState,
|
||||
validatorState: StakingStates.ValidatorState = this.validatorState,
|
||||
): StakingUiState = copy(
|
||||
initialInfoState = initialInfoState,
|
||||
amountState = amountState,
|
||||
confirmationState = confirmationState,
|
||||
validatorState = validatorState,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
internal sealed class StakingStates {
|
||||
|
||||
|
|
@ -64,7 +51,7 @@ internal sealed class StakingStates {
|
|||
sealed class InitialInfoState : StakingStates() {
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val showBanner: Boolean,
|
||||
val isBannerVisible: Boolean,
|
||||
val infoItems: ImmutableList<RoundedListWithDividersItemData>,
|
||||
val onInfoClick: (InfoType) -> Unit,
|
||||
val yieldBalance: InnerYieldBalanceState,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal object InitialStakingStatePreview {
|
||||
val defaultState = StakingStates.InitialInfoState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
showBanner = true,
|
||||
isBannerVisible = true,
|
||||
infoItems = persistentListOf(
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_available,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal class SetButtonsStateTransformer(
|
|||
return prevState.copy(buttonsState = buttonsState)
|
||||
}
|
||||
|
||||
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton? {
|
||||
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton {
|
||||
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val innerConfirmState = confirmState?.innerState
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ internal class SetButtonsStateTransformer(
|
|||
val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled()
|
||||
return NavigationButton(
|
||||
textReference = prevState.getButtonText(),
|
||||
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon },
|
||||
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.isColdWalletInteractionIconVisible },
|
||||
isDimmed = isPrimaryButtonDisabled,
|
||||
isIconVisible = isIconVisible,
|
||||
shouldShowProgress = isInProgress,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ internal class SetInitialDataStateTransformer(
|
|||
isPrimaryButtonEnabled = with(status) {
|
||||
!amount.isNullOrZero() && sources.stakingBalanceSource.isActual() && sources.networkSource.isActual()
|
||||
},
|
||||
showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
|
||||
isBannerVisible = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
|
||||
infoItems = getInfoItems(),
|
||||
onInfoClick = clickIntents::onInfoClick,
|
||||
yieldBalance = yieldBalance,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import com.tangem.utils.transformer.Transformer
|
|||
|
||||
internal class AmountCurrencyChangeStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val value: Boolean,
|
||||
private val isFiatValue: Boolean,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
|
||||
amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, isFiatValue).transform(prevState.amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ internal class SetApprovalBottomSheetInProgressTransformer(
|
|||
),
|
||||
onCancel = onDismiss,
|
||||
)
|
||||
} as TangemBottomSheetConfigContent,
|
||||
} as? TangemBottomSheetConfigContent ?: return prevState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal class SetApprovalBottomSheetTypeChangeTransformer(
|
|||
bottomSheetConfig = prevState.bottomSheetConfig?.copy(
|
||||
content = approvalBottomSheetConfig?.copy(
|
||||
data = approvalBottomSheetConfig.data.copy(approveType = approveType),
|
||||
) as TangemBottomSheetConfigContent,
|
||||
) as? TangemBottomSheetConfigContent ?: return prevState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,12 +177,12 @@ internal class AddStakingNotificationsTransformer(
|
|||
}
|
||||
|
||||
private fun isPrimaryButtonEnabled(notifications: ImmutableList<NotificationUM>, isActualSources: Boolean) =
|
||||
notifications.none {
|
||||
it is StakingNotification.Error ||
|
||||
it is NotificationUM.Error ||
|
||||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
|
||||
it is StakingNotification.Warning.TransactionInProgress ||
|
||||
it is StakingNotification.Warning.InitializeTonAccount
|
||||
notifications.none { notification ->
|
||||
notification is StakingNotification.Error ||
|
||||
notification is NotificationUM.Error ||
|
||||
notification is NotificationUM.Warning.NetworkFeeUnreachable ||
|
||||
notification is StakingNotification.Warning.TransactionInProgress ||
|
||||
notification is StakingNotification.Warning.InitializeTonAccount
|
||||
} && isActualSources
|
||||
|
||||
private fun MutableList<NotificationUM>.addStakingErrorNotifications(
|
||||
|
|
@ -302,8 +302,8 @@ internal class AddStakingNotificationsTransformer(
|
|||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
if (!isSubtractionAvailable) return
|
||||
|
||||
val showNotification = sendingAmount + feeAmount > balance
|
||||
if (showNotification) {
|
||||
val isExceedsBalance = sendingAmount + feeAmount > balance
|
||||
if (isExceedsBalance) {
|
||||
onNotEnoughFeeNotificationShow()
|
||||
val notification = if (actionType is StakingActionCommonType.Enter) {
|
||||
NotificationUM.Error.TotalExceedsBalance
|
||||
|
|
|
|||
|
|
@ -150,10 +150,10 @@ internal class StakingInfoNotificationsFactory(
|
|||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val isTron = isTron(cryptoCurrencyStatus.currency.network.rawId)
|
||||
val hasStakedBalance = (cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit)?.balance
|
||||
?.items?.any {
|
||||
it.type == BalanceType.PREPARING ||
|
||||
it.type == BalanceType.STAKED ||
|
||||
it.type == BalanceType.LOCKED
|
||||
?.items?.any { item ->
|
||||
item.type == BalanceType.PREPARING ||
|
||||
item.type == BalanceType.STAKED ||
|
||||
item.type == BalanceType.LOCKED
|
||||
} == true
|
||||
if (isTron && hasStakedBalance) {
|
||||
add(
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ internal fun StakingInitialInfoContent(
|
|||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
if (state.showBanner) {
|
||||
if (state.isBannerVisible) {
|
||||
item(key = BANNER_BLOCK_KEY) {
|
||||
Column(
|
||||
modifier = Modifier.animateItem(),
|
||||
|
|
@ -175,7 +175,7 @@ private fun LazyListScope.activeStakingBlock(
|
|||
ActiveStakingBlock(
|
||||
balance = balance,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = clickIntents::onActiveStake,
|
||||
onClick = { clickIntents.onActiveStake(balance) },
|
||||
onAnalytic = clickIntents::onActiveStakeAnalytic,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
|
|
@ -288,7 +288,7 @@ private fun StakingRewardBlock(
|
|||
private fun ActiveStakingBlock(
|
||||
balance: BalanceState,
|
||||
isBalanceHidden: Boolean,
|
||||
onClick: (BalanceState) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onAnalytic: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -304,7 +304,7 @@ private fun ActiveStakingBlock(
|
|||
enabled = balance.isClickable,
|
||||
onClick = {
|
||||
onAnalytic()
|
||||
onClick(balance)
|
||||
onClick()
|
||||
},
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
|
|
@ -351,20 +351,18 @@ private fun ActiveStakingBlock(
|
|||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (balance.formattedCryptoAmount != null) {
|
||||
Text(
|
||||
text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) {
|
||||
private fun StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) {
|
||||
if (balance.hasImage() || icon != null) {
|
||||
StakingTargetIcon(
|
||||
image = if (balance.hasImage()) balance.target?.image.toImageReference() else null,
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ private fun BoxScope.FeeLoading(feeState: FeeState) {
|
|||
targetState = feeState,
|
||||
label = "Fee Loading State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeState.Loading) {
|
||||
) { state ->
|
||||
if (state == FeeState.Loading) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier.size(
|
||||
|
|
@ -128,8 +128,8 @@ private fun BoxScope.FeeError(feeState: FeeState) {
|
|||
targetState = feeState,
|
||||
label = "Fee Error State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeState.Error) {
|
||||
) { state ->
|
||||
if (state == FeeState.Error) {
|
||||
Text(
|
||||
text = DASH_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -151,13 +151,6 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va
|
|||
|
||||
private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState> {
|
||||
|
||||
override val values: Sequence<FeeState>
|
||||
get() = sequenceOf(
|
||||
contentState,
|
||||
FeeState.Loading,
|
||||
FeeState.Error,
|
||||
)
|
||||
|
||||
private val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "MATIC",
|
||||
|
|
@ -174,6 +167,13 @@ private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState> {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
)
|
||||
|
||||
override val values: Sequence<FeeState>
|
||||
get() = sequenceOf(
|
||||
contentState,
|
||||
FeeState.Loading,
|
||||
FeeState.Error,
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -2,9 +2,7 @@ package com.tangem.features.swap
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -17,8 +15,6 @@ interface SwapComponent : ComposableContentComponent {
|
|||
val isInitialReverseOrder: Boolean = false,
|
||||
val screenSource: String,
|
||||
val tangemPayInput: TangemPayInput? = null,
|
||||
val preselectedToToken: CryptoCurrencyStatus? = null,
|
||||
val preselectedAccount: Account? = null,
|
||||
) {
|
||||
data class TangemPayInput(
|
||||
val cryptoAmount: BigDecimal,
|
||||
|
|
|
|||
|
|
@ -162,8 +162,12 @@ internal class SavedSwapTransactionListConverter(
|
|||
}
|
||||
|
||||
private fun findAccountByDerivationIndex(accountList: AccountList?, derivationIndex: DerivationIndex?): Account? {
|
||||
return accountList?.accounts?.asSequence()?.filterIsInstance<Account.CryptoPortfolio>()
|
||||
?.firstOrNull { it.derivationIndex == derivationIndex }
|
||||
val accounts = accountList?.accounts ?: return null
|
||||
|
||||
return accounts.asSequence()
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.firstOrNull { it.derivationIndex == derivationIndex }
|
||||
?: accounts.firstOrNull { it is Account.Payment }.takeIf { derivationIndex == null }
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.Token.getDerivationIndex(): DerivationIndex? {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.domain.express.models)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.visa.models)
|
||||
|
||||
implementation(projects.features.swap.domain.api)
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ interface SwapInteractor {
|
|||
@Throws(IllegalStateException::class)
|
||||
suspend fun findBestQuote(
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
fromAccount: Account?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
toAccount: Account?,
|
||||
providers: List<SwapProvider>,
|
||||
amountToSwap: String,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
|
|
|
|||
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