Updated on 2026-08-14
This commit is contained in:
commit
ae15bcfc55
211 changed files with 4715 additions and 3641 deletions
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.tokens)
|
||||
|
|
@ -56,12 +57,6 @@ dependencies {
|
|||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.earn)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
// Instead, create some kind of interface for such cases.
|
||||
/* Redux -_- */
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
|
|
@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
|
|
@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
private val router: Router,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
||||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
|
|
@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
|
|
|
|||
|
|
@ -32,12 +32,10 @@ import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConv
|
|||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter
|
||||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter
|
||||
import com.tangem.features.feed.model.earn.state.EarnStateController
|
||||
import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.*
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager
|
||||
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
|
@ -69,8 +67,8 @@ internal class EarnModel @Inject constructor(
|
|||
private val earnNetworks = MutableStateFlow<EarnNetworks>(Either.Right(emptyList()))
|
||||
private val earnListConfigProvider = Provider {
|
||||
createEarnTokensListConfig(
|
||||
selectedTypeFilter = stateController.value.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.selectedNetworkFilter,
|
||||
selectedTypeFilter = stateController.value.earnFilterUM.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.earnFilterUM.selectedNetworkFilter,
|
||||
earnNetworks = earnNetworks.value,
|
||||
)
|
||||
}
|
||||
|
|
@ -104,7 +102,6 @@ internal class EarnModel @Inject constructor(
|
|||
init {
|
||||
updateInitialState()
|
||||
fetchEarnNetworks()
|
||||
fetchTopEarnTokens()
|
||||
subscribeOnStoredFilters()
|
||||
subscribeOnNetworks()
|
||||
subscribeOnBatchFlow()
|
||||
|
|
@ -117,20 +114,23 @@ internal class EarnModel @Inject constructor(
|
|||
batchFlowManager.initialLoadingError,
|
||||
batchFlowManager.paginationStatus,
|
||||
) { items, error, paginationStatus ->
|
||||
val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
EarnListStateManager.calculateState(
|
||||
items = items,
|
||||
error = error,
|
||||
paginationStatus = paginationStatus,
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
onRetryClick = { batchFlowManager.reload() },
|
||||
onRetryClick = {
|
||||
batchFlowManager.reload()
|
||||
reloadEarnNetworks()
|
||||
},
|
||||
onLoadMore = { batchFlowManager.loadMore() },
|
||||
onClearFiltersClick = ::onClearFiltersClick,
|
||||
)
|
||||
}.onEach { bestOpportunitiesState ->
|
||||
) to error
|
||||
}.onEach { (bestOpportunitiesState, error) ->
|
||||
stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState))
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
|
|
@ -156,23 +156,27 @@ internal class EarnModel @Inject constructor(
|
|||
|
||||
private fun subscribeOnStoredFilters() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
getEarnFilterUseCase()
|
||||
.collect { filter ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}
|
||||
combine(
|
||||
getEarnFilterUseCase(),
|
||||
earnNetworks,
|
||||
) { filter, networks ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
earnNetworks = networks,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchTopEarnTokens() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
stateController.update(UpdateMostlyUsedStateLoadingTransformer())
|
||||
fetchTopEarnTokensUseCase()
|
||||
}
|
||||
}
|
||||
|
|
@ -183,13 +187,21 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun reloadEarnNetworks() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
if (earnNetworks.value.isLeft()) {
|
||||
fetchEarnNetworks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* start of clicks area */
|
||||
private fun onTypeFilterClick() {
|
||||
val currentState = state.value
|
||||
bottomSheetNavigation.activate(
|
||||
FeedBottomSheetRoute.TypeFilter(
|
||||
params = EarnTypeFilterComponent.Params(
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter),
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.earnFilterUM.selectedTypeFilter),
|
||||
onFilterSelected = ::onTypeFilterOptionSelected,
|
||||
onDismiss = { bottomSheetNavigation.dismiss() },
|
||||
),
|
||||
|
|
@ -210,7 +222,7 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createNetworkFilters(): List<EarnFilterNetwork> {
|
||||
val selectedFilter = state.value.selectedNetworkFilter
|
||||
val selectedFilter = state.value.earnFilterUM.selectedNetworkFilter
|
||||
return buildList {
|
||||
add(
|
||||
EarnFilterNetwork.AllNetworks(
|
||||
|
|
@ -277,11 +289,14 @@ internal class EarnModel @Inject constructor(
|
|||
modelScope.launch(dispatchers.default) {
|
||||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter),
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(
|
||||
value = state.value.earnFilterUM.selectedNetworkFilter,
|
||||
),
|
||||
earnFilterType = type,
|
||||
),
|
||||
)
|
||||
bottomSheetNavigation.dismiss()
|
||||
reloadEarnNetworks()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +305,7 @@ internal class EarnModel @Inject constructor(
|
|||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = filter,
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter),
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.earnFilterUM.selectedTypeFilter),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -319,11 +334,13 @@ internal class EarnModel @Inject constructor(
|
|||
is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty()
|
||||
else -> null to ""
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
if (state.value.bestOpportunities !is EarnBestOpportunitiesUM.Error) {
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,9 @@ internal fun createEarnTokensListConfig(
|
|||
earnNetworks.fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { networks ->
|
||||
networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId)
|
||||
networks.filter(EarnNetwork::isAdded)
|
||||
.map(EarnNetwork::networkId)
|
||||
.ifEmpty { listOf(NO_ONE_NETWORK) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -34,4 +36,9 @@ internal fun createEarnTokensListConfig(
|
|||
networks = networks,
|
||||
isForEarn = isForEarn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This id means that backend has to return empty result
|
||||
*/
|
||||
private const val NO_ONE_NETWORK = "-1"
|
||||
|
|
@ -26,8 +26,10 @@ internal class EarnStateController @Inject constructor() {
|
|||
return EarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.domain.models.earn.EarnNetworks
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnFilterSelectedStateTransformer(
|
||||
private val earnNetworks: EarnNetworks,
|
||||
private val filterType: EarnFilterTypeUM,
|
||||
private val filterNetwork: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
earnFilterUM = prevState.earnFilterUM.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
isNetworkFilterEnabled = earnNetworks.isRight(),
|
||||
isTypeFilterEnabled = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnNetworkFilterSelectedStateTransformer(
|
||||
private val filter: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(selectedNetworkFilter = filter)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class UpdateMostlyUsedStateLoadingTransformer : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(mostlyUsed = EarnListUM.Loading)
|
||||
}
|
||||
}
|
||||
|
|
@ -91,12 +91,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
SpacerH(12.dp)
|
||||
BestOpportunitiesFilters(
|
||||
state = state.bestOpportunities,
|
||||
selectedNetworkFilterText = when (state.selectedNetworkFilter) {
|
||||
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
|
||||
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
|
||||
is EarnFilterNetworkUM.Network -> TextReference.Str(state.selectedNetworkFilter.text)
|
||||
},
|
||||
selectedTypeFilterText = state.selectedTypeFilterText,
|
||||
earnFilterUM = state.earnFilterUM,
|
||||
onNetworkFilterClick = state.onNetworkFilterClick,
|
||||
onTypeFilterClick = state.onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -151,10 +146,12 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(vertical = 32.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = st.onRetryClicked)
|
||||
|
|
@ -218,17 +215,14 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier:
|
|||
@Composable
|
||||
private fun BestOpportunitiesFilters(
|
||||
state: EarnBestOpportunitiesUM,
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer()
|
||||
else -> FilterButtons(
|
||||
selectedNetworkFilterText = selectedNetworkFilterText,
|
||||
selectedTypeFilterText = selectedTypeFilterText,
|
||||
isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered,
|
||||
earnFilterUM = earnFilterUM,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -307,9 +301,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM)
|
|||
|
||||
@Composable
|
||||
private fun FilterButtons(
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
isEnabled: Boolean,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -319,10 +311,14 @@ private fun FilterButtons(
|
|||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedNetworkFilterText,
|
||||
text = when (earnFilterUM.selectedNetworkFilter) {
|
||||
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
|
||||
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
|
||||
is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text)
|
||||
},
|
||||
onClick = onNetworkFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = earnFilterUM.isNetworkFilterEnabled,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -330,10 +326,10 @@ private fun FilterButtons(
|
|||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedTypeFilterText,
|
||||
text = earnFilterUM.selectedTypeFilter.text,
|
||||
onClick = onTypeFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = earnFilterUM.isTypeFilterEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -496,7 +492,7 @@ private fun EarnContentLoadingPreview() {
|
|||
) {
|
||||
EarnContent(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
),
|
||||
)
|
||||
|
|
@ -588,8 +584,12 @@ private fun previewEarnUM(
|
|||
): EarnUM = EarnUM(
|
||||
mostlyUsed = mostlyUsed,
|
||||
bestOpportunities = bestOpportunities,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
isTypeFilterEnabled = true,
|
||||
isNetworkFilterEnabled = true,
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal data class EarnFilterUM(
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val isTypeFilterEnabled: Boolean = true,
|
||||
val isNetworkFilterEnabled: Boolean = true,
|
||||
)
|
||||
|
|
@ -1,20 +1,14 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class EarnUM(
|
||||
val mostlyUsed: EarnListUM,
|
||||
val bestOpportunities: EarnBestOpportunitiesUM,
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val earnFilterUM: EarnFilterUM,
|
||||
val onBackClick: () -> Unit,
|
||||
val onNetworkFilterClick: () -> Unit,
|
||||
val onTypeFilterClick: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
) {
|
||||
|
||||
val selectedTypeFilterText: TextReference
|
||||
get() = selectedTypeFilter.text
|
||||
}
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -8,12 +9,18 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Composable
|
||||
internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) {
|
||||
internal fun Header(
|
||||
onSeeAllClick: () -> Unit,
|
||||
isLoading: Boolean,
|
||||
shouldShowSeeAll: Boolean,
|
||||
title: @Composable () -> Unit,
|
||||
) {
|
||||
AnimatedContent(isLoading) { animatedState ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -25,13 +32,18 @@ internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title
|
|||
if (animatedState) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp))
|
||||
} else {
|
||||
title()
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
title()
|
||||
}
|
||||
SpacerW(8.dp)
|
||||
AnimatedVisibility(shouldShowSeeAll) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -33,10 +34,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif
|
|||
text = stringResourceSafe(R.string.markets_earn_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = onSeeAllClick,
|
||||
isLoading = earnListUM is EarnListUM.Loading,
|
||||
shouldShowSeeAll = earnListUM is EarnListUM.Content,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import androidx.compose.animation.AnimatedContent
|
|||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -19,6 +14,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
|
|
@ -55,9 +51,13 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
|
|||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
shouldShowSeeAll = currentChart is MarketChartUM.Content,
|
||||
isLoading = currentChart is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
@ -88,9 +88,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall
|
|||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
shouldShowSeeAll = true,
|
||||
isLoading = marketChartConfig.marketCharts[marketChartConfig.currentSortByType] is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.graphics.Brush
|
||||
|
|
@ -19,6 +17,7 @@ import androidx.compose.ui.layout.onFirstVisible
|
|||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
|
|
@ -65,12 +64,6 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend
|
|||
@Composable
|
||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(news.content) {
|
||||
news.content.map { it.isViewed }
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
}
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
|
|
@ -104,10 +97,14 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||
isLoading = news.newsUMState == NewsUMState.LOADING,
|
||||
shouldShowSeeAll = news.newsUMState == NewsUMState.CONTENT,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
|
||||
|
|
@ -133,7 +130,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
) {
|
||||
itemsIndexed(
|
||||
items = news.content,
|
||||
key = { _, article -> article.id },
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
|
|
@ -181,10 +178,14 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
|||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = {},
|
||||
shouldShowSeeAll = false,
|
||||
isLoading = false,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
BlockCard(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject
|
|||
|
||||
/**
|
||||
* Mocking it for release/external builds to exclude SumSub dependency
|
||||
* This will never be called if the FT [isTangemPayEnabled] is off
|
||||
*/
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
internal class MockKycComponent @AssistedInject constructor(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ dependencies {
|
|||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
|
|
@ -49,12 +50,6 @@ dependencies {
|
|||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
// Instead, create some kind of interface for such cases.
|
||||
/* Redux -_- */
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package com.tangem.features.markets.portfolio.impl.model
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
|
|
@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
|
|
@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
private val router: Router,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
||||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
|
|
@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.onramp.alloffers.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
|
||||
internal interface AllOffersIntents {
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import com.tangem.core.ui.format.bigdecimal.percent
|
|||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.*
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodStatus
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed interface AllOffersStateUM {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersIntents
|
|||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
|||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.mainv2.ui.Offer
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.ui.Offer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ import com.tangem.domain.onramp.model.PaymentMethodType
|
|||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.mainv2.ui.TimingBlock
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.ui.TimingBlock
|
||||
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ import com.arkivanov.decompose.ComponentContext
|
|||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.features.onramp.alloffers.AllOffersComponent
|
||||
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
|
||||
import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig
|
||||
import com.tangem.features.onramp.main.model.OnrampMainComponentModel
|
||||
import com.tangem.features.onramp.main.ui.OnrampMainComponentContent
|
||||
import com.tangem.features.onramp.providers.SelectProviderComponent
|
||||
import com.tangem.features.onramp.main.ui.OnrampMainScreen
|
||||
import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -27,10 +28,15 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
@Assisted private val params: OnrampMainComponent.Params,
|
||||
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
|
||||
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
|
||||
private val selectProviderComponentFactory: SelectProviderComponent.Factory,
|
||||
private val allOffersComponentFactory: AllOffersComponent.Factory,
|
||||
) : OnrampMainComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnrampMainComponentModel = getOrCreateModel(params)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(onStop = model::onStop)
|
||||
}
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = null,
|
||||
|
|
@ -43,7 +49,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
val state by model.state.collectAsState()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
OnrampMainComponentContent(modifier = modifier, state = state)
|
||||
OnrampMainScreen(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +63,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
country = config.country,
|
||||
isLaunchSepa = params.isLaunchSepa,
|
||||
isLaunchSepa = false,
|
||||
onDismiss = {
|
||||
model.bottomSheetNavigation.dismiss()
|
||||
model.handleOnrampAvailable()
|
||||
|
|
@ -72,14 +78,14 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create(
|
||||
is OnrampMainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = SelectProviderComponent.Params(
|
||||
onProviderClick = model::onProviderSelected,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
selectedProviderId = config.selectedProviderId,
|
||||
selectedPaymentMethod = config.selectedPaymentMethod,
|
||||
params = AllOffersComponent.Params(
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
openRedirectPage = params.openRedirectPage,
|
||||
amountCurrencyCode = config.amountCurrencyCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ internal interface OnrampMainComponent : ComposableContentComponent {
|
|||
val source: OnrampSource,
|
||||
val openSettings: () -> Unit,
|
||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||
val isLaunchSepa: Boolean,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnrampMainComponent>
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.main.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.main.model.OnrampMainComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface OnrampMainComponentModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampMainComponentModel::class)
|
||||
fun bindOnrampSelectCountryModel(model: OnrampMainComponentModel): Model
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
package com.tangem.features.onramp.main.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.main.DefaultOnrampMainComponent
|
||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.main.model.OnrampMainComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -15,4 +19,9 @@ internal interface OnrampMainComponentModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindOnrampMainComponentFactory(factory: DefaultOnrampMainComponent.Factory): OnrampMainComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampMainComponentModel::class)
|
||||
fun bindOnrampMainComponentModel(model: OnrampMainComponentModel): Model
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class OnrampAmountBlockUM(
|
||||
val currencyUM: OnrampCurrencyUM,
|
||||
val amountFieldModel: AmountFieldModel,
|
||||
val secondaryFieldModel: OnrampAmountSecondaryFieldUM,
|
||||
)
|
||||
|
||||
internal data class OnrampCurrencyUM(
|
||||
val code: String,
|
||||
val iconUrl: String?,
|
||||
val precision: Int,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampAmountSecondaryFieldUM {
|
||||
data object Loading : OnrampAmountSecondaryFieldUM
|
||||
data class Content(val amount: TextReference) : OnrampAmountSecondaryFieldUM
|
||||
data class Error(val error: TextReference) : OnrampAmountSecondaryFieldUM
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class OnrampNewAmountBlockUM(
|
||||
val currencyUM: OnrampNewCurrencyUM,
|
||||
internal data class OnrampAmountBlockUM(
|
||||
val currencyUM: OnrampCurrencyUM,
|
||||
val amountFieldModel: AmountFieldModel,
|
||||
val secondaryFieldModel: OnrampSecondaryFieldErrorUM,
|
||||
)
|
||||
|
||||
internal data class OnrampNewCurrencyUM(
|
||||
internal data class OnrampCurrencyUM(
|
||||
val unit: String,
|
||||
val code: String,
|
||||
val iconUrl: String?,
|
||||
|
|
@ -25,9 +25,9 @@ internal sealed interface OnrampSecondaryFieldErrorUM {
|
|||
data class Error(val error: TextReference) : OnrampSecondaryFieldErrorUM
|
||||
}
|
||||
|
||||
internal sealed interface OnrampV2AmountButtonUMState {
|
||||
data class Loaded(val amountButtons: ImmutableList<OnrampAmountButtonUM>) : OnrampV2AmountButtonUMState
|
||||
data object None : OnrampV2AmountButtonUMState
|
||||
internal sealed interface OnrampAmountButtonUMState {
|
||||
data class Loaded(val amountButtons: ImmutableList<OnrampAmountButtonUM>) : OnrampAmountButtonUMState
|
||||
data object None : OnrampAmountButtonUMState
|
||||
}
|
||||
|
||||
internal data class OnrampAmountButtonUM(
|
||||
|
|
@ -2,12 +2,15 @@ package com.tangem.features.onramp.main.entity
|
|||
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
|
||||
interface OnrampIntents {
|
||||
fun onAmountValueChanged(value: String, isValuePasted: Boolean)
|
||||
internal interface OnrampIntents {
|
||||
fun onAmountValueChanged(value: String)
|
||||
fun openSettings()
|
||||
fun openCurrenciesList()
|
||||
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
|
||||
fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
)
|
||||
fun openProviders()
|
||||
fun onRefresh()
|
||||
fun onLinkClick(link: String)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampAmount
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
data class OnrampLastUpdate(
|
||||
val fromAmount: OnrampAmount,
|
||||
val countryCode: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
)
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampCountry
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed interface OnrampMainBottomSheetConfig {
|
||||
sealed interface OnrampMainBottomSheetConfig {
|
||||
@Serializable
|
||||
data class ConfirmResidency(val country: OnrampCountry) : OnrampMainBottomSheetConfig
|
||||
|
||||
|
|
@ -13,8 +12,5 @@ internal sealed interface OnrampMainBottomSheetConfig {
|
|||
data object CurrenciesList : OnrampMainBottomSheetConfig
|
||||
|
||||
@Serializable
|
||||
data class ProvidersList(
|
||||
val selectedProviderId: String,
|
||||
val selectedPaymentMethod: OnrampPaymentMethod,
|
||||
) : OnrampMainBottomSheetConfig
|
||||
data class AllOffers(val amountCurrencyCode: String) : OnrampMainBottomSheetConfig
|
||||
}
|
||||
|
|
@ -4,55 +4,29 @@ import androidx.compose.runtime.Immutable
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.onramp.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampMainComponentUM {
|
||||
|
||||
val topBarConfig: OnrampMainTopBarUM
|
||||
val buyButtonConfig: BuyButtonConfig
|
||||
val errorNotification: NotificationUM?
|
||||
|
||||
data class InitialLoading(
|
||||
val currency: String,
|
||||
val onClose: () -> Unit,
|
||||
val openSettings: () -> Unit,
|
||||
override val errorNotification: NotificationUM? = null,
|
||||
) : OnrampMainComponentUM {
|
||||
override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM(
|
||||
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
|
||||
startButtonUM = TopAppBarButtonUM.Back(
|
||||
onBackClicked = onClose,
|
||||
enabled = true,
|
||||
),
|
||||
endButtonUM = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_more_vertical_24,
|
||||
onClicked = openSettings,
|
||||
isEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig(
|
||||
text = resourceReference(R.string.common_buy),
|
||||
onClick = {},
|
||||
isEnabled = false,
|
||||
)
|
||||
}
|
||||
override val topBarConfig: OnrampMainTopBarUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
) : OnrampMainComponentUM
|
||||
|
||||
data class Content(
|
||||
override val topBarConfig: OnrampMainTopBarUM,
|
||||
override val buyButtonConfig: BuyButtonConfig,
|
||||
override val errorNotification: NotificationUM?,
|
||||
val amountBlockState: OnrampAmountBlockUM,
|
||||
val providerBlockState: OnrampProviderBlockUM,
|
||||
val offersBlockState: OnrampOffersBlockUM,
|
||||
val onrampAmountButtonUMState: OnrampAmountButtonUMState,
|
||||
) : OnrampMainComponentUM
|
||||
}
|
||||
|
||||
internal data class BuyButtonConfig(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val isEnabled: Boolean,
|
||||
internal data class OnrampMainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
val endButtonUM: TopAppBarButtonUM,
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class OnrampMainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
val endButtonUM: TopAppBarButtonUM,
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
sealed class OnrampProviderBlockUM {
|
||||
data object Empty : OnrampProviderBlockUM()
|
||||
data object Loading : OnrampProviderBlockUM()
|
||||
data class Content(
|
||||
val providerId: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
val providerName: String,
|
||||
val termsOfUseLink: String?,
|
||||
val privacyPolicyLink: String?,
|
||||
val isBestRate: Boolean,
|
||||
val onLinkClick: (String) -> Unit,
|
||||
val onClick: () -> Unit,
|
||||
) : OnrampProviderBlockUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
sealed interface OnrampProvidersUM {
|
||||
|
||||
data object Empty : OnrampProvidersUM
|
||||
|
||||
data object Loading : OnrampProvidersUM
|
||||
|
||||
data class Content(
|
||||
val providerId: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
) : OnrampProvidersUM
|
||||
}
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.converter
|
||||
package com.tangem.features.onramp.main.entity.converter
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class OnrampV2AmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
internal class OnrampAmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
) : Converter<String, OnrampV2MainComponentUM> {
|
||||
private val onrampIntents: OnrampIntents,
|
||||
) : Converter<String, OnrampMainComponentUM> {
|
||||
|
||||
override fun convert(value: String): OnrampV2MainComponentUM {
|
||||
override fun convert(value: String): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
if (state !is OnrampV2MainComponentUM.Content) return state
|
||||
if (state !is OnrampMainComponentUM.Content) return state
|
||||
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
|
|
@ -36,13 +36,13 @@ internal class OnrampV2AmountFieldChangeConverter(
|
|||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.Content {
|
||||
private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content {
|
||||
val amountFieldModel = amountBlockState.amountFieldModel.copy(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
package com.tangem.features.onramp.main.entity.factory
|
||||
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class OnrampAmountButtonUMStateFactory {
|
||||
|
|
@ -12,7 +12,7 @@ internal class OnrampAmountButtonUMStateFactory {
|
|||
currencyCode: String,
|
||||
currencySymbol: String,
|
||||
onAmountValueChanged: (String) -> Unit,
|
||||
): OnrampV2AmountButtonUMState {
|
||||
): OnrampAmountButtonUMState {
|
||||
return when (currencyCode) {
|
||||
USD_CODE, EUR_CODE -> {
|
||||
val buttons = defaultPreselectedAmount.map { value ->
|
||||
|
|
@ -22,9 +22,9 @@ internal class OnrampAmountButtonUMStateFactory {
|
|||
onClick = { onAmountValueChanged(value.toString()) },
|
||||
)
|
||||
}.toPersistentList()
|
||||
OnrampV2AmountButtonUMState.Loaded(buttons)
|
||||
OnrampAmountButtonUMState.Loaded(buttons)
|
||||
}
|
||||
else -> OnrampV2AmountButtonUMState.None
|
||||
else -> OnrampAmountButtonUMState.None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
package com.tangem.features.onramp.main.entity.factory
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -11,34 +11,34 @@ import com.tangem.domain.onramp.model.OnrampQuote
|
|||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.main.entity.converter.OnrampAmountFieldChangeConverter
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class OnrampV2AmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
internal class OnrampAmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
) {
|
||||
|
||||
private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy(
|
||||
private val onrampAmountFieldChangeConverter: OnrampAmountFieldChangeConverter by lazy(
|
||||
mode = LazyThreadSafetyMode.NONE,
|
||||
) {
|
||||
OnrampV2AmountFieldChangeConverter(
|
||||
OnrampAmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
onrampIntents = onrampIntents,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM {
|
||||
fun getOnAmountValueChange(value: String): OnrampMainComponentUM {
|
||||
return onrampAmountFieldChangeConverter.convert(value)
|
||||
}
|
||||
|
||||
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM {
|
||||
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
|
|
@ -72,9 +72,9 @@ internal class OnrampV2AmountStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getSecondaryFieldAmountErrorState(quotes: List<OnrampQuote>): OnrampV2MainComponentUM {
|
||||
fun getSecondaryFieldAmountErrorState(quotes: List<OnrampQuote>): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState
|
||||
|
|
@ -91,23 +91,23 @@ internal class OnrampV2AmountStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryFieldResetState(): OnrampV2MainComponentUM {
|
||||
fun getAmountSecondaryFieldResetState(): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
if (amountState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Empty) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
errorNotification = null,
|
||||
offersBlockState = currentState.offersBlockState,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
|
||||
amountState: OnrampNewAmountBlockUM,
|
||||
amountState: OnrampAmountBlockUM,
|
||||
): OnrampSecondaryFieldErrorUM.Error {
|
||||
val amount = error.requiredAmount.format {
|
||||
fiat(
|
||||
|
|
@ -1,24 +1,23 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
package com.tangem.features.onramp.main.entity.factory
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.onramp.model.*
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class OnrampOffersStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
) {
|
||||
|
||||
fun getOffersState(offers: List<OnrampOffersBlock>): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider.invoke()
|
||||
return when (currentState) {
|
||||
is OnrampV2MainComponentUM.InitialLoading -> currentState
|
||||
is OnrampV2MainComponentUM.Content -> {
|
||||
fun getOffersState(offers: List<OnrampOffersBlock>): OnrampMainComponentUM {
|
||||
return when (val currentState = currentStateProvider.invoke()) {
|
||||
is OnrampMainComponentUM.InitialLoading -> currentState
|
||||
is OnrampMainComponentUM.Content -> {
|
||||
if (currentState.offersBlockState is OnrampOffersBlockUM.Loading) {
|
||||
return currentState
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
|
|
@ -22,16 +24,30 @@ import java.math.BigDecimal
|
|||
|
||||
internal class OnrampStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
) {
|
||||
|
||||
fun getInitialState(currency: String, onClose: () -> Unit): OnrampMainComponentUM.InitialLoading {
|
||||
fun getInitialState(
|
||||
currency: String,
|
||||
onClose: () -> Unit,
|
||||
openSettings: () -> Unit,
|
||||
): OnrampMainComponentUM.InitialLoading {
|
||||
return OnrampMainComponentUM.InitialLoading(
|
||||
currency = currency,
|
||||
onClose = onClose,
|
||||
openSettings = onrampIntents::openSettings,
|
||||
errorNotification = null,
|
||||
topBarConfig = OnrampMainTopBarUM(
|
||||
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
|
||||
startButtonUM = TopAppBarButtonUM.Close(
|
||||
onCloseClick = onClose,
|
||||
enabled = true,
|
||||
),
|
||||
endButtonUM = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_more_vertical_24,
|
||||
onClicked = openSettings,
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -42,12 +58,19 @@ internal class OnrampStateFactory(
|
|||
is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(isEnabled = true)
|
||||
}
|
||||
|
||||
val initialAmountBlockState = getInitialAmountBlockState(currency)
|
||||
|
||||
return OnrampMainComponentUM.Content(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
buyButtonConfig = state.buyButtonConfig,
|
||||
amountBlockState = getInitialAmountBlockState(currency),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
amountBlockState = initialAmountBlockState,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = null,
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencyCode = currency.code,
|
||||
currencySymbol = currency.unit,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,21 +91,6 @@ internal class OnrampStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampMainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
buyButtonConfig = contentState.buyButtonConfig.copy(isEnabled = false),
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
|
|
@ -93,15 +101,15 @@ internal class OnrampStateFactory(
|
|||
return when (state) {
|
||||
is OnrampMainComponentUM.Content -> state.copy(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
buyButtonConfig = state.buyButtonConfig.copy(isEnabled = false),
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
|
||||
),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
)
|
||||
is OnrampMainComponentUM.InitialLoading -> state.copy(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
|
|
@ -112,6 +120,22 @@ internal class OnrampStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampMainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM {
|
||||
return OnrampAmountBlockUM(
|
||||
currencyUM = OnrampCurrencyUM(
|
||||
|
|
@ -119,11 +143,12 @@ internal class OnrampStateFactory(
|
|||
iconUrl = currency.image,
|
||||
precision = currency.precision,
|
||||
onClick = onrampIntents::openCurrenciesList,
|
||||
unit = currency.unit,
|
||||
),
|
||||
amountFieldModel = AmountFieldModel(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
onValueChange = { onrampIntents.onAmountValueChanged(value = it, isValuePasted = false) },
|
||||
onValueChange = onrampIntents::onAmountValueChanged,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
|
|
@ -139,7 +164,7 @@ internal class OnrampStateFactory(
|
|||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
),
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -149,8 +174,4 @@ internal class OnrampStateFactory(
|
|||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val PREDEFINED_SEPA_AMOUNT = "100"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity.factory.amount
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class OnrampAmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
) : Converter<OnrampAmountFieldChangeConverter.Input, OnrampMainComponentUM> {
|
||||
|
||||
override fun convert(input: Input): OnrampMainComponentUM {
|
||||
val value = input.value
|
||||
val isValuePasted = input.isValuePasted
|
||||
|
||||
val state = currentStateProvider()
|
||||
if (state !is OnrampMainComponentUM.Content) return state
|
||||
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
val amountState = state.amountBlockState
|
||||
val amountTextField = amountState.amountFieldModel
|
||||
val fiatDecimal = value.parseBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
val isDoneActionEnabled = !fiatDecimal.isNullOrZero()
|
||||
val amountFieldModel = amountState.amountFieldModel.copy(
|
||||
fiatValue = value,
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
isValuePasted = isValuePasted,
|
||||
)
|
||||
|
||||
return state.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
|
||||
),
|
||||
providerBlockState = OnrampProviderBlockUM.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content {
|
||||
val amountFieldModel = amountBlockState.amountFieldModel.copy(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO),
|
||||
fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO),
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
)
|
||||
return copy(
|
||||
amountBlockState = amountBlockState.copy(
|
||||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
|
||||
),
|
||||
buyButtonConfig = buyButtonConfig.copy(isEnabled = false),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
data class Input(val value: String, val isValuePasted: Boolean)
|
||||
}
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity.factory.amount
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.providers.entity.SelectProviderResult
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
|
||||
internal class OnrampAmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val needApplyFCARestrictions: Provider<Boolean>,
|
||||
) {
|
||||
|
||||
private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
)
|
||||
|
||||
fun getOnAmountValueChange(value: String, isValuePasted: Boolean) =
|
||||
onrampAmountFieldChangeConverter.convert(OnrampAmountFieldChangeConverter.Input(value, isValuePasted))
|
||||
|
||||
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
currencyUM = amountState.currencyUM.copy(
|
||||
code = currency.code,
|
||||
iconUrl = currency.image,
|
||||
precision = currency.precision,
|
||||
),
|
||||
amountFieldModel = amountState.amountFieldModel.copy(
|
||||
isError = false,
|
||||
fiatAmount = amountState.amountFieldModel.fiatAmount.copy(
|
||||
currencySymbol = currency.unit,
|
||||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryLoadingState(): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
|
||||
providerBlockState = OnrampProviderBlockUM.Loading,
|
||||
buyButtonConfig = currentState.buyButtonConfig.copy(isEnabled = false),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
amountFieldModel = amountState.amountFieldModel.copy(isError = false),
|
||||
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel,
|
||||
),
|
||||
buyButtonConfig = currentState.buyButtonConfig.copy(
|
||||
isEnabled = quote is OnrampQuote.Data,
|
||||
onClick = {
|
||||
if (quote is OnrampQuote.Data) {
|
||||
onrampIntents.onBuyClick(
|
||||
OnrampProviderWithQuote.Data(
|
||||
provider = quote.provider,
|
||||
paymentMethod = quote.paymentMethod,
|
||||
toAmount = quote.toAmount,
|
||||
fromAmount = quote.fromAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List<OnrampQuote>): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ProviderCalculated(
|
||||
providerName = selectedQuote.provider.info.name,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
paymentMethod = selectedQuote.paymentMethod.name,
|
||||
),
|
||||
)
|
||||
|
||||
val bestProvider = quotes.firstOrNull()
|
||||
val isMultipleQuotes = !quotes.isSingleItem()
|
||||
val isOtherQuotesHasData = quotes
|
||||
.filter { it.paymentMethod == selectedQuote.paymentMethod }
|
||||
.filterNot { it == bestProvider }
|
||||
.any { it is OnrampQuote.Data }
|
||||
|
||||
val isBestProvider = selectedQuote == bestProvider &&
|
||||
isMultipleQuotes &&
|
||||
isOtherQuotesHasData &&
|
||||
!needApplyFCARestrictions()
|
||||
|
||||
return currentState.copy(
|
||||
providerBlockState = selectedQuote.toProviderBlockState(isBestProvider),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryUpdatedState(
|
||||
providerResult: SelectProviderResult,
|
||||
isBestRate: Boolean,
|
||||
): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
val secondaryField = when (providerResult) {
|
||||
is SelectProviderResult.ProviderWithError -> {
|
||||
providerResult.quoteError.toSecondaryFieldUiModel(amountState)
|
||||
}
|
||||
is SelectProviderResult.ProviderWithQuote -> {
|
||||
val amount = providerResult.toAmount.value.format {
|
||||
crypto(symbol = providerResult.toAmount.symbol, decimals = providerResult.toAmount.decimals)
|
||||
}
|
||||
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
|
||||
}
|
||||
}
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(secondaryFieldModel = secondaryField),
|
||||
providerBlockState = OnrampProviderBlockUM.Content(
|
||||
paymentMethod = providerResult.paymentMethod,
|
||||
providerId = providerResult.provider.id,
|
||||
providerName = providerResult.provider.info.name,
|
||||
isBestRate = isBestRate && !needApplyFCARestrictions(),
|
||||
onClick = onrampIntents::openProviders,
|
||||
termsOfUseLink = providerResult.provider.info.termsOfUseLink,
|
||||
privacyPolicyLink = providerResult.provider.info.privacyPolicyLink,
|
||||
onLinkClick = onrampIntents::onLinkClick,
|
||||
),
|
||||
buyButtonConfig = currentState.buyButtonConfig.copy(
|
||||
isEnabled = providerResult is SelectProviderResult.ProviderWithQuote,
|
||||
onClick = {
|
||||
if (providerResult is SelectProviderResult.ProviderWithQuote) {
|
||||
onrampIntents.onBuyClick(
|
||||
OnrampProviderWithQuote.Data(
|
||||
provider = providerResult.provider,
|
||||
paymentMethod = providerResult.paymentMethod,
|
||||
toAmount = providerResult.toAmount,
|
||||
fromAmount = providerResult.fromAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryResetState(): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
if (amountState.secondaryFieldModel is OnrampAmountSecondaryFieldUM.Content) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(
|
||||
amount = TextReference.EMPTY,
|
||||
),
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toProviderBlockState(isBestRate: Boolean): OnrampProviderBlockUM {
|
||||
return OnrampProviderBlockUM.Content(
|
||||
paymentMethod = paymentMethod,
|
||||
providerId = provider.id,
|
||||
providerName = provider.info.name,
|
||||
isBestRate = isBestRate,
|
||||
onClick = onrampIntents::openProviders,
|
||||
termsOfUseLink = provider.info.termsOfUseLink,
|
||||
privacyPolicyLink = provider.info.privacyPolicyLink,
|
||||
onLinkClick = onrampIntents::onLinkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? {
|
||||
return when (this) {
|
||||
is OnrampQuote.Error -> null
|
||||
is OnrampQuote.Data -> {
|
||||
val amount = toAmount.value.format {
|
||||
crypto(symbol = toAmount.symbol, decimals = toAmount.decimals)
|
||||
}
|
||||
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
|
||||
}
|
||||
is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
|
||||
amountState: OnrampAmountBlockUM,
|
||||
): OnrampAmountSecondaryFieldUM.Error {
|
||||
val amount = error.requiredAmount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
|
||||
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
|
||||
)
|
||||
}
|
||||
|
||||
val errorTextRes = when (error) {
|
||||
is OnrampError.AmountError.TooBigError -> {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError())
|
||||
R.string.onramp_max_amount_restriction
|
||||
}
|
||||
is OnrampError.AmountError.TooSmallError -> {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError())
|
||||
R.string.onramp_min_amount_restriction
|
||||
}
|
||||
}
|
||||
|
||||
return OnrampAmountSecondaryFieldUM.Error(
|
||||
resourceReference(
|
||||
errorTextRes,
|
||||
wrappedList(amount),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +1,25 @@
|
|||
package com.tangem.features.onramp.main.model
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.onramp.*
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.OnrampAvailability
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampAmountStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampOffersStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT
|
||||
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
|
||||
import com.tangem.features.onramp.providers.entity.SelectProviderResult
|
||||
import com.tangem.features.onramp.utils.model.EUR_CURRENCY
|
||||
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -43,7 +30,6 @@ import com.tangem.utils.isNullOrZero
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
|
|
@ -51,85 +37,141 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
|
||||
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
|
||||
private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase,
|
||||
private val fetchQuotesUseCase: OnrampFetchQuotesUseCase,
|
||||
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
|
||||
private val fetchPairsUseCase: OnrampFetchPairsUseCase,
|
||||
private val onrampSaveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase,
|
||||
private val onrampGetDefaultCurrencyUseCase: OnrampGetDefaultCurrencyUseCase,
|
||||
private val amountInputManager: InputManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val urlOpener: UrlOpener,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
getUserCountryUseCase: GetUserCountryUseCase,
|
||||
private val getOnrampOffersUseCase: GetOnrampOffersUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model(), OnrampIntents {
|
||||
|
||||
private val params: OnrampMainComponent.Params = paramsContainer.require()
|
||||
val params = paramsContainer.require<OnrampMainComponent.Params>()
|
||||
|
||||
private var shouldForceChooseSepa = params.isLaunchSepa
|
||||
private var currencyToRestore: OnrampCurrency? = null
|
||||
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
|
||||
|
||||
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
|
||||
private var userCountry: UserCountry? = null
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountButtonUMStateFactory()
|
||||
}
|
||||
|
||||
@Suppress("PropertyUsedBeforeDeclaration")
|
||||
private val stateFactory = OnrampStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
)
|
||||
private val stateFactory: OnrampStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
val state: StateFlow<OnrampMainComponentUM>
|
||||
field = MutableStateFlow<OnrampMainComponentUM>(
|
||||
value = stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = ::onCloseClick,
|
||||
openSettings = ::openSettings,
|
||||
),
|
||||
)
|
||||
|
||||
private val amountStateFactory = OnrampAmountStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() },
|
||||
)
|
||||
private val amountStateFactory: OnrampAmountStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampOffersStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onrampIntents = this,
|
||||
)
|
||||
}
|
||||
|
||||
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
|
||||
|
||||
init {
|
||||
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
|
||||
?: UserCountry.Other(Locale.getDefault().country)
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase()
|
||||
|
||||
if (params.isLaunchSepa) {
|
||||
currencyToRestore = onrampGetDefaultCurrencyUseCase.invoke().getOrNull()
|
||||
onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY)
|
||||
}
|
||||
}
|
||||
|
||||
startLoadingQuotes()
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
subscribeOnOffers()
|
||||
}
|
||||
|
||||
private fun sendScreenOpenAnalytics() {
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
quotesTaskScheduler.cancelTask()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAmountValueChanged(value: String) {
|
||||
state.update { amountStateFactory.getOnAmountValueChange(value) }
|
||||
modelScope.launch { amountInputManager.update(value) }
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
params.openSettings.invoke()
|
||||
}
|
||||
|
||||
override fun openCurrenciesList() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened())
|
||||
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList)
|
||||
}
|
||||
|
||||
override fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
source = params.source,
|
||||
OnrampAnalyticsEvent.OnBuyClick(
|
||||
providerName = quote.provider.info.name,
|
||||
currency = currentContentState.amountBlockState.currencyUM.code,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
sendOfferClickEvent(
|
||||
quote = quote,
|
||||
onrampOfferAdvantagesUM = onrampOfferAdvantagesUM,
|
||||
categoryUM = categoryUM,
|
||||
)
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
|
||||
override fun openProviders() {
|
||||
val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return
|
||||
val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code
|
||||
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.AllOffers(amountCurrentCode))
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
state.update {
|
||||
stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = router::pop,
|
||||
openSettings = ::openSettings,
|
||||
)
|
||||
}
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
handleOnrampAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
}
|
||||
|
||||
fun handleOnrampAvailable() {
|
||||
|
|
@ -137,92 +179,6 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
subscribeToQuotesUpdate()
|
||||
}
|
||||
|
||||
fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) {
|
||||
state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) }
|
||||
|
||||
if (result.paymentMethod.id != SEPA_METHOD_ID) {
|
||||
shouldForceChooseSepa = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkResidenceCountry() {
|
||||
modelScope.launch {
|
||||
checkOnrampAvailabilityUseCase(userWallet)
|
||||
.onRight(::handleOnrampAvailability)
|
||||
.onLeft(::handleOnrampError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnrampAvailability(availability: OnrampAvailability) {
|
||||
when (availability) {
|
||||
is OnrampAvailability.Available -> handleOnrampAvailable()
|
||||
is OnrampAvailability.ConfirmResidency,
|
||||
is OnrampAvailability.NotSupported,
|
||||
-> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country))
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||
getOnrampCountryUseCase.invoke()
|
||||
.onEach { maybeCountry ->
|
||||
maybeCountry.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { country ->
|
||||
if (country == null) return@onEach
|
||||
|
||||
val wasInitialLoading = state.value is OnrampMainComponentUM.InitialLoading
|
||||
state.update { prevState ->
|
||||
if (prevState is OnrampMainComponentUM.InitialLoading) {
|
||||
stateFactory.getReadyState(country.defaultCurrency)
|
||||
} else {
|
||||
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
updatePairsAndQuotes()
|
||||
|
||||
if (wasInitialLoading && params.isLaunchSepa) {
|
||||
onAmountValueChanged(value = PREDEFINED_SEPA_AMOUNT, isValuePasted = true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||
startLoadingQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
state.update { prevState ->
|
||||
val contentState = state.value as? OnrampMainComponentUM.Content ?: return@update prevState
|
||||
|
||||
if (contentState.amountBlockState.amountFieldModel.fiatValue.isNotEmpty()) {
|
||||
amountStateFactory.getAmountSecondaryLoadingState()
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { state.update { amountStateFactory.getAmountSecondaryResetState() } },
|
||||
)
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
sendOnrampErrorAnalytic(onrampError)
|
||||
state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask())
|
||||
|
|
@ -233,11 +189,12 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
delay = UPDATE_DELAY,
|
||||
task = {
|
||||
runSuspendCatching {
|
||||
val content = state.value as? OnrampMainComponentUM.Content ?: return@runSuspendCatching
|
||||
val amountBlockState = content.amountBlockState
|
||||
if (amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) {
|
||||
return@runSuspendCatching
|
||||
}
|
||||
val amountBlockState = (state.value as? OnrampMainComponentUM.Content)?.amountBlockState
|
||||
?: return@runSuspendCatching
|
||||
|
||||
val fiatAmount = amountBlockState.amountFieldModel.fiatAmount
|
||||
if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching
|
||||
|
||||
fetchQuotesUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
amount = amountBlockState.amountFieldModel.fiatAmount,
|
||||
|
|
@ -250,6 +207,84 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun checkResidenceCountry() {
|
||||
modelScope.launch {
|
||||
checkOnrampAvailabilityUseCase(userWallet)
|
||||
.onRight(::handleOnrampAvailability)
|
||||
.onLeft(::handleOnrampError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnrampAvailability(availability: OnrampAvailability) {
|
||||
when (availability) {
|
||||
is OnrampAvailability.Available -> Unit
|
||||
is OnrampAvailability.ConfirmResidency,
|
||||
is OnrampAvailability.NotSupported,
|
||||
-> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp())
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun subscribeOnOffers() = modelScope.launch {
|
||||
getOnrampOffersUseCase
|
||||
.invoke()
|
||||
.collectLatest { maybeOffers ->
|
||||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offers ->
|
||||
val currentState = state.value
|
||||
if (currentState is OnrampMainComponentUM.Content) {
|
||||
if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) {
|
||||
state.update {
|
||||
currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
}
|
||||
return@fold
|
||||
}
|
||||
state.update {
|
||||
onrampOffersStateFactory.getOffersState(offers)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
startLoadingQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||
getOnrampCountryUseCase.invoke()
|
||||
.onEach { maybeCountry ->
|
||||
maybeCountry.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { country ->
|
||||
if (country == null) return@onEach
|
||||
state.update { prevState ->
|
||||
when (prevState) {
|
||||
is OnrampMainComponentUM.Content -> {
|
||||
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||
}
|
||||
is OnrampMainComponentUM.InitialLoading -> {
|
||||
stateFactory.getReadyState(country.defaultCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePairsAndQuotes()
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToQuotesUpdate() {
|
||||
getOnrampQuotesUseCase.invoke()
|
||||
.conflate()
|
||||
|
|
@ -262,152 +297,31 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
override fun onAmountValueChanged(value: String, isValuePasted: Boolean) {
|
||||
state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) }
|
||||
modelScope.launch { amountInputManager.update(value) }
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
params.openSettings()
|
||||
}
|
||||
|
||||
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
|
||||
if (userWallet is UserWallet.Cold && isDemoCardUseCase.invoke(userWallet.cardId)) {
|
||||
showDemoWarning()
|
||||
} else {
|
||||
val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.OnBuyClick(
|
||||
providerName = quote.provider.info.name,
|
||||
currency = currentContentState.amountBlockState.currencyUM.code,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openCurrenciesList() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened())
|
||||
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList)
|
||||
}
|
||||
|
||||
override fun openProviders() {
|
||||
val providerState = (state.value as? OnrampMainComponentUM.Content)?.providerBlockState ?: return
|
||||
val providerContentState = providerState as? OnrampProviderBlockUM.Content ?: return
|
||||
bottomSheetNavigation.activate(
|
||||
OnrampMainBottomSheetConfig.ProvidersList(
|
||||
selectedPaymentMethod = providerContentState.paymentMethod,
|
||||
selectedProviderId = providerContentState.providerId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
state.update {
|
||||
stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = router::pop,
|
||||
)
|
||||
}
|
||||
quotesTaskScheduler.cancelTask()
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkClick(link: String) = urlOpener.openUrl(link)
|
||||
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
quotesTaskScheduler.cancelTask()
|
||||
|
||||
modelScope.launch {
|
||||
if (params.isLaunchSepa) {
|
||||
currencyToRestore?.let { onrampSaveDefaultCurrencyUseCase.invoke(it) }
|
||||
}
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp())
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
|
||||
sendOnrampQuotesErrorAnalytic(quotes)
|
||||
|
||||
val quote = selectOrUpdateQuote(quotes)
|
||||
|
||||
if (quote == null) {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
lastUpdateState.value = null
|
||||
return
|
||||
}
|
||||
state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! Important quote selection logic !!!
|
||||
* Selects or updated quote based on input data (amount, country, currency).
|
||||
* If input data has changed select new best quote, otherwise last selected quote.
|
||||
* If last selected quote on same input data is in an error state, select next best quote
|
||||
* If new best quote or next best quote does not exist (i.e. Error state) select nothing.
|
||||
*/
|
||||
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
|
||||
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
|
||||
|
||||
val bestSepaQuote = if (params.isLaunchSepa && shouldForceChooseSepa) {
|
||||
quotes.filterIsInstance<OnrampQuote.Data>()
|
||||
.filter { it.paymentMethod.id == SEPA_METHOD_ID }
|
||||
.maxByOrNull { it.toAmount.value }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Check if amount, country or currency has changed
|
||||
val newQuote = bestSepaQuote ?: if (isAmountOrCountryChanged(quoteToCheck)) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
val state = state.value as? OnrampMainComponentUM.Content
|
||||
val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content
|
||||
|
||||
// Get current selected quote to update
|
||||
val lastSelectedQuote = quotes.firstOrNull { quote ->
|
||||
quote.provider.id == providerState?.providerId &&
|
||||
quote.paymentMethod.id == providerState.paymentMethod.id
|
||||
when {
|
||||
quotes.isEmpty() -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
|
||||
// Check if selected updated quote is not error
|
||||
if (lastSelectedQuote is OnrampQuote.Error) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
lastSelectedQuote
|
||||
quotes.all { it is OnrampQuote.AmountError } -> {
|
||||
state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) }
|
||||
}
|
||||
quotes.none { it is OnrampQuote.Data } -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
else -> {
|
||||
state.update { prevState ->
|
||||
val resetState = amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
if (prevState is OnrampMainComponentUM.Content &&
|
||||
resetState is OnrampMainComponentUM.Content &&
|
||||
prevState.offersBlockState is OnrampOffersBlockUM.Loading
|
||||
) {
|
||||
resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
} else {
|
||||
resetState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newQuote != null) {
|
||||
updateProvider(newQuote, quotes)
|
||||
}
|
||||
|
||||
return newQuote
|
||||
}
|
||||
|
||||
private fun updateProvider(quote: OnrampQuote, quotes: List<OnrampQuote>) {
|
||||
lastUpdateState.value = OnrampLastUpdate(
|
||||
quote.fromAmount,
|
||||
quote.countryCode,
|
||||
quote.paymentMethod,
|
||||
)
|
||||
|
||||
if (quote.paymentMethod.id != SEPA_METHOD_ID) {
|
||||
shouldForceChooseSepa = false
|
||||
}
|
||||
|
||||
state.update {
|
||||
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -415,41 +329,30 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
state.update { prevState ->
|
||||
(prevState as? OnrampMainComponentUM.Content)?.copy(
|
||||
errorNotification = null,
|
||||
providerBlockState = OnrampProviderBlockUM.Loading,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
amountBlockState = prevState.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
) ?: prevState
|
||||
}
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
private fun showDemoWarning() {
|
||||
val alertUM = AlertDemoModeUM(onConfirmClick = {})
|
||||
val message = DialogMessage(
|
||||
title = alertUM.title,
|
||||
message = alertUM.message,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = alertUM.confirmButtonText,
|
||||
onClick = alertUM.onConfirmClick,
|
||||
)
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = {
|
||||
state.update {
|
||||
amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
}
|
||||
startLoadingQuotes()
|
||||
},
|
||||
secondActionBuilder = { cancelAction() },
|
||||
)
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
private fun sendOnrampErrorAnalytic(error: OnrampError) {
|
||||
val content = state.value as? OnrampMainComponentUM.Content
|
||||
val providerContent = content?.providerBlockState as? OnrampProviderBlockUM.Content
|
||||
analyticsEventHandler.sendOnrampErrorEvent(
|
||||
error = error,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = providerContent?.providerName,
|
||||
paymentMethod = providerContent?.paymentMethod?.name,
|
||||
)
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
|
||||
|
|
@ -467,20 +370,48 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
providerName = errorState.provider.info.name,
|
||||
paymentMethod = errorState.paymentMethod.name,
|
||||
)
|
||||
else -> { /* no-op */
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAmountOrCountryChanged(quote: OnrampQuote?): Boolean {
|
||||
return lastUpdateState.value?.fromAmount != quote?.fromAmount ||
|
||||
lastUpdateState.value?.countryCode != quote?.countryCode
|
||||
private fun sendScreenOpenAnalytics() {
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
source = params.source,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendOfferClickEvent(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val event = when (categoryUM) {
|
||||
OnrampOfferCategoryUM.RecentlyUsed -> {
|
||||
OnrampAnalyticsEvent.RecentlyBuyClicked(
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethod = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
OnrampOfferCategoryUM.Recommended -> {
|
||||
onrampOfferAdvantagesUM.toAnalyticsEvent(
|
||||
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethodName = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (event != null) {
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val UPDATE_DELAY = 10_000L
|
||||
|
||||
const val SEPA_METHOD_ID = "sepa"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -17,61 +19,89 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampCurrencyUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampSecondaryFieldErrorUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier = Modifier) {
|
||||
internal fun OnrampAmountContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(vertical = TangemTheme.dimens.spacing28),
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||
)
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||
.animateContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnrampCurrencyIcon(currencyUM = state.currencyUM)
|
||||
OnrampAmountField(amountField = state.amountFieldModel)
|
||||
OnrampAmountSecondary(state = state.secondaryFieldModel)
|
||||
OnrampHeaderTitle()
|
||||
|
||||
OnrampAmountField(
|
||||
amountField = state.amountBlockState.amountFieldModel,
|
||||
currencyCode = state.amountBlockState.currencyUM.code,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty,
|
||||
) {
|
||||
if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) {
|
||||
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountField(amountField: AmountFieldModel) {
|
||||
private fun OnrampHeaderTitle() {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_you_will_pay_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val requester = remember { FocusRequester() }
|
||||
val symbolColor = if (amountField.fiatValue.isBlank()) {
|
||||
TangemTheme.colors.text.disabled
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
}
|
||||
AmountTextField(
|
||||
value = amountField.fiatValue,
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
symbol = amountField.fiatAmount.currencySymbol,
|
||||
currencyCode = amountField.fiatAmount.currencySymbol,
|
||||
symbol = currencyCode,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
symbolColor = symbolColor,
|
||||
symbolColor = if (amountField.fiatValue.isBlank()) {
|
||||
TangemTheme.colors.text.disabled
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
),
|
||||
onValueChange = amountField.onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
keyboardActions = amountField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
textStyle = TangemTheme.typography.head.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
|
|
@ -82,7 +112,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel) {
|
|||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
|
|
@ -96,7 +127,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) {
|
||||
private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -107,24 +138,12 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) {
|
|||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampAmountSecondaryFieldUM.Content -> Text(
|
||||
text = state.amount.resolveReference(),
|
||||
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
is OnrampAmountSecondaryFieldUM.Error -> Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
is OnrampAmountSecondaryFieldUM.Loading -> TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.width(TangemTheme.dimens.size62),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,20 +151,27 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) {
|
|||
private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(onClick = currencyUM.onClick)
|
||||
.padding(start = TangemTheme.dimens.spacing24),
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.size(20.dp)
|
||||
.clip(CircleShape)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON),
|
||||
model = currencyUM.iconUrl,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = currencyUM.code,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.extensions.appendColored
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
|
||||
|
||||
private const val TERMS_OF_USE_KEY = "termsOfUse"
|
||||
private const val PRIVACY_POLICY_KEY = "privacyPolicy"
|
||||
|
||||
@Composable
|
||||
internal fun OnrampButtonComponent(state: OnrampMainComponentUM) {
|
||||
val content = state as? OnrampMainComponentUM.Content
|
||||
val providerState = content?.providerBlockState as? OnrampProviderBlockUM.Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
OnrampTosText(providerState)
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_buy),
|
||||
onClick = state.buyButtonConfig.onClick,
|
||||
enabled = state.buyButtonConfig.isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) {
|
||||
val termsOfUse = stringResourceSafe(R.string.common_terms_of_use)
|
||||
val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy)
|
||||
val tosText = stringResourceSafe(R.string.onramp_legal, termsOfUse, privacyPolicy)
|
||||
|
||||
val clickableAnnotation = buildAnnotatedString {
|
||||
append(tosText.substringBefore(termsOfUse))
|
||||
|
||||
pushStringAnnotation(TERMS_OF_USE_KEY, "")
|
||||
appendColored(termsOfUse, TangemTheme.colors.text.accent)
|
||||
pop()
|
||||
|
||||
append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy))
|
||||
|
||||
pushStringAnnotation(PRIVACY_POLICY_KEY, "")
|
||||
appendColored(privacyPolicy, TangemTheme.colors.text.accent)
|
||||
pop()
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = provider,
|
||||
transitionSpec = { fadeIn().togetherWith(fadeOut()) },
|
||||
label = "Onramp Legal Info Animation",
|
||||
) { state ->
|
||||
val termsOfUseLink = provider?.termsOfUseLink
|
||||
val privacyPolicyLink = provider?.privacyPolicyLink
|
||||
|
||||
if (state != null && termsOfUseLink != null && privacyPolicyLink != null) {
|
||||
ClickableText(
|
||||
text = clickableAnnotation,
|
||||
style = TangemTheme.typography.caption2.copy(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
onClick = { offset ->
|
||||
val tosAnnotations = clickableAnnotation.getStringAnnotations(
|
||||
tag = TERMS_OF_USE_KEY,
|
||||
start = offset,
|
||||
end = offset,
|
||||
)
|
||||
|
||||
if (tosAnnotations.any()) {
|
||||
state.onLinkClick(termsOfUseLink)
|
||||
}
|
||||
|
||||
val privacyPolicyAnnotations = clickableAnnotation.getStringAnnotations(
|
||||
tag = PRIVACY_POLICY_KEY,
|
||||
start = offset,
|
||||
end = offset,
|
||||
)
|
||||
|
||||
if (privacyPolicyAnnotations.any()) {
|
||||
state.onLinkClick(privacyPolicyLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
|
|
@ -20,13 +20,13 @@ import com.tangem.core.ui.components.Keyboard
|
|||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState
|
||||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOffersBlockUM
|
||||
|
||||
@Composable
|
||||
internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
internal fun BoxScope.OnrampFooterContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
|
|
@ -53,16 +53,16 @@ internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) {
|
||||
private fun OnrampAmountButtons(state: OnrampAmountButtonUMState) {
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state is OnrampV2AmountButtonUMState.Loaded,
|
||||
visible = state is OnrampAmountButtonUMState.Loaded,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampV2AmountButtonUMState.Loaded -> {
|
||||
is OnrampAmountButtonUMState.Loaded -> {
|
||||
if (keyboard is Keyboard.Opened) {
|
||||
LazyRow(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.button.secondary),
|
||||
|
|
@ -82,7 +82,7 @@ private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) {
|
|||
}
|
||||
}
|
||||
}
|
||||
OnrampV2AmountButtonUMState.None -> Unit
|
||||
OnrampAmountButtonUMState.None -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,12 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
|
|
@ -21,41 +20,58 @@ import com.tangem.core.ui.utils.WindowInsetsZero
|
|||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) {
|
||||
internal fun OnrampMainScreen(state: OnrampMainComponentUM, modifier: Modifier = Modifier) {
|
||||
Scaffold(
|
||||
modifier = modifier.imePadding(),
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
modifier = modifier.systemBarsPadding(),
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
startButton = state.topBarConfig.startButtonUM,
|
||||
endButton = state.topBarConfig.endButtonUM,
|
||||
title = state.topBarConfig.title.resolveReference(),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
val contentModifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
) { scaffoldPaddings ->
|
||||
OnrampMainComponentContent(
|
||||
state = state,
|
||||
modifier = Modifier.padding(scaffoldPaddings),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier, state = state)
|
||||
is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state)
|
||||
is OnrampMainComponentUM.InitialLoading -> InitialLoading(state = state)
|
||||
is OnrampMainComponentUM.Content -> Content(state = state)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
OnrampButtonComponent(state)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
)
|
||||
}
|
||||
|
||||
if (state is OnrampMainComponentUM.Content) {
|
||||
OnrampFooterContent(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampAmountContentLoading()
|
||||
|
|
@ -64,27 +80,38 @@ private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) {
|
||||
private fun OnrampAmountContentLoading() {
|
||||
Column(
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(vertical = TangemTheme.dimens.spacing28),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size40))
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16)
|
||||
.size(width = TangemTheme.dimens.size96, height = TangemTheme.dimens.size24),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
.size(width = 76.dp, height = 20.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16)
|
||||
.size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.size(width = 136.dp, height = 44.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.size(width = 52.dp, height = 16.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing20)
|
||||
.size(width = 84.dp, height = 28.dp),
|
||||
radius = TangemTheme.dimens.radius14,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -93,13 +120,20 @@ private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) {
|
|||
private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = TangemTheme.dimens.spacing76),
|
||||
.padding(
|
||||
bottom = 76.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampAmountContent(state = state.amountBlockState)
|
||||
OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth())
|
||||
OnrampAmountContent(state = state)
|
||||
|
||||
OnrampOffersContent(state = state.offersBlockState)
|
||||
|
||||
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
|
@ -33,7 +33,7 @@ import com.tangem.core.ui.test.OnrampOffersBlockTestTags
|
|||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.tangem.core.ui.extensions.appendSpace
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
|
||||
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
|
||||
|
||||
@Composable
|
||||
internal fun OnrampProviderContent(state: OnrampProviderBlockUM, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is OnrampProviderBlockUM.Empty -> Unit
|
||||
is OnrampProviderBlockUM.Loading -> OnrampProviderLoading(modifier)
|
||||
is OnrampProviderBlockUM.Content -> OnrampProviderBlock(modifier = modifier, state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = state.onClick)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
PaymentMethodIcon(imageUrl = state.paymentMethod.imageUrl)
|
||||
Column(modifier = Modifier.weight(1F)) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResourceSafe(id = R.string.onramp_pay_with))
|
||||
appendSpace()
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
fontWeight = TangemTheme.typography.subtitle2.fontWeight,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
),
|
||||
) {
|
||||
append(state.paymentMethod.name)
|
||||
}
|
||||
},
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResourceSafe(id = R.string.onramp_via))
|
||||
appendSpace()
|
||||
append(state.providerName)
|
||||
},
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = state.isBestRate,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
label = "Best Rate visibility animation",
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius4),
|
||||
)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing6,
|
||||
vertical = TangemTheme.dimens.spacing1,
|
||||
),
|
||||
text = stringResourceSafe(id = R.string.express_provider_best_rate),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampProviderLoading(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.express_provider),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.express_fetch_best_rates),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.features.onramp.alloffers.AllOffersComponent
|
||||
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig
|
||||
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
||||
import com.tangem.features.onramp.mainv2.ui.OnrampNewMainScreen
|
||||
import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: OnrampV2MainComponent.Params,
|
||||
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
|
||||
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
|
||||
private val allOffersComponentFactory: AllOffersComponent.Factory,
|
||||
) : OnrampV2MainComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnrampV2MainComponentModel = getOrCreateModel(params)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(onStop = model::onStop)
|
||||
}
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = null,
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsState()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
OnrampNewMainScreen(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: OnrampV2MainBottomSheetConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
is OnrampV2MainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = ConfirmResidencyComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
country = config.country,
|
||||
isLaunchSepa = false,
|
||||
onDismiss = {
|
||||
model.bottomSheetNavigation.dismiss()
|
||||
model.handleOnrampAvailable()
|
||||
},
|
||||
),
|
||||
)
|
||||
is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = SelectCurrencyComponent.Params(
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
is OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AllOffersComponent.Params(
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
openRedirectPage = params.openRedirectPage,
|
||||
amountCurrencyCode = config.amountCurrencyCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnrampV2MainComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: OnrampV2MainComponent.Params,
|
||||
): DefaultOnrampV2MainComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
class DefaultOnrampV2MainFeatureToggle(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : OnrampV2MainFeatureToggle {
|
||||
override val isOnrampNewMainEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED")
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
|
||||
internal interface OnrampV2MainComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val source: OnrampSource,
|
||||
val openSettings: () -> Unit,
|
||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnrampV2MainComponent>
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
internal interface OnrampV2MainFeatureToggle {
|
||||
val isOnrampNewMainEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface OnrampMainV2ComponentModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampV2MainComponentModel::class)
|
||||
fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainFeatureToggle
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface OnrampNewMainComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnrampV2MainComponentFactory(factory: DefaultOnrampV2MainComponent.Factory): OnrampV2MainComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureToggleModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOnrampV2MainFeatureToggle(featureTogglesManager: FeatureTogglesManager): OnrampV2MainFeatureToggle {
|
||||
return DefaultOnrampV2MainFeatureToggle(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
|
||||
internal interface OnrampV2Intents {
|
||||
fun onAmountValueChanged(value: String)
|
||||
fun openSettings()
|
||||
fun openCurrenciesList()
|
||||
fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
)
|
||||
fun openProviders()
|
||||
fun onRefresh()
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampCountry
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface OnrampV2MainBottomSheetConfig {
|
||||
@Serializable
|
||||
data class ConfirmResidency(val country: OnrampCountry) : OnrampV2MainBottomSheetConfig
|
||||
|
||||
@Serializable
|
||||
data object CurrenciesList : OnrampV2MainBottomSheetConfig
|
||||
|
||||
@Serializable
|
||||
data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampV2MainComponentUM {
|
||||
|
||||
val topBarConfig: OnrampV2MainTopBarUM
|
||||
val errorNotification: NotificationUM?
|
||||
|
||||
data class InitialLoading(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
) : OnrampV2MainComponentUM
|
||||
|
||||
data class Content(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
val amountBlockState: OnrampNewAmountBlockUM,
|
||||
val offersBlockState: OnrampOffersBlockUM,
|
||||
val onrampAmountButtonUMState: OnrampV2AmountButtonUMState,
|
||||
) : OnrampV2MainComponentUM
|
||||
}
|
||||
|
||||
internal data class OnrampV2MainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
val endButtonUM: TopAppBarButtonUM,
|
||||
)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
sealed interface OnrampV2ProvidersUM {
|
||||
|
||||
data object Empty : OnrampV2ProvidersUM
|
||||
|
||||
data object Loading : OnrampV2ProvidersUM
|
||||
|
||||
data class Content(
|
||||
val providerId: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
) : OnrampV2ProvidersUM
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.tokens.model.convertToAmount
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class OnrampV2StateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
) {
|
||||
|
||||
fun getInitialState(
|
||||
currency: String,
|
||||
onClose: () -> Unit,
|
||||
openSettings: () -> Unit,
|
||||
): OnrampV2MainComponentUM.InitialLoading {
|
||||
return OnrampV2MainComponentUM.InitialLoading(
|
||||
errorNotification = null,
|
||||
topBarConfig = OnrampV2MainTopBarUM(
|
||||
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
|
||||
startButtonUM = TopAppBarButtonUM.Close(
|
||||
onCloseClick = onClose,
|
||||
enabled = true,
|
||||
),
|
||||
endButtonUM = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_more_vertical_24,
|
||||
onClicked = openSettings,
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content {
|
||||
val state = currentStateProvider()
|
||||
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(isEnabled = true)
|
||||
}
|
||||
|
||||
val initialAmountBlockState = getInitialAmountBlockState(currency)
|
||||
|
||||
return OnrampV2MainComponentUM.Content(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
amountBlockState = initialAmountBlockState,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = null,
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencyCode = currency.code,
|
||||
currencySymbol = currency.unit,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnrampErrorState(onrampError: OnrampError): OnrampV2MainComponentUM {
|
||||
return when (onrampError) {
|
||||
OnrampError.PairsNotFound -> getNoPairsErrorState()
|
||||
is OnrampError.DataError -> getErrorState(
|
||||
errorCode = onrampError.code,
|
||||
onRefresh = onrampIntents::onRefresh,
|
||||
)
|
||||
is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh)
|
||||
is OnrampError.AmountError.TooBigError,
|
||||
is OnrampError.AmountError.TooSmallError,
|
||||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
OnrampError.AlreadyHandledTransaction,
|
||||
-> currentStateProvider() // ignore error state
|
||||
}
|
||||
}
|
||||
|
||||
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(isEnabled = true)
|
||||
}
|
||||
|
||||
return when (state) {
|
||||
is OnrampV2MainComponentUM.Content -> state.copy(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
)
|
||||
is OnrampV2MainComponentUM.InitialLoading -> state.copy(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampV2MainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM {
|
||||
return OnrampNewAmountBlockUM(
|
||||
currencyUM = OnrampNewCurrencyUM(
|
||||
code = currency.code,
|
||||
iconUrl = currency.image,
|
||||
precision = currency.precision,
|
||||
onClick = onrampIntents::openCurrenciesList,
|
||||
unit = currency.unit,
|
||||
),
|
||||
amountFieldModel = AmountFieldModel(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
onValueChange = onrampIntents::onAmountValueChanged,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
isFiatValue = true,
|
||||
cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency),
|
||||
fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency),
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = TextReference.EMPTY,
|
||||
isFiatUnavailable = false,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount(
|
||||
currencySymbol = currency.unit,
|
||||
value = this,
|
||||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,417 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.domain.onramp.*
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.OnrampAvailability
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampOffersStateFactory
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory
|
||||
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.PeriodicTask
|
||||
import com.tangem.utils.coroutines.SingleTaskScheduler
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class OnrampV2MainComponentModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
|
||||
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
|
||||
private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase,
|
||||
private val fetchQuotesUseCase: OnrampFetchQuotesUseCase,
|
||||
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
|
||||
private val fetchPairsUseCase: OnrampFetchPairsUseCase,
|
||||
private val amountInputManager: InputManager,
|
||||
private val getOnrampOffersUseCase: GetOnrampOffersUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model(), OnrampV2Intents {
|
||||
|
||||
val params = paramsContainer.require<OnrampV2MainComponent.Params>()
|
||||
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountButtonUMStateFactory()
|
||||
}
|
||||
|
||||
@Suppress("PropertyUsedBeforeDeclaration")
|
||||
private val stateFactory: OnrampV2StateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampV2StateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
val state: StateFlow<OnrampV2MainComponentUM>
|
||||
field = MutableStateFlow<OnrampV2MainComponentUM>(
|
||||
value = stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = ::onCloseClick,
|
||||
openSettings = ::openSettings,
|
||||
),
|
||||
)
|
||||
|
||||
private val amountStateFactory: OnrampV2AmountStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampV2AmountStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampOffersStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onrampIntents = this,
|
||||
)
|
||||
}
|
||||
|
||||
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampV2MainBottomSheetConfig> = SlotNavigation()
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase()
|
||||
}
|
||||
startLoadingQuotes()
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
subscribeOnOffers()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
quotesTaskScheduler.cancelTask()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAmountValueChanged(value: String) {
|
||||
state.update { amountStateFactory.getOnAmountValueChange(value) }
|
||||
modelScope.launch { amountInputManager.update(value) }
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
params.openSettings.invoke()
|
||||
}
|
||||
|
||||
override fun openCurrenciesList() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened())
|
||||
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList)
|
||||
}
|
||||
|
||||
override fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.OnBuyClick(
|
||||
providerName = quote.provider.info.name,
|
||||
currency = currentContentState.amountBlockState.currencyUM.code,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
sendOfferClickEvent(
|
||||
quote = quote,
|
||||
onrampOfferAdvantagesUM = onrampOfferAdvantagesUM,
|
||||
categoryUM = categoryUM,
|
||||
)
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
|
||||
override fun openProviders() {
|
||||
val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return
|
||||
val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code
|
||||
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers(amountCurrentCode))
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
state.update {
|
||||
stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = router::pop,
|
||||
openSettings = ::openSettings,
|
||||
)
|
||||
}
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
handleOnrampAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
}
|
||||
|
||||
fun handleOnrampAvailable() {
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask())
|
||||
}
|
||||
|
||||
private fun loadQuotesTask(): PeriodicTask<Unit> {
|
||||
return PeriodicTask(
|
||||
delay = UPDATE_DELAY,
|
||||
task = {
|
||||
runSuspendCatching {
|
||||
val amountBlockState = (state.value as? OnrampV2MainComponentUM.Content)?.amountBlockState
|
||||
?: return@runSuspendCatching
|
||||
|
||||
val fiatAmount = amountBlockState.amountFieldModel.fiatAmount
|
||||
if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching
|
||||
|
||||
fetchQuotesUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
amount = amountBlockState.amountFieldModel.fiatAmount,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
).onLeft(::handleOnrampError)
|
||||
}
|
||||
},
|
||||
onSuccess = {},
|
||||
onError = {},
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkResidenceCountry() {
|
||||
modelScope.launch {
|
||||
checkOnrampAvailabilityUseCase(userWallet)
|
||||
.onRight(::handleOnrampAvailability)
|
||||
.onLeft(::handleOnrampError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnrampAvailability(availability: OnrampAvailability) {
|
||||
when (availability) {
|
||||
is OnrampAvailability.Available -> Unit
|
||||
is OnrampAvailability.ConfirmResidency,
|
||||
is OnrampAvailability.NotSupported,
|
||||
-> bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.ConfirmResidency(availability.country))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp())
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun subscribeOnOffers() = modelScope.launch {
|
||||
getOnrampOffersUseCase
|
||||
.invoke()
|
||||
.collectLatest { maybeOffers ->
|
||||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offers ->
|
||||
val currentState = state.value
|
||||
if (currentState is OnrampV2MainComponentUM.Content) {
|
||||
if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) {
|
||||
state.update {
|
||||
currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
}
|
||||
return@fold
|
||||
}
|
||||
state.update {
|
||||
onrampOffersStateFactory.getOffersState(offers)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
startLoadingQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||
getOnrampCountryUseCase.invoke()
|
||||
.onEach { maybeCountry ->
|
||||
maybeCountry.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { country ->
|
||||
if (country == null) return@onEach
|
||||
state.update { prevState ->
|
||||
when (prevState) {
|
||||
is OnrampV2MainComponentUM.Content -> {
|
||||
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||
}
|
||||
is OnrampV2MainComponentUM.InitialLoading -> {
|
||||
stateFactory.getReadyState(country.defaultCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePairsAndQuotes()
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToQuotesUpdate() {
|
||||
getOnrampQuotesUseCase.invoke()
|
||||
.conflate()
|
||||
.onEach { maybeQuotes ->
|
||||
maybeQuotes.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = ::handleQuoteResult,
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
|
||||
sendOnrampQuotesErrorAnalytic(quotes)
|
||||
when {
|
||||
quotes.isEmpty() -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
quotes.all { it is OnrampQuote.AmountError } -> {
|
||||
state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) }
|
||||
}
|
||||
quotes.none { it is OnrampQuote.Data } -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
else -> {
|
||||
state.update { prevState ->
|
||||
val resetState = amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
if (prevState is OnrampV2MainComponentUM.Content &&
|
||||
resetState is OnrampV2MainComponentUM.Content &&
|
||||
prevState.offersBlockState is OnrampOffersBlockUM.Loading
|
||||
) {
|
||||
resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
} else {
|
||||
resetState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetryQuotes() {
|
||||
state.update { prevState ->
|
||||
(prevState as? OnrampV2MainComponentUM.Content)?.copy(
|
||||
errorNotification = null,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
amountBlockState = prevState.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
) ?: prevState
|
||||
}
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = {
|
||||
state.update {
|
||||
amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
}
|
||||
startLoadingQuotes()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
|
||||
quotes.forEach { errorState ->
|
||||
when (errorState) {
|
||||
is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent(
|
||||
error = errorState.error,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = errorState.provider.info.name,
|
||||
paymentMethod = errorState.paymentMethod.name,
|
||||
)
|
||||
is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent(
|
||||
error = errorState.error,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = errorState.provider.info.name,
|
||||
paymentMethod = errorState.paymentMethod.name,
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendScreenOpenAnalytics() {
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
source = params.source,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendOfferClickEvent(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val event = when (categoryUM) {
|
||||
OnrampOfferCategoryUM.RecentlyUsed -> {
|
||||
OnrampAnalyticsEvent.RecentlyBuyClicked(
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethod = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
OnrampOfferCategoryUM.Recommended -> {
|
||||
onrampOfferAdvantagesUM.toAnalyticsEvent(
|
||||
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethodName = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (event != null) {
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val UPDATE_DELAY = 10_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampNewMainScreen(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) {
|
||||
Scaffold(
|
||||
modifier = modifier.systemBarsPadding(),
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
startButton = state.topBarConfig.startButtonUM,
|
||||
endButton = state.topBarConfig.endButtonUM,
|
||||
title = state.topBarConfig.title.resolveReference(),
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
) { scaffoldPaddings ->
|
||||
OnrampNewMainComponentContent(
|
||||
state = state,
|
||||
modifier = Modifier.padding(scaffoldPaddings),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampV2MainComponentUM.InitialLoading -> InitialLoading(state = state)
|
||||
is OnrampV2MainComponentUM.Content -> Content(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
if (state is OnrampV2MainComponentUM.Content) {
|
||||
OnrampFooterContent(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampAmountContentLoading()
|
||||
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountContentLoading() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(vertical = TangemTheme.dimens.spacing28),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16)
|
||||
.size(width = 76.dp, height = 20.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.size(width = 136.dp, height = 44.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.size(width = 52.dp, height = 16.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing20)
|
||||
.size(width = 84.dp, height = 28.dp),
|
||||
radius = TangemTheme.dimens.radius14,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
bottom = 76.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampV2AmountContent(state = state)
|
||||
|
||||
OnrampOffersContent(state = state.offersBlockState)
|
||||
|
||||
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampSecondaryFieldErrorUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||
)
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||
.animateContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnrampHeaderTitle()
|
||||
|
||||
OnrampAmountField(
|
||||
amountField = state.amountBlockState.amountFieldModel,
|
||||
currencyCode = state.amountBlockState.currencyUM.code,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty,
|
||||
) {
|
||||
if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) {
|
||||
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampHeaderTitle() {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_you_will_pay_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val requester = remember { FocusRequester() }
|
||||
AmountTextField(
|
||||
value = amountField.fiatValue,
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
symbol = currencyCode,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
symbolColor = if (amountField.fiatValue.isBlank()) {
|
||||
TangemTheme.colors.text.disabled
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
),
|
||||
onValueChange = amountField.onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
keyboardActions = amountField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.head.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
isEnabled = !amountField.isError,
|
||||
isAutoResize = true,
|
||||
isValuePasted = amountField.isValuePasted,
|
||||
onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss,
|
||||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.requiredHeightIn(min = TangemTheme.dimens.size32)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD),
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampCurrencyIcon(currencyUM: OnrampNewCurrencyUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(onClick = currencyUM.onClick)
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clip(CircleShape)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON),
|
||||
model = currencyUM.iconUrl,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = currencyUM.code,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,6 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.onramp.component.OnrampComponent
|
||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle
|
||||
import com.tangem.features.onramp.redirect.OnrampRedirectComponent
|
||||
import com.tangem.features.onramp.root.entity.OnrampChild
|
||||
import com.tangem.features.onramp.settings.OnrampSettingsComponent
|
||||
|
|
@ -32,9 +30,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
|
|||
@Assisted private val params: OnrampComponent.Params,
|
||||
private val settingsComponentFactory: OnrampSettingsComponent.Factory,
|
||||
private val onrampMainComponentFactory: OnrampMainComponent.Factory,
|
||||
private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory,
|
||||
private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory,
|
||||
private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle,
|
||||
) : OnrampComponent, AppComponentContext by context {
|
||||
|
||||
private val navigation = StackNavigation<OnrampChild>()
|
||||
|
|
@ -71,44 +67,23 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
|
|||
onBack = navigation::pop,
|
||||
),
|
||||
)
|
||||
OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) {
|
||||
onrampMainV2ComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampV2MainComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||
source = params.source,
|
||||
openRedirectPage = { quote ->
|
||||
navigation.push(
|
||||
OnrampChild.RedirectPage(
|
||||
quote = quote,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
onrampMainComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampMainComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||
source = params.source,
|
||||
openRedirectPage = { onrampProviderWithQuoteData ->
|
||||
navigation.push(
|
||||
OnrampChild.RedirectPage(
|
||||
quote = onrampProviderWithQuoteData,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
),
|
||||
)
|
||||
},
|
||||
isLaunchSepa = params.shouldLaunchSepa,
|
||||
),
|
||||
)
|
||||
}
|
||||
OnrampChild.Main -> onrampMainComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampMainComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||
source = params.source,
|
||||
openRedirectPage = { quote ->
|
||||
navigation.push(
|
||||
OnrampChild.RedirectPage(
|
||||
quote = quote,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampRedirectComponent.Params(
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
|
|
@ -19,9 +21,8 @@ import com.tangem.domain.demo.IsDemoCardUseCase
|
|||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.impl.R
|
||||
|
|
@ -43,7 +44,8 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
private val router: AppRouter,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val rampStateManager: RampStateManager,
|
||||
|
|
@ -119,9 +121,13 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync()
|
||||
.getOrElse { AppCurrency.Default }.code
|
||||
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.Sell(status, appCurrencyCode),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = status,
|
||||
appCurrencyCode = appCurrencyCode,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.features.tangempay
|
||||
|
||||
interface TangemPayFeatureToggles {
|
||||
val isTangemPayEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.tangempay
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
internal class DefaultTangemPayFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TangemPayFeatureToggles {
|
||||
override val isTangemPayEnabled
|
||||
get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED")
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.features.tangempay.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
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 TangemPayDetailsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles {
|
||||
return DefaultTangemPayFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.tangempay.deeplink
|
|||
import android.net.Uri
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -11,18 +10,13 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted uri: Uri,
|
||||
appRouter: AppRouter,
|
||||
tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
) : OnboardVisaDeepLinkHandler {
|
||||
|
||||
init {
|
||||
if (tangemPayFeatureToggles.isTangemPayEnabled) {
|
||||
val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink(
|
||||
deeplink = uri.toString(),
|
||||
)
|
||||
appRouter.push(AppRoute.TangemPayOnboarding(mode))
|
||||
} else {
|
||||
appRouter.push(AppRoute.Home())
|
||||
}
|
||||
val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink(
|
||||
deeplink = uri.toString(),
|
||||
)
|
||||
appRouter.push(AppRoute.TangemPayOnboarding(mode))
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.markets.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.promo)
|
||||
|
|
|
|||
|
|
@ -176,7 +176,6 @@ internal object TokenDetailsPreviewData {
|
|||
bottomSheetConfig = null,
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
isYieldSupplyFeatureEnabled = false,
|
||||
)
|
||||
|
||||
val tokenDetailsState_2 = TokenDetailsState(
|
||||
|
|
@ -202,7 +201,6 @@ internal object TokenDetailsPreviewData {
|
|||
bottomSheetConfig = null,
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = true,
|
||||
isYieldSupplyFeatureEnabled = true,
|
||||
)
|
||||
|
||||
val tokenDetailsState_3 = tokenDetailsState_2.copy(stakingBlocksState = stakingBalanceBlock)
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -47,15 +49,14 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.promo.ShouldShowPromoTokenUseCase
|
||||
import com.tangem.domain.promo.models.PromoId
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
|
||||
|
|
@ -91,7 +92,6 @@ import com.tangem.features.tokendetails.TokenDetailsComponent
|
|||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
|
||||
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
|
|
@ -130,7 +130,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase,
|
||||
private val openTrustlineUseCase: OpenTrustlineUseCase,
|
||||
private val dismissIncompleteTransactionUseCase: DismissIncompleteTransactionUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||
private val vibratorHapticManager: VibratorHapticManager,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
|
|
@ -144,7 +145,6 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
|
||||
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
|
||||
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
|
||||
|
|
@ -193,7 +193,6 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
networkHasDerivationUseCase = networkHasDerivationUseCase,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
userWalletId = userWalletId,
|
||||
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
|
||||
)
|
||||
|
||||
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
|
||||
|
|
@ -423,9 +422,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun subscribeOnYieldSupplyBalanceIfActive(status: CryptoCurrencyStatus) {
|
||||
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
|
||||
status.value.yieldSupplyStatus?.isActive == true
|
||||
) {
|
||||
if (status.value.yieldSupplyStatus?.isActive == true) {
|
||||
if (yieldSupplyBalanceJobHolder.isActive && status.value.sources.networkSource != StatusSource.ACTUAL) {
|
||||
return
|
||||
}
|
||||
|
|
@ -709,12 +706,13 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
showErrorIfDemoModeOrElse {
|
||||
val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse
|
||||
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = status,
|
||||
appCurrencyCode = selectedAppCurrencyFlow.value.code,
|
||||
),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = status,
|
||||
appCurrencyCode = selectedAppCurrencyFlow.value.code,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventsHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1235,13 +1233,11 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun needShowYieldSupplyWarning(): Boolean {
|
||||
return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
|
||||
needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
|
||||
return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
private fun isActiveYieldSupply(): Boolean {
|
||||
return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
|
||||
cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true
|
||||
return cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true
|
||||
}
|
||||
|
||||
override fun onYieldSupplyWarningAcknowledged(tokenAction: TokenAction) {
|
||||
|
|
|
|||
|
|
@ -23,5 +23,4 @@ internal data class TokenDetailsState(
|
|||
val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isMarketPriceAvailable: Boolean,
|
||||
val isYieldSupplyFeatureEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsYieldSupplyState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -31,7 +30,6 @@ internal class TokenDetailsLoadedBalanceConverter(
|
|||
private val currentStateProvider: Provider<TokenDetailsState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, TokenDetailsState> {
|
||||
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): TokenDetailsState {
|
||||
|
|
@ -113,10 +111,7 @@ internal class TokenDetailsLoadedBalanceConverter(
|
|||
selectedBalanceType = currentState.selectedBalanceType,
|
||||
isBalanceSelectorEnabled = isBalanceSelectorEnabled,
|
||||
isBalanceFlickering = status.value.isFlickering(),
|
||||
yieldSupplyState =
|
||||
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
|
||||
status.value.yieldSupplyStatus?.isActive == true
|
||||
) {
|
||||
yieldSupplyState = if (status.value.yieldSupplyStatus?.isActive == true) {
|
||||
TokenDetailsYieldSupplyState.Active(clickIntents::onYieldInfoClick)
|
||||
} else {
|
||||
TokenDetailsYieldSupplyState.Empty
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -31,7 +30,6 @@ internal class TokenDetailsSkeletonStateConverter(
|
|||
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val userWalletId: UserWalletId,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
) : Converter<CryptoCurrency, TokenDetailsState> {
|
||||
|
||||
private val iconStateConverter by lazy { TokenDetailsIconStateConverter() }
|
||||
|
|
@ -72,7 +70,6 @@ internal class TokenDetailsSkeletonStateConverter(
|
|||
bottomSheetConfig = null,
|
||||
isBalanceHidden = true,
|
||||
isMarketPriceAvailable = value.id.rawCurrencyId != null,
|
||||
isYieldSupplyFeatureEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import arrow.core.Either
|
|||
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
|
||||
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
|
||||
import com.tangem.common.ui.tokens.getUnavailabilityReasonText
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -33,8 +32,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -48,7 +47,6 @@ internal class TokenDetailsStateFactory(
|
|||
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val userWalletId: UserWalletId,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
) {
|
||||
|
||||
private val skeletonStateConverter by lazy {
|
||||
|
|
@ -57,7 +55,6 @@ internal class TokenDetailsStateFactory(
|
|||
networkHasDerivationUseCase = networkHasDerivationUseCase,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
userWalletId = userWalletId,
|
||||
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +71,6 @@ internal class TokenDetailsStateFactory(
|
|||
currentStateProvider = currentStateProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
clickIntents = tokenDetailsClickIntents,
|
||||
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,10 +148,8 @@ internal fun TokenDetailsScreen(
|
|||
)
|
||||
}
|
||||
|
||||
if (state.isYieldSupplyFeatureEnabled) {
|
||||
item {
|
||||
yieldSupplyComponent.Content(modifier = itemModifier)
|
||||
}
|
||||
item {
|
||||
yieldSupplyComponent.Content(modifier = itemModifier)
|
||||
}
|
||||
|
||||
expressTransactionsItems(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "com.tangem.feature.wallet.impl"
|
||||
packaging {
|
||||
resources {
|
||||
// To build and run composable preview
|
||||
merges += "paymentrequest.proto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -44,6 +50,16 @@ dependencies {
|
|||
exclude(group = "com.google.firebase", module = "protolite-well-known-types")
|
||||
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
|
||||
}
|
||||
implementation(deps.haze) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
implementation(deps.haze.materials) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
@ -84,6 +100,7 @@ dependencies {
|
|||
implementation(projects.domain.nft)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.hotWallet)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.promo)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
|
|
@ -14,6 +15,7 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
|
@ -23,6 +25,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletModel
|
|||
import com.tangem.feature.wallet.navigation.WalletRoute
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent
|
||||
import com.tangem.feature.walletsettings.component.RenameWalletComponent
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
|
|
@ -38,6 +41,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
internal class WalletComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
|
|
@ -50,6 +54,7 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: WalletModel = getOrCreateModel()
|
||||
|
|
@ -148,18 +153,33 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
var headerSize by remember { mutableStateOf(0.dp) }
|
||||
val dialog by dialog.subscribeAsState()
|
||||
|
||||
WalletScreen(
|
||||
state = model.uiState.collectAsStateWithLifecycle().value,
|
||||
bottomSheetContent = {
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
)
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
WalletScreen2(
|
||||
state = model.uiState.collectAsStateWithLifecycle().value,
|
||||
bottomSheetContent = {
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
)
|
||||
} else {
|
||||
WalletScreen(
|
||||
state = model.uiState.collectAsStateWithLifecycle().value,
|
||||
bottomSheetContent = {
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
)
|
||||
}
|
||||
|
||||
when (val dialog = dialog.child?.instance) {
|
||||
is ComposableDialogComponent -> dialog.Dialog()
|
||||
|
|
|
|||
|
|
@ -45,9 +45,7 @@ import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvid
|
|||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
|
|
@ -90,10 +88,8 @@ internal class WalletModel @Inject constructor(
|
|||
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
|
||||
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
|
||||
private val tangemPayOnboardingRepository: OnboardingRepository,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
|
||||
private val getAppThemeModeUseCase: GetAppThemeModeUseCase,
|
||||
|
|
@ -166,10 +162,8 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateYieldSupplyApy() {
|
||||
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
yieldSupplyApyUpdateUseCase()
|
||||
}
|
||||
modelScope.launch(dispatchers.default) {
|
||||
yieldSupplyApyUpdateUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -394,7 +388,6 @@ internal class WalletModel @Inject constructor(
|
|||
* Update state each time a user opens/returns to wallet screen
|
||||
* and every minute while user stays on the main screen
|
||||
*/
|
||||
if (!tangemPayFeatureToggles.isTangemPayEnabled) return
|
||||
|
||||
combine(
|
||||
flow = screenLifecycleProvider.isBackgroundState,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletType
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -110,7 +111,7 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
when (walletState) {
|
||||
is WalletState.MultiCurrency -> {
|
||||
val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id }
|
||||
walletState.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold
|
||||
walletState.type == WalletType.Hot && wallet is UserWallet.Cold
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
|
@ -212,7 +213,7 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
val previousState = state.wallets.firstOrNull { it.walletCardState.id == wallet.walletId }
|
||||
?: return@filter false
|
||||
wallet is UserWallet.Cold && previousState is WalletState.MultiCurrency &&
|
||||
previousState.type == WalletState.MultiCurrency.WalletType.Hot
|
||||
previousState.type == WalletType.Hot
|
||||
}
|
||||
return Action.ReinitializeWallets(selectedWallet, walletsToUpdate)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -64,7 +63,6 @@ internal interface TangemPayIntents {
|
|||
@ModelScoped
|
||||
internal class TangemPayClickIntentsImplementor @Inject constructor(
|
||||
private val stateHolder: WalletStateController,
|
||||
private val featureToggles: TangemPayFeatureToggles,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase,
|
||||
private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase,
|
||||
|
|
@ -77,9 +75,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
|
|||
|
||||
override suspend fun onPullToRefresh() {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
if (!featureToggles.isTangemPayEnabled ||
|
||||
!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)
|
||||
) {
|
||||
if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) {
|
||||
return
|
||||
}
|
||||
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -36,13 +38,12 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.promo.GetStoryContentUseCase
|
||||
import com.tangem.domain.promo.models.StoryContentIds
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
|
||||
|
|
@ -63,7 +64,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -136,14 +136,14 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
private val getStoryContentUseCase: GetStoryContentUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val vibratorHapticManager: VibratorHapticManager,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val appRouter: AppRouter,
|
||||
private val rampStateManager: RampStateManager,
|
||||
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
|
||||
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
|
||||
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
|
||||
|
|
@ -335,12 +335,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
showErrorIfDemoModeOrElse {
|
||||
modelScope.launch(dispatchers.main) {
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code,
|
||||
),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -472,9 +473,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {
|
||||
val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return
|
||||
val tokenListState = selectedWallet.tokensListState
|
||||
|
||||
when (tokenListState) {
|
||||
when (val tokenListState = selectedWallet.tokensListState) {
|
||||
is WalletTokensListState.ContentState.Content -> checkSwapCryptoAvailability(
|
||||
tokenCount = tokenListState.items.count { it is TokensListItemUM.Token },
|
||||
)
|
||||
|
|
@ -663,8 +662,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun needShowYieldSupplyWarning(cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
|
||||
return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
|
||||
needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
|
||||
return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.wallet.presentation.common
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
|
|
@ -186,17 +185,6 @@ internal object WalletPreviewData {
|
|||
)
|
||||
}
|
||||
|
||||
val bottomSheet by lazy {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = WalletBottomSheetConfig.UnlockWallets(
|
||||
onUnlockClick = {},
|
||||
onScanClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val actionsBottomSheet = ActionsBottomSheetConfig(
|
||||
actions = listOf(
|
||||
TokenActionButtonConfig(
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ internal object WalletScreenPreviewData {
|
|||
onItemClick = { },
|
||||
),
|
||||
tangemPayState = TangemPayState.Empty,
|
||||
type = WalletState.MultiCurrency.WalletType.Cold,
|
||||
type = WalletType.Cold,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +218,7 @@ internal object WalletScreenPreviewData {
|
|||
singleWalletLockedState,
|
||||
multiWalletState,
|
||||
),
|
||||
wallets2 = persistentListOf(),
|
||||
onWalletChange = { _, _ -> },
|
||||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.feature.wallet.presentation.preview
|
||||
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.styledStringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM
|
||||
|
||||
internal object WalletBalancePreview {
|
||||
|
||||
val content: WalletBalanceUM.Content = WalletBalanceUM.Content(
|
||||
id = UserWalletId("0"),
|
||||
name = "My Wallet",
|
||||
balance = combinedReference(
|
||||
stringReference("1,234"),
|
||||
styledStringReference(
|
||||
".56",
|
||||
{
|
||||
TangemTheme.typography2.headingRegular28.toSpanStyle()
|
||||
},
|
||||
),
|
||||
stringReference(" $"),
|
||||
),
|
||||
isBalanceFlickering = false,
|
||||
isZeroBalance = false,
|
||||
)
|
||||
|
||||
val loading: WalletBalanceUM.Loading = WalletBalanceUM.Loading(
|
||||
id = UserWalletId("1"),
|
||||
name = "My Wallet",
|
||||
)
|
||||
|
||||
val error: WalletBalanceUM.Error = WalletBalanceUM.Error(
|
||||
id = UserWalletId("2"),
|
||||
name = "My Wallet",
|
||||
)
|
||||
}
|
||||
|
|
@ -8,9 +8,8 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.*
|
|||
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -34,10 +33,29 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun send(displayedWalletUM: WalletUM?, newNotifications: List<WalletNotificationUM>) {
|
||||
if (screenLifecycleProvider.isBackgroundState.value) return
|
||||
if (newNotifications.isEmpty()) return
|
||||
if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return
|
||||
|
||||
val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel
|
||||
val notificationsDiff = newNotifications.filter { it !in totalNotifications }
|
||||
|
||||
val eventsToSend = getEvents2(notificationsDiff)
|
||||
|
||||
eventsToSend.forEach { event ->
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEvents(warnings: List<WalletNotification>): Set<AnalyticsEvent> {
|
||||
return warnings.mapNotNullTo(mutableSetOf(), ::getEvent)
|
||||
}
|
||||
|
||||
private fun getEvents2(notifications: List<WalletNotificationUM>): Set<AnalyticsEvent> {
|
||||
return notifications.mapNotNullTo(mutableSetOf(), ::getEvent2)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getEvent(warning: WalletNotification): AnalyticsEvent? {
|
||||
return when (warning) {
|
||||
|
|
@ -106,4 +124,53 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.UpgradeHotWalletPromo -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getEvent2(notificationUM: WalletNotificationUM): AnalyticsEvent? {
|
||||
return when (notificationUM) {
|
||||
WalletNotificationUM.DevCard -> DevelopmentCard()
|
||||
WalletNotificationUM.FailedCardValidation -> ProductSampleCard()
|
||||
is WalletNotificationUM.MissingBackup -> BackupYourWallet()
|
||||
is WalletNotificationUM.NumberOfSignedHashesIncorrect -> CardSignedTransactions()
|
||||
WalletNotificationUM.TestnetCard -> TestnetCard()
|
||||
WalletNotificationUM.DemoCard -> DemoCard()
|
||||
is WalletNotificationUM.MissingAddresses -> MissingAddresses()
|
||||
is WalletNotificationUM.RateApp -> HowDoYouLikeTangem()
|
||||
is WalletNotificationUM.BackupError -> BackupError()
|
||||
is WalletNotificationUM.NoteMigration -> NotePromo()
|
||||
is WalletNotificationUM.OnePlusOnePromo -> NoticePromotionBanner(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
program = Program.OnePlusOne,
|
||||
)
|
||||
is WalletNotificationUM.YieldPromo -> NoticePromotionBanner(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
program = Program.YieldPromo,
|
||||
)
|
||||
is WalletNotificationUM.FinishWalletActivation -> {
|
||||
val activationState = if (notificationUM.isBackupExists) {
|
||||
NoticeFinishActivation.ActivationState.Unfinished
|
||||
} else {
|
||||
NoticeFinishActivation.ActivationState.NotStarted
|
||||
}
|
||||
val balanceState = when (notificationUM.type) {
|
||||
WalletNotificationType.Warning -> AnalyticsParam.EmptyFull.Full
|
||||
else -> AnalyticsParam.EmptyFull.Empty
|
||||
}
|
||||
NoticeFinishActivation(
|
||||
activationState = activationState,
|
||||
balanceState = balanceState,
|
||||
)
|
||||
}
|
||||
is WalletNotificationUM.SeedPhraseNotification -> NoticeSeedPhraseSupport()
|
||||
is WalletNotificationUM.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond()
|
||||
is WalletNotificationUM.PushNotifications -> PushBanner()
|
||||
is WalletNotificationUM.UnlockWallets,
|
||||
is WalletNotificationUM.NoAccount,
|
||||
is WalletNotificationUM.LowSignatures,
|
||||
WalletNotificationUM.SomeNetworksUnreachable,
|
||||
is WalletNotificationUM.UsedOutdatedData,
|
||||
is WalletNotificationUM.CloreMigration,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,8 @@ import com.tangem.common.routing.AppRoute.WalletBackup
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
|
||||
import com.tangem.core.ui.components.bottomsheets.message.icon
|
||||
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.message.onClick
|
||||
import com.tangem.core.ui.components.bottomsheets.message.primaryButton
|
||||
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
|
||||
import com.tangem.core.ui.components.bottomsheets.message.*
|
||||
import com.tangem.core.ui.ds.message.TangemMessageEffect
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -19,7 +15,9 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
|
|||
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
|
@ -71,6 +69,44 @@ internal class WalletWarningsSingleEventSender @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun send(
|
||||
userWalletId: UserWalletId,
|
||||
displayedWalletUM: WalletUM?,
|
||||
newNotifications: List<WalletNotificationUM>,
|
||||
) {
|
||||
if (screenLifecycleProvider.isBackgroundState.value) return
|
||||
if (newNotifications.isEmpty()) return
|
||||
if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return
|
||||
|
||||
val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel
|
||||
val events = newNotifications.filter { it !in totalNotifications }
|
||||
|
||||
// We must show activation bs only for the first seen wallet when open the app (if need, see conditions below),
|
||||
// so we keep this wallet id and use for future checks, ignore other wallets during the app session.
|
||||
if (isActivationBottomSheetShown.isEmpty()) {
|
||||
isActivationBottomSheetShown[userWalletId] = false
|
||||
}
|
||||
|
||||
events.forEach { event ->
|
||||
when (event) {
|
||||
is WalletNotificationUM.SeedPhraseNotification -> {
|
||||
seedPhraseNotificationUseCase.notified(userWalletId = userWalletId)
|
||||
}
|
||||
is WalletNotificationUM.FinishWalletActivation -> {
|
||||
// We check that map contains the first seen wallet (will return null instead false/true otherwise)
|
||||
// and for this wallet we haven't shown the activation bs yet (check that returns false, not true)
|
||||
if (isActivationBottomSheetShown[userWalletId] == false) {
|
||||
if (event.messageEffect == TangemMessageEffect.Warning && event.isBackupExists.not()) {
|
||||
showFinishActivationBottomSheet(userWalletId)
|
||||
}
|
||||
isActivationBottomSheetShown[userWalletId] = true
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) {
|
||||
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return
|
||||
if (userWallet !is UserWallet.Hot) return
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import com.tangem.common.TangemSiteUrlBuilder
|
||||
import com.tangem.common.ui.notifications.NotificationId
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
|
||||
import com.tangem.domain.promo.models.PromoId
|
||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
|
||||
import com.tangem.utils.extensions.addIf
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Factory for creating a list of notifications that can be shown on the wallet screen.
|
||||
* These notifications are not critical and can be stacked with each other.
|
||||
*/
|
||||
@ModelScoped
|
||||
internal class GetWalletNotificationsCarouselFactory @Inject constructor(
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
) {
|
||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
|
||||
return combine(
|
||||
flow = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo)
|
||||
.distinctUntilChanged(),
|
||||
flow2 = notificationsRepository.getShouldShowNotification(
|
||||
NotificationId.EnablePushesReminderNotification.key,
|
||||
).distinctUntilChanged(),
|
||||
flow3 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne)
|
||||
.distinctUntilChanged(),
|
||||
flow4 = isReadyToShowRateAppUseCase().distinctUntilChanged(),
|
||||
flow5 = getWalletsUseCase().conflate(),
|
||||
) { showYieldPromo, showPushesNotification, showOnePlusOnePromo, showRateAppPromo, wallets ->
|
||||
|
||||
buildList {
|
||||
addNoteMigrationNotification(userWallet, wallets, clickIntents)
|
||||
addRateAppNotification(showRateAppPromo, clickIntents)
|
||||
addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo)
|
||||
addYieldPromoNotification(clickIntents, showYieldPromo)
|
||||
|
||||
addPushNotification(
|
||||
shouldShow = showPushesNotification,
|
||||
isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}.sortedBy { it.type.ordinal }.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addRateAppNotification(
|
||||
isReadyToShowRating: Boolean,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
addIf(isReadyToShowRating) {
|
||||
WalletNotificationUM.RateApp(
|
||||
onLikeClick = clickIntents::onLikeAppClick,
|
||||
onDislikeClick = clickIntents::onDislikeAppClick,
|
||||
onCloseClick = clickIntents::onCloseRateAppWarningClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addYieldPromoNotification(
|
||||
clickIntents: WalletClickIntents,
|
||||
shouldShowPromo: Boolean,
|
||||
) {
|
||||
addIf(shouldShowPromo) {
|
||||
WalletNotificationUM.YieldPromo(
|
||||
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) },
|
||||
onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addOnePlusOnePromoNotification(
|
||||
clickIntents: WalletClickIntents,
|
||||
shouldShowPromo: Boolean,
|
||||
) {
|
||||
addIf(shouldShowPromo) {
|
||||
WalletNotificationUM.OnePlusOnePromo(
|
||||
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) },
|
||||
onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addNoteMigrationNotification(
|
||||
userWallet: UserWallet,
|
||||
userWallets: List<UserWallet>,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
|
||||
|
||||
val isUserHasWalletOrWallet2 = userWallets.filterIsInstance<UserWallet.Cold>().any { wallet ->
|
||||
val typesResolver = wallet.scanResponse.cardTypesResolver
|
||||
typesResolver.isTangemWallet() || typesResolver.isWallet2()
|
||||
}
|
||||
|
||||
addIf(cardTypesResolver != null && cardTypesResolver.isTangemNote() && !isUserHasWalletOrWallet2) {
|
||||
WalletNotificationUM.NoteMigration(
|
||||
onClick = { clickIntents.onNoteMigrationButtonClick(TangemSiteUrlBuilder.NOTE_MIGRATION_URL) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addPushNotification(
|
||||
shouldShow: Boolean,
|
||||
isPushesAllowed: Boolean,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
addIf(shouldShow && !isPushesAllowed) {
|
||||
WalletNotificationUM.PushNotifications(
|
||||
onCloseClick = clickIntents::onDenyPermissions,
|
||||
onEnabledClick = clickIntents::onAllowPermissions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.ui.ds.message.TangemMessageEffect
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.card.CardTypesResolver
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
|
||||
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.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
import com.tangem.feature.wallet.presentation.account.AccountDependencies
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.extensions.addIf
|
||||
import com.tangem.utils.extensions.isPositive
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Factory for creating a list of notifications that can be shown on the wallet screen.
|
||||
* These notifications are critical and should be shown separately from each other.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class GetWalletWarningsFactory @Inject constructor(
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
private val backupValidator: BackupValidator,
|
||||
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
|
||||
private val accountDependencies: AccountDependencies,
|
||||
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
|
||||
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
|
||||
) {
|
||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
|
||||
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
|
||||
|
||||
val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
|
||||
val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params)
|
||||
|
||||
return combine(
|
||||
flow = accountStatusListFlow,
|
||||
flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(),
|
||||
flow3 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(),
|
||||
flow4 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(),
|
||||
) { accountList, isNeedToBackup, seedPhraseIssueStatus, shouldAccessCodeSkipped ->
|
||||
val totalFiatBalance = accountList.totalFiatBalance
|
||||
val flattenCurrencies = accountList.flattenCurrencies()
|
||||
|
||||
buildList {
|
||||
addUsedOutdatedDataNotification(totalFiatBalance)
|
||||
|
||||
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
|
||||
|
||||
addFinishWalletActivationNotification(
|
||||
userWallet = userWallet,
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
clickIntents = clickIntents,
|
||||
shouldAccessCodeSkipped = shouldAccessCodeSkipped,
|
||||
)
|
||||
|
||||
addInformationalNotifications(
|
||||
userWallet = userWallet,
|
||||
cardTypesResolver = cardTypesResolver,
|
||||
flattenCurrencies = flattenCurrencies,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
addWarningNotifications(
|
||||
userWallet = userWallet,
|
||||
cardTypesResolver = cardTypesResolver,
|
||||
flattenCurrencies = flattenCurrencies,
|
||||
isNeedToBackup = isNeedToBackup,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}.sortedBy { it.type.ordinal }.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) {
|
||||
addIf(
|
||||
element = WalletNotificationUM.UsedOutdatedData,
|
||||
condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addCriticalNotifications(
|
||||
userWallet: UserWallet,
|
||||
seedPhraseIssueStatus: SeedPhraseNotificationsStatus,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
if (userWallet !is UserWallet.Cold) {
|
||||
return
|
||||
}
|
||||
|
||||
addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents)
|
||||
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
addIf(
|
||||
element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() },
|
||||
condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError,
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotificationUM.DevCard,
|
||||
condition = !cardTypesResolver.isReleaseFirmwareType(),
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotificationUM.FailedCardValidation,
|
||||
condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(),
|
||||
)
|
||||
|
||||
cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures ->
|
||||
addIf(
|
||||
element = WalletNotificationUM.LowSignatures(count = remainingSignatures),
|
||||
condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addInformationalNotifications(
|
||||
userWallet: UserWallet,
|
||||
cardTypesResolver: CardTypesResolver?,
|
||||
flattenCurrencies: List<CryptoCurrencyStatus>,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotificationUM.DemoCard,
|
||||
condition = cardTypesResolver != null && isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
|
||||
)
|
||||
|
||||
addMissingAddressesNotification(userWallet, flattenCurrencies, clickIntents)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addMissingAddressesNotification(
|
||||
userWallet: UserWallet,
|
||||
flattenCurrencies: List<CryptoCurrencyStatus>,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
val currencies = flattenCurrencies.getMissingAddressCurrencies().ifEmpty { return }
|
||||
|
||||
addIf(
|
||||
element = WalletNotificationUM.MissingAddresses(
|
||||
tangemIcon = walletInterationIcon(userWallet),
|
||||
missingAddressesCount = currencies.count(),
|
||||
onGenerateClick = {
|
||||
clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies)
|
||||
},
|
||||
),
|
||||
condition = currencies.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.getMissingAddressCurrencies(): List<CryptoCurrency> {
|
||||
return this
|
||||
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
|
||||
.map(CryptoCurrencyStatus::currency)
|
||||
}
|
||||
|
||||
private suspend fun MutableList<WalletNotificationUM>.addWarningNotifications(
|
||||
userWallet: UserWallet,
|
||||
cardTypesResolver: CardTypesResolver?,
|
||||
flattenCurrencies: List<CryptoCurrencyStatus>,
|
||||
isNeedToBackup: Boolean,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotificationUM.MissingBackup(
|
||||
onClick = clickIntents::onAddBackupCardClick,
|
||||
),
|
||||
condition = isNeedToBackup,
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotificationUM.TestnetCard,
|
||||
condition = cardTypesResolver?.isTestCard() == true,
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotificationUM.SomeNetworksUnreachable,
|
||||
condition = flattenCurrencies.hasUnreachableNetworks(),
|
||||
)
|
||||
|
||||
addCloreMigrationNotification(flattenCurrencies, clickIntents)
|
||||
|
||||
addNoAccountWarning(cryptoCurrencyStatus = flattenCurrencies.firstOrNull())
|
||||
|
||||
addIf(
|
||||
element = WalletNotificationUM.NumberOfSignedHashesIncorrect(
|
||||
onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick,
|
||||
),
|
||||
condition = hasSignedHashes(userWallet, flattenCurrencies.firstOrNull()),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) {
|
||||
val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount
|
||||
if (noAccountStatus != null) {
|
||||
add(
|
||||
element = WalletNotificationUM.NoAccount(
|
||||
network = cryptoCurrencyStatus.currency.name,
|
||||
amount = noAccountStatus.amountToCreateAccount.toString(),
|
||||
symbol = cryptoCurrencyStatus.currency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addCloreMigrationNotification(
|
||||
flattenCurrencies: List<CryptoCurrencyStatus>,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return
|
||||
|
||||
add(
|
||||
WalletNotificationUM.CloreMigration(
|
||||
onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.findCloreCurrency(): CryptoCurrencyStatus? {
|
||||
return find { currencyStatus ->
|
||||
BlockchainUtils.isClore(currencyStatus.currency.network.rawId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.hasUnreachableNetworks(): Boolean {
|
||||
return any { it.value is CryptoCurrencyStatus.Unreachable }
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addFinishWalletActivationNotification(
|
||||
userWallet: UserWallet,
|
||||
totalFiatBalance: TotalFiatBalance,
|
||||
clickIntents: WalletClickIntents,
|
||||
shouldAccessCodeSkipped: Boolean,
|
||||
) {
|
||||
if (userWallet !is UserWallet.Hot) return
|
||||
|
||||
val isBackupExists = userWallet.backedUp
|
||||
val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword &&
|
||||
!shouldAccessCodeSkipped
|
||||
val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired
|
||||
|
||||
val messageEffect = when (totalFiatBalance) {
|
||||
TotalFiatBalance.Failed,
|
||||
TotalFiatBalance.Loading,
|
||||
-> TangemMessageEffect.None
|
||||
is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) {
|
||||
TangemMessageEffect.Warning
|
||||
} else {
|
||||
TangemMessageEffect.None
|
||||
}
|
||||
}
|
||||
|
||||
addIf(
|
||||
element = WalletNotificationUM.FinishWalletActivation(
|
||||
messageEffect = messageEffect,
|
||||
onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) },
|
||||
isBackupExists = isBackupExists,
|
||||
),
|
||||
condition = shouldShowFinishActivation,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotificationUM>.addSeedNotificationIfNeeded(
|
||||
userWallet: UserWallet.Cold,
|
||||
seedPhraseIssueStatus: SeedPhraseNotificationsStatus,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
val isNotificationAvailable = with(userWallet) {
|
||||
val isDemo = isDemoCardUseCase(cardId = userWallet.cardId)
|
||||
val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported
|
||||
|
||||
!isDemo && isWalletWithSeedPhrase
|
||||
}
|
||||
|
||||
when (seedPhraseIssueStatus) {
|
||||
SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf(
|
||||
element = WalletNotificationUM.SeedPhraseNotification(
|
||||
onDeclineClick = clickIntents::onSeedPhraseNotificationDecline,
|
||||
onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm,
|
||||
),
|
||||
condition = isNotificationAvailable,
|
||||
)
|
||||
SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf(
|
||||
element = WalletNotificationUM.SeedPhraseSecondNotification(
|
||||
onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject,
|
||||
onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept,
|
||||
),
|
||||
condition = isNotificationAvailable,
|
||||
)
|
||||
SeedPhraseNotificationsStatus.NOT_NEEDED -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hasSignedHashes(
|
||||
selectedWallet: UserWallet,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
): Boolean {
|
||||
if (selectedWallet !is UserWallet.Cold || !selectedWallet.isMultiCurrency) return false
|
||||
val network = cryptoCurrencyStatus?.currency?.network ?: return false
|
||||
|
||||
return hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.firstOrNull() == true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_REMAINING_SIGNATURES_COUNT = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList
|
|||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Deprecated("Use MultiWalletContentLoaderV2 instead")
|
||||
|
|
@ -44,7 +43,6 @@ internal class MultiWalletContentLoader(
|
|||
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
|
||||
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
|
||||
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory,
|
||||
) : WalletContentLoader(id = userWallet.walletId) {
|
||||
|
||||
|
|
@ -88,9 +86,7 @@ internal class MultiWalletContentLoader(
|
|||
getStoryContentUseCase = getStoryContentUseCase,
|
||||
).let(::add)
|
||||
|
||||
if (tangemPayFeatureToggles.isTangemPayEnabled) {
|
||||
add(tangemPayMainSubscriberFactory.create(userWallet))
|
||||
}
|
||||
add(tangemPayMainSubscriberFactory.create(userWallet))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList
|
|||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.TangemPayMainSubscriber
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -43,7 +42,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
|
|||
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
|
||||
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
|
||||
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory,
|
||||
) {
|
||||
|
||||
|
|
@ -66,7 +64,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
|
|||
currenciesRepository = currenciesRepository,
|
||||
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
|
||||
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
|
||||
tangemPayFeatureToggles = tangemPayFeatureToggles,
|
||||
tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory,
|
||||
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarni
|
|||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -25,11 +24,10 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor(
|
|||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
private val getStoryContentUseCase: GetStoryContentUseCase,
|
||||
private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory,
|
||||
) : WalletContentLoader(id = userWallet.walletId) {
|
||||
|
||||
override fun create(): List<WalletSubscriber> = listOfNotNull(
|
||||
override fun create(): List<WalletSubscriber> = listOf(
|
||||
accountListSubscriberFactory.create(userWallet = userWallet),
|
||||
walletNFTListSubscriberV2Factory.create(userWallet = userWallet),
|
||||
checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet),
|
||||
|
|
@ -46,12 +44,7 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor(
|
|||
stateHolder = stateController,
|
||||
getStoryContentUseCase = getStoryContentUseCase,
|
||||
),
|
||||
|
||||
if (tangemPayFeatureToggles.isTangemPayEnabled) {
|
||||
tangemPayMainSubscriberFactory.create(userWallet)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
tangemPayMainSubscriberFactory.create(userWallet),
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state
|
||||
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer
|
||||
|
|
@ -25,7 +23,9 @@ import javax.inject.Singleton
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
internal class WalletStateController @Inject constructor() {
|
||||
internal class WalletStateController @Inject constructor(
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
) {
|
||||
|
||||
val uiState: StateFlow<WalletScreenState> get() = mutableUiState
|
||||
|
||||
|
|
@ -53,6 +53,10 @@ internal class WalletStateController @Inject constructor() {
|
|||
return value.wallets.firstOrNull { it.walletCardState.id == userWalletId }
|
||||
}
|
||||
|
||||
fun getWalletUM(userWalletId: UserWalletId): WalletUM? {
|
||||
return value.wallets2.firstOrNull { it.walletsBalanceUM.id == userWalletId }
|
||||
}
|
||||
|
||||
fun getWalletStateIfSelected(walletId: UserWalletId): WalletState? {
|
||||
val selectedWalletId = getSelectedWalletId()
|
||||
|
||||
|
|
@ -61,16 +65,40 @@ internal class WalletStateController @Inject constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getWalletUMIfSelected(walletId: UserWalletId): WalletUM? {
|
||||
val selectedWalletId = getSelectedWalletId()
|
||||
|
||||
return value.wallets2.firstOrNull {
|
||||
it.walletsBalanceUM.id == walletId && it.walletsBalanceUM.id == selectedWalletId
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedWallet(): WalletState {
|
||||
return with(value) { wallets[selectedWalletIndex] }
|
||||
}
|
||||
|
||||
fun getSelectedWalletUM(): WalletUM {
|
||||
return with(value) { wallets2[selectedWalletIndex] }
|
||||
}
|
||||
|
||||
fun getSelectedWalletId(): UserWalletId {
|
||||
return with(value) { wallets[selectedWalletIndex].walletCardState.id }
|
||||
return with(value) {
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
wallets2[selectedWalletIndex].walletsBalanceUM.id
|
||||
} else {
|
||||
wallets[selectedWalletIndex].walletCardState.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getWalletIndexByWalletId(userWalletId: UserWalletId): Int? {
|
||||
return with(value) { wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId } }
|
||||
return with(value) {
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
wallets2.indexOfFirstOrNull { it.walletsBalanceUM.id == userWalletId }
|
||||
} else {
|
||||
wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showBottomSheet(
|
||||
|
|
@ -105,6 +133,7 @@ internal class WalletStateController @Inject constructor() {
|
|||
topBarConfig = WalletTopBarConfig(onDetailsClick = {}),
|
||||
selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX,
|
||||
wallets = persistentListOf(),
|
||||
wallets2 = persistentListOf(),
|
||||
onWalletChange = { _, _ -> },
|
||||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Represents the state of the wallet balance in the UI.
|
||||
*
|
||||
* The sealed interface has three implementations:
|
||||
* - [Content]: Represents the state when the wallet balance is successfully loaded.
|
||||
* - [Error]: Represents the state when there was an error loading the wallet balance.
|
||||
* - [Loading]: Represents the state when the wallet balance is currently being loaded.
|
||||
*
|
||||
* @property id The unique identifier of the wallet.
|
||||
* @property name The name of the wallet.
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed interface WalletBalanceUM {
|
||||
|
||||
/** Wallet Id */
|
||||
val id: UserWalletId
|
||||
|
||||
/** Wallet Name */
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* Wallet card content state
|
||||
*
|
||||
* @property id wallet id
|
||||
* @property name wallet name
|
||||
* @property balance wallet balance
|
||||
*/
|
||||
data class Content(
|
||||
override val id: UserWalletId,
|
||||
override val name: String,
|
||||
val balance: TextReference,
|
||||
val isBalanceFlickering: Boolean,
|
||||
val isZeroBalance: Boolean?,
|
||||
) : WalletBalanceUM
|
||||
|
||||
/**
|
||||
* Wallet card error state
|
||||
*
|
||||
* @property id wallet id
|
||||
* @property name wallet name
|
||||
*/
|
||||
data class Error(
|
||||
override val id: UserWalletId,
|
||||
override val name: String,
|
||||
) : WalletBalanceUM
|
||||
|
||||
/**
|
||||
* Wallet card loading state
|
||||
*
|
||||
* @property id wallet id
|
||||
* @property name wallet name
|
||||
*/
|
||||
data class Loading(
|
||||
override val id: UserWalletId,
|
||||
override val name: String,
|
||||
) : WalletBalanceUM
|
||||
|
||||
fun copySealed(name: String): WalletBalanceUM {
|
||||
return when (this) {
|
||||
is Content -> copy(name = name)
|
||||
is Error -> copy(name = name)
|
||||
is Loading -> copy(name = name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
||||
/**
|
||||
* Wallet bottom sheet config
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class WalletBottomSheetConfig(
|
||||
open val title: TextReference,
|
||||
open val subtitle: TextReference,
|
||||
@DrawableRes open val iconResId: Int,
|
||||
val primaryButtonConfig: ButtonConfig,
|
||||
val secondaryButtonConfig: ButtonConfig,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class ButtonConfig(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
@DrawableRes val iconResId: Int? = null,
|
||||
)
|
||||
|
||||
data class UnlockWallets(val onUnlockClick: () -> Unit, val onScanClick: () -> Unit) : WalletBottomSheetConfig(
|
||||
title = resourceReference(id = R.string.common_access_denied),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.unlock_wallet_description_full,
|
||||
formatArgs = wrappedList(
|
||||
resourceReference(R.string.common_biometrics),
|
||||
),
|
||||
),
|
||||
iconResId = R.drawable.ic_locked_24,
|
||||
primaryButtonConfig = ButtonConfig(
|
||||
text = resourceReference(
|
||||
id = R.string.user_wallet_list_unlock_all_with,
|
||||
formatArgs = wrappedList(resourceReference(R.string.common_biometrics)),
|
||||
),
|
||||
onClick = onUnlockClick,
|
||||
),
|
||||
secondaryButtonConfig = ButtonConfig(
|
||||
text = resourceReference(id = R.string.welcome_unlock_card),
|
||||
onClick = onScanClick,
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
),
|
||||
)
|
||||
}
|
||||
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