Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-27 11:42:24 +01:00
commit c3522320ae
660 changed files with 10672 additions and 6904 deletions

View file

@ -56,6 +56,7 @@ dependencies {
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.settings)
implementation(projects.domain.tokens)

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.addcustomtoken.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@ -24,6 +25,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.common.state.AlertState
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
@ -287,17 +289,10 @@ private fun TokenTextFieldTitle(state: TextFieldState?, title: String) {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ChooseDerivationScreen_Light() {
TangemTheme(isDark = false) {
AddCustomTokenScreen(state = AddCustomTokenPreviewData.state)
}
}
@Preview
@Composable
private fun Preview_ChooseDerivationScreen_Dark() {
TangemTheme(isDark = true) {
private fun Preview_ChooseDerivationScreen() {
TangemThemePreview {
AddCustomTokenScreen(state = AddCustomTokenPreviewData.state)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.addcustomtoken.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -13,6 +14,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock
@ -98,17 +100,10 @@ private fun DerivationsList(state: ChooseDerivationState) {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ChooseDerivationScreen_Light() {
TangemTheme(isDark = false) {
ChooseDerivationScreen(state = ChooseDerivationPreviewData.state)
}
}
@Preview
@Composable
private fun Preview_ChooseDerivationScreen_Dark() {
TangemTheme(isDark = true) {
private fun Preview_ChooseDerivationScreen() {
TangemThemePreview {
ChooseDerivationScreen(state = ChooseDerivationPreviewData.state)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.addcustomtoken.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -13,6 +14,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.common.ui.components.NetworkItem
@ -78,17 +80,10 @@ internal fun ChooseNetworkCustomScreen(state: ChooseNetworkState, modifier: Modi
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ChooseNetworkScreen_Light() {
TangemTheme(isDark = false) {
ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state)
}
}
@Preview
@Composable
private fun Preview_ChooseNetworkScreen_Dark() {
TangemTheme(isDark = true) {
private fun Preview_ChooseNetworkScreen() {
TangemThemePreview {
ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.common.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -23,6 +24,7 @@ import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
import com.tangem.managetokens.presentation.common.state.WalletState
@ -137,19 +139,10 @@ private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifi
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ChooseWalletScreen_Light() {
TangemTheme(isDark = false) {
ChooseWalletScreen(
state = ChooseWalletStatePreviewData.state,
)
}
}
@Preview
@Composable
private fun Preview_ChooseWalletScreen_Dark() {
TangemTheme(isDark = false) {
private fun Preview_ChooseWalletScreen() {
TangemThemePreview {
ChooseWalletScreen(
state = ChooseWalletStatePreviewData.state,
)

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.common.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.common.state.NetworkItemState
@ -122,17 +124,10 @@ internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) {
TangemTheme(isDark = false) {
NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded)
}
}
@Preview
@Composable
private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) {
TangemTheme(isDark = true) {
private fun Preview_NetworkItem(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) {
TangemThemePreview {
NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.common.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -12,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
@Composable
@ -54,17 +56,10 @@ fun SimpleSelectionBlock(
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_SimpleSelectionBlock_Light() {
TangemTheme(isDark = false) {
SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { })
}
}
@Preview
@Composable
private fun Preview_SimpleSelectionBlock_Dark() {
TangemTheme(isDark = true) {
private fun Preview_SimpleSelectionBlock() {
TangemThemePreview {
SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { })
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.managetokens.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -17,6 +18,7 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.WarningCardTitleOnly
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
@ -187,20 +189,10 @@ private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ChooseNetworkScreen_Light() {
TangemTheme(isDark = false) {
ChooseNetworkScreen(
state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded,
walletState = ChooseWalletStatePreviewData.state,
)
}
}
@Preview
@Composable
private fun Preview_ChooseNetworkScreen_Dark() {
TangemTheme(isDark = true) {
private fun Preview_ChooseNetworkScreen() {
TangemThemePreview {
ChooseNetworkScreen(
state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded,
walletState = ChooseWalletStatePreviewData.state,

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -22,6 +23,7 @@ import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH18
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenBottomSheet
import com.tangem.managetokens.presentation.common.state.AlertState
@ -177,23 +179,13 @@ private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state:
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ManageTokensScreen_LightTheme(
private fun Preview_ManageTokensScreen(
@PreviewParameter(ManageTokensConfigProvider::class)
state: ManageTokensState,
) {
TangemTheme(isDark = false) {
ManageTokensScreen(state) {}
}
}
@Preview
@Composable
private fun Preview_ManageTokensScreen_DarkTheme(
@PreviewParameter(ManageTokensConfigProvider::class)
state: ManageTokensState,
) {
TangemTheme(isDark = true) {
TangemThemePreview {
ManageTokensScreen(state) {}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.managetokens.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -14,6 +15,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.managetokens.impl.R
@Composable
@ -49,17 +51,10 @@ internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AddCustomTokenButton_Preview_Light() {
TangemTheme(isDark = false) {
AddCustomTokenButton(onButtonClick = { })
}
}
@Preview
@Composable
private fun AddCustomTokenButton_Preview_Dark() {
TangemTheme(isDark = true) {
private fun AddCustomTokenButton_Preview() {
TangemThemePreview {
AddCustomTokenButton(onButtonClick = { })
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.managetokens.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@ -18,6 +19,7 @@ import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.managetokens.state.previewdata.DerivationNotificationStatePreviewData
@ -127,17 +129,10 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ManageTokensScreen_LightTheme() {
TangemTheme(isDark = false) {
DerivationNotification(DerivationNotificationStatePreviewData.state.config)
}
}
@Preview
@Composable
private fun Preview_ManageTokensScreen_DarkTheme() {
TangemTheme(isDark = true) {
private fun Preview_ManageTokensScreen() {
TangemThemePreview {
DerivationNotification(DerivationNotificationStatePreviewData.state.config)
}
}

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -105,7 +106,7 @@ private fun getValuePercentageForRange(value: Float, max: Float, min: Float): Fl
@Preview(widthDp = 150, heightDp = 150, showBackground = true)
@Composable
private fun Chart_Positive_Preview() {
TangemTheme(isDark = true) {
TangemThemePreview(isDark = true) {
PriceChangesChart(
persistentListOf(1f, 2f, 4f, 1f, 5f),
)
@ -115,7 +116,7 @@ private fun Chart_Positive_Preview() {
@Preview(widthDp = 150, heightDp = 150, showBackground = true)
@Composable
private fun Chart_Negative_Preview() {
TangemTheme(isDark = true) {
TangemThemePreview(isDark = true) {
PriceChangesChart(
persistentListOf(10f, 2f, 4f, 1f, 5f),
)
@ -125,7 +126,7 @@ private fun Chart_Negative_Preview() {
@Preview(widthDp = 150, heightDp = 150, showBackground = true)
@Composable
private fun Chart_Neutral_Preview() {
TangemTheme(isDark = true) {
TangemThemePreview(isDark = true) {
PriceChangesChart(
persistentListOf(5f, 2f, 4f, 1f, 5f),
)

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.managetokens.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
@ -20,6 +21,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.managetokens.state.SearchBarState
@ -107,20 +109,13 @@ private fun searchbarTextFieldColors(): TextFieldColors {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokensSearchBar_Light(
private fun Preview_TokensSearchBar(
@PreviewParameter(SearchBarkConfigProvider::class)
state: SearchBarState,
) {
TangemTheme(isDark = false) {
TokensSearchBar(state)
}
}
@Preview
@Composable
private fun Preview_TokensSearchBar_Dark(@PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarState) {
TangemTheme(isDark = true) {
TangemThemePreview {
TokensSearchBar(state)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.managetokens.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
@ -18,6 +19,7 @@ import com.tangem.core.ui.components.buttons.PrimarySmallButton
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType
@ -66,17 +68,10 @@ internal fun TokenButton(type: TokenButtonType, onClick: () -> Unit, modifier: M
}
@Preview(backgroundColor = 0xffffff, showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TokenButton_Light_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) {
TangemTheme(isDark = false) {
TokenButton(type = type, {})
}
}
@Preview(showBackground = true)
@Composable
private fun TokenButton_Dark_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) {
TangemTheme(isDark = true) {
private fun TokenButton_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) {
TangemThemePreview {
TokenButton(type = type, {})
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.managetokens.presentation.managetokens.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Surface
@ -14,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.managetokens.presentation.managetokens.state.QuotesState
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
@ -182,17 +184,10 @@ private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () -
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
TangemTheme(isDark = false) {
TokenRowItem(state)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
TangemTheme(isDark = true) {
private fun Preview_Tokens(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
TangemThemePreview {
TokenRowItem(state)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -21,6 +22,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
@ -204,11 +206,12 @@ private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: D
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SuggestionsBlockPreview_Light(
private fun SuggestionsBlockPreview(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemTheme(isDark = false) {
TangemThemePreview {
SuggestionsBlock(
suggestionsList = suggestions,
onClick = {},
@ -217,24 +220,12 @@ private fun SuggestionsBlockPreview_Light(
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SuggestionsBlockPreview_Dark(
private fun ImportSeedPhraseScreenPreview(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemTheme(isDark = true) {
SuggestionsBlock(
suggestionsList = suggestions,
onClick = {},
)
}
}
@Preview
@Composable
private fun ImportSeedPhraseScreenPreview_Light(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemTheme(isDark = false) {
TangemThemePreview {
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ImportSeedPhraseScreen(
ImportSeedPhraseState(

View file

@ -18,6 +18,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
@ -83,7 +84,7 @@ fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetConte
@Preview
@Composable
private fun PassphraseInfoBottomSheetContentPreview() {
TangemTheme {
TangemThemePreview {
PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { })
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -16,6 +17,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
@ -164,9 +166,10 @@ private inline fun <T> VerticalGrid(
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun YourSeedPhraseScreenPreview_Light() {
TangemTheme(isDark = false) {
TangemThemePreview {
YourSeedPhraseScreen(
state = YourSeedPhraseState(
segmentSeedState = SegmentSeedState(

View file

@ -20,9 +20,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.mlkit.vision.common.InputImage
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import com.tangem.feature.qrscanning.presentation.QrScanningContent
@ -39,7 +39,7 @@ import kotlin.properties.Delegates
internal class QrScanningFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var router: QrScanningRouter

View file

@ -21,6 +21,7 @@ dependencies {
/** Domain modules */
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)

View file

@ -2,12 +2,13 @@ package com.tangem.feature.referral.data
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.StartReferralBody
import com.tangem.datasource.demo.DemoModeDatasource
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
@ -15,7 +16,6 @@ import com.tangem.feature.referral.converters.ReferralConverter
import com.tangem.feature.referral.domain.ReferralRepository
import com.tangem.feature.referral.domain.models.ReferralData
import com.tangem.feature.referral.domain.models.TokenData
import com.tangem.lib.auth.AuthProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject

View file

@ -1,12 +1,12 @@
package com.tangem.feature.referral.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.demo.DemoModeDatasource
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.feature.referral.converters.ReferralConverter
import com.tangem.feature.referral.data.ReferralRepositoryImpl
import com.tangem.feature.referral.domain.ReferralRepository
import com.tangem.lib.auth.AuthProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides

View file

@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.feature.referral.router.ReferralRouter
import com.tangem.feature.referral.ui.ReferralScreen
import com.tangem.feature.referral.viewmodels.ReferralViewModel
@ -20,7 +20,7 @@ import javax.inject.Inject
class ReferralFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
override lateinit var uiDependencies: UiDependencies
private val viewModel by viewModels<ReferralViewModel>()

View file

@ -1,5 +1,6 @@
package com.tangem.feature.referral.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -16,6 +17,7 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.referral.presentation.R
@ -58,17 +60,10 @@ private fun AgreementHtmlView(url: String) {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AgreementBottomSheet_InLightTheme() {
TangemTheme(isDark = false) {
AgreementBottomSheetContent(url = "https://tangem.com/en/")
}
}
@Preview
@Composable
private fun Preview_AgreementBottomSheet_InDarkTheme() {
TangemTheme(isDark = true) {
private fun Preview_AgreementBottomSheet() {
TangemThemePreview {
AgreementBottomSheetContent(url = "https://tangem.com/en/")
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.referral.ui
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -15,6 +16,7 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.referral.presentation.R
@ -53,19 +55,10 @@ private fun annotatedAgreementString(firstPart: String): AnnotatedString {
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AgreementText_InLightTheme() {
TangemTheme(isDark = false) {
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = {})
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_AgreementText_InDarkTheme() {
TangemTheme(isDark = true) {
private fun Preview_AgreementText() {
TangemThemePreview {
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = {})
}

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Suppress("LongParameterList")
@Composable
@ -56,8 +57,8 @@ internal fun AwardText(
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_AwardItem_Light() {
TangemTheme {
private fun Preview_AwardItem() {
TangemThemePreview {
AwardText(
startText = "startText",
startTextColor = TangemTheme.colors.text.tertiary,

View file

@ -1,14 +1,17 @@
package com.tangem.feature.referral.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryEndIconButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.presentation.R
@Composable
@ -18,29 +21,23 @@ internal fun NonParticipateBottomBlock(onAgreementClick: () -> Unit, onParticipa
firstPartResId = R.string.referral_tos_not_enroled_prefix,
onClick = onAgreementClick,
)
PrimaryEndIconButton(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
PrimaryButtonIconEnd(
text = stringResource(id = R.string.referral_button_participate),
iconResId = R.drawable.ic_tangem_24,
onClick = onParticipateClick,
modifier = Modifier
.fillMaxWidth()
.padding(all = TangemTheme.dimens.spacing16),
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NonParticipateBottomBlock_InLightTheme() {
TangemTheme(isDark = false) {
Column(Modifier.background(TangemTheme.colors.background.primary)) {
NonParticipateBottomBlock(onAgreementClick = {}, onParticipateClick = {})
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_NonParticipateBottomBlock_InDarkTheme() {
TangemTheme(isDark = true) {
private fun Preview_NonParticipateBottomBlock() {
TangemThemePreview {
Column(Modifier.background(TangemTheme.colors.background.primary)) {
NonParticipateBottomBlock(onAgreementClick = {}, onParticipateClick = {})
}

View file

@ -2,19 +2,14 @@ package com.tangem.feature.referral.ui
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import androidx.compose.animation.*
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.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
@ -30,11 +25,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.core.content.ContextCompat.startActivity
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards
import com.tangem.feature.referral.presentation.R
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
@Composable
@ -43,8 +40,8 @@ internal fun ParticipateBottomBlock(
code: String,
shareLink: String,
expectedAwards: ExpectedAwards?,
snackbarHostState: SnackbarHostState,
onAgreementClick: () -> Unit,
onShowCopySnackbar: () -> Unit,
onCopyClick: () -> Unit,
onShareClick: () -> Unit,
) {
@ -61,7 +58,7 @@ internal fun ParticipateBottomBlock(
AdditionalButtons(
code = code,
shareLink = shareLink,
onShowCopySnackbar = onShowCopySnackbar,
snackbarHostState = snackbarHostState,
onCopyClick = onCopyClick,
onShareClick = onShareClick,
)
@ -110,9 +107,9 @@ private fun Awards(expectedAwards: ExpectedAwards) {
val elementsCountToShowInLessMode = 3
val isExpanded = remember { mutableStateOf(false) }
Divider(
color = TangemTheme.colors.stroke.primary,
HorizontalDivider(
thickness = TangemTheme.dimens.size0_5,
color = TangemTheme.colors.stroke.primary,
)
AwardText(
startText = if (expectedAwards.expectedAwards.isNotEmpty()) {
@ -270,32 +267,40 @@ private fun PersonalCodeCard(code: String) {
private fun AdditionalButtons(
code: String,
shareLink: String,
onShowCopySnackbar: () -> Unit,
snackbarHostState: SnackbarHostState,
onCopyClick: () -> Unit,
onShareClick: () -> Unit,
) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val resources = LocalContext.current.resources
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
PrimaryStartIconButton(
modifier = Modifier.weight(1f),
PrimaryButtonIconStart(
text = stringResource(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
onCopyClick.invoke()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(code))
onShowCopySnackbar()
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getString(R.string.referral_promo_code_copied),
duration = SnackbarDuration.Short,
)
}
},
modifier = Modifier.weight(1f),
)
val context = LocalContext.current
PrimaryStartIconButton(
modifier = Modifier.weight(1f),
PrimaryButtonIconStart(
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
@ -303,6 +308,7 @@ private fun AdditionalButtons(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(context.getString(R.string.referral_share_link, shareLink))
},
modifier = Modifier.weight(1f),
)
}
}
@ -318,11 +324,12 @@ private fun Context.shareText(text: String) {
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ParticipateBottomBlockPreview_Light(
private fun ParticipateBottomBlockPreview(
@PreviewParameter(ParticipateBottomBlockDataProvider::class) data: ParticipateBottomBlockData,
) {
TangemTheme(isDark = false) {
TangemThemePreview {
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
ParticipateBottomBlock(
purchasedWalletCount = data.purchasedWalletCount,
@ -330,7 +337,7 @@ private fun ParticipateBottomBlockPreview_Light(
shareLink = data.shareLink,
expectedAwards = data.expectedAwards,
onAgreementClick = data.onAgreementClick,
onShowCopySnackbar = data.onShowCopySnackbar,
snackbarHostState = SnackbarHostState(),
onCopyClick = data.onCopyClick,
onShareClick = data.onShareClick,
)
@ -339,42 +346,10 @@ private fun ParticipateBottomBlockPreview_Light(
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ParticipateBottomBlockPreview_Dark(
@PreviewParameter(ParticipateBottomBlockDataProvider::class) state: ParticipateBottomBlockData,
) {
TangemTheme(isDark = true) {
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
ParticipateBottomBlock(
purchasedWalletCount = state.purchasedWalletCount,
code = state.code,
shareLink = state.shareLink,
expectedAwards = state.expectedAwards,
onAgreementClick = state.onAgreementClick,
onShowCopySnackbar = state.onShowCopySnackbar,
onCopyClick = state.onCopyClick,
onShareClick = state.onShareClick,
)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun LessMoreButton_Light() {
TangemTheme(isDark = false) {
LessMoreButton(
isExpanded = remember {
mutableStateOf(false)
},
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun LessMoreButton_Dark() {
TangemTheme(isDark = true) {
private fun LessMoreButtonPreview() {
TangemThemePreview {
LessMoreButton(
isExpanded = remember {
mutableStateOf(false)
@ -420,7 +395,6 @@ private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterPro
purchasedWalletCount = 0,
expectedAwards = null,
),
),
)

View file

@ -1,5 +1,7 @@
package com.tangem.feature.referral.ui
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -10,9 +12,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@ -21,13 +21,15 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards
import com.tangem.feature.referral.models.DemoModeException
@ -48,6 +50,8 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier
var isBottomSheetVisible by remember { mutableStateOf(value = false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val snackbarHostState = remember(::SnackbarHostState)
Scaffold(
modifier = modifier,
topBar = {
@ -56,10 +60,26 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier
onBackClick = stateHolder.headerState.onBackClicked,
)
},
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = TangemTheme.dimens.spacing108),
) {
// TODO: use StateEvent
if (stateHolder.errorSnackbar != null) {
TangemSnackbar(data = it, actionOnNewLine = true)
} else {
CopiedTextSnackbar(it)
}
}
},
containerColor = TangemTheme.colors.background.secondary,
) {
ReferralContent(
stateHolder = stateHolder,
snackbarHostState = snackbarHostState,
onAgreementClick = {
stateHolder.analytics.onAgreementClicked.invoke()
isBottomSheetVisible = true
@ -68,6 +88,26 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier
)
}
val errorSnackbar = stateHolder.errorSnackbar
val coroutineScope = rememberCoroutineScope()
val resources = LocalContext.current.resources
SideEffect {
if (errorSnackbar != null) {
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable),
actionLabel = resources.getString(R.string.warning_button_ok),
duration = SnackbarDuration.Indefinite,
)
if (result == SnackbarResult.ActionPerformed) {
errorSnackbar.onOkClicked()
}
}
}
}
ReferralBottomSheet(
sheetState = sheetState,
isVisible = isBottomSheetVisible,
@ -79,11 +119,10 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier
@Composable
private fun ReferralContent(
stateHolder: ReferralStateHolder,
snackbarHostState: SnackbarHostState,
onAgreementClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val isCopyButtonPressed = remember { mutableStateOf(value = false) }
Box(modifier = modifier) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@ -93,14 +132,11 @@ private fun ReferralContent(
item {
ReferralInfo(
stateHolder = stateHolder,
snackbarHostState = snackbarHostState,
onAgreementClick = onAgreementClick,
onShowCopySnackbar = { isCopyButtonPressed.value = true },
)
}
}
ErrorSnackbarHost(errorSnackbar = stateHolder.errorSnackbar)
CopySnackbarHost(isCopyButtonPressed = isCopyButtonPressed)
}
}
@ -131,8 +167,8 @@ private fun Header() {
@Composable
private fun ReferralInfo(
stateHolder: ReferralStateHolder,
snackbarHostState: SnackbarHostState,
onAgreementClick: () -> Unit,
onShowCopySnackbar: () -> Unit,
) {
when (val state = stateHolder.referralInfoState) {
is ReferralInfoState.ParticipantContent -> {
@ -142,8 +178,8 @@ private fun ReferralInfo(
code = state.code,
shareLink = state.shareLink,
expectedAwards = state.expectedAwards,
snackbarHostState = snackbarHostState,
onAgreementClick = onAgreementClick,
onShowCopySnackbar = onShowCopySnackbar,
onCopyClick = stateHolder.analytics.onCopyClicked,
onShareClick = stateHolder.analytics.onShareClicked,
)
@ -333,122 +369,19 @@ private fun ShimmerInfo() {
}
}
// TODO() Replace component with component from ds
@Composable
private fun BoxScope.ErrorSnackbarHost(errorSnackbar: ErrorSnackbar?) {
if (errorSnackbar != null) {
val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) }
val coroutineScope = rememberCoroutineScope()
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter),
snackbar = {
Snackbar(
snackbarData = it,
modifier = Modifier.fillMaxWidth(),
actionOnNewLine = true,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius8),
containerColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.text.primary2,
actionColor = TangemTheme.colors.text.primary2,
)
},
)
val actionLabel = stringResource(id = R.string.warning_button_ok)
val message = getMessageForErrorSnackbar(errorSnackbar)
SideEffect {
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = message,
actionLabel = actionLabel,
duration = SnackbarDuration.Indefinite,
)
if (result == SnackbarResult.ActionPerformed) {
errorSnackbar.onOkClicked()
}
}
}
}
}
// TODO() Replace component with component from ds
@Composable
private fun BoxScope.CopySnackbarHost(isCopyButtonPressed: MutableState<Boolean>) {
if (isCopyButtonPressed.value) {
val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) }
val coroutineScope = rememberCoroutineScope()
var snackbarSize by remember { mutableIntStateOf(value = 0) }
val width = LocalConfiguration.current.screenWidthDp.dp
val snackbarWidth = with(LocalDensity.current) { snackbarSize.toDp() }
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(start = (width - snackbarWidth).div(2), bottom = TangemTheme.dimens.spacing12)
.fillMaxWidth(),
snackbar = {
Box(
modifier = Modifier
.onSizeChanged { snackbarSize = it.width }
.background(
color = TangemTheme.colors.icon.primary1,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius8),
)
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing14,
),
) {
Text(
text = it.visuals.message,
color = TangemTheme.colors.text.primary2,
style = TangemTheme.typography.body2,
)
}
},
)
val message = stringResource(id = R.string.referral_promo_code_copied)
SideEffect {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = message,
duration = SnackbarDuration.Short,
)
isCopyButtonPressed.value = false
}
}
}
}
@Composable
private fun getMessageForErrorSnackbar(errorSnackbar: ErrorSnackbar): String {
return when (errorSnackbar.throwable) {
is DemoModeException -> {
stringResource(id = R.string.alert_demo_feature_disabled)
}
else -> {
if (errorSnackbar.throwable.cause != null) {
String.format(
format = stringResource(id = R.string.referral_error_failed_to_load_info_with_reason),
errorSnackbar.throwable.cause,
)
} else {
stringResource(id = R.string.referral_error_failed_to_load_info)
}
}
private fun Resources.getMessageForErrorSnackbar(throwable: Throwable): String {
return when {
throwable is DemoModeException -> getString(R.string.alert_demo_feature_disabled)
throwable.cause != null -> getString(R.string.referral_error_failed_to_load_info_with_reason, throwable.cause)
else -> getString(R.string.referral_error_failed_to_load_info)
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ReferralScreen_Participant_InLightTheme() {
TangemTheme(isDark = false) {
private fun Preview_ReferralScreen_Participant() {
TangemThemePreview {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),
@ -475,38 +408,10 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() {
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ReferralScreen_Participant_InDarkTheme() {
TangemTheme(isDark = true) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.ParticipantContent(
award = "10 USDT",
networkName = "Tron",
address = "ma80...zk8q2",
discount = "10%",
purchasedWalletCount = 3,
code = "x4JdK",
shareLink = "",
url = "",
expectedAwards = null,
),
errorSnackbar = ErrorSnackbar(DemoModeException()) {},
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
),
),
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() {
TangemTheme(isDark = false) {
private fun Preview_ReferralScreen_Participant_With_Referrals() {
TangemThemePreview {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),
@ -549,9 +454,10 @@ private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() {
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ReferralScreen_NonParticipant_InLightTheme() {
TangemTheme(isDark = false) {
private fun Preview_ReferralScreen_NonParticipant() {
TangemThemePreview {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),
@ -574,53 +480,10 @@ private fun Preview_ReferralScreen_NonParticipant_InLightTheme() {
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ReferralScreen_NonParticipant_InDarkTheme() {
TangemTheme(isDark = true) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.NonParticipantContent(
award = "10 USDT",
networkName = "Tron",
discount = "10%",
url = "",
onParticipateClicked = {},
),
errorSnackbar = null,
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
),
),
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_ReferralScreen_Loading_InLightTheme() {
TangemTheme(isDark = false) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.Loading,
errorSnackbar = null,
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
),
),
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_ReferralScreen_Loading_InDarkTheme() {
TangemTheme(isDark = true) {
private fun Preview_ReferralScreen_Loading() {
TangemThemePreview {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),

View file

@ -59,6 +59,7 @@ dependencies {
/** Domain modules */
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)

View file

@ -6,10 +6,10 @@ import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.state.StateRouter
@ -26,7 +26,7 @@ import javax.inject.Inject
internal class SendFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var router: SendRouter

View file

@ -162,4 +162,28 @@ internal sealed class SendNotification(val config: NotificationConfig) {
),
)
}
sealed interface Cardano {
data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning(
title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title),
subtitle = resourceReference(
id = R.string.cardano_coin_will_be_send_with_token_description,
formatArgs = wrappedList(minAdaValue, tokenName),
),
)
data object InsufficientBalanceToTransferCoin : Error(
title = resourceReference(id = R.string.cardano_max_amount_has_token_title),
subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description),
)
data class InsufficientBalanceToTransferToken(val tokenName: String) : Error(
title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title),
subtitle = resourceReference(
id = R.string.cardano_insufficient_balance_to_send_token_description,
formatArgs = wrappedList(tokenName),
),
)
}
}

View file

@ -1,18 +1,11 @@
package com.tangem.features.send.impl.presentation.state
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
@ -20,18 +13,14 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientWalletListConverter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LongParameterList", "LargeClass")
@Suppress("LongParameterList")
internal class SendStateFactory(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
@ -41,7 +30,6 @@ internal class SendStateFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val isTapHelpPreviewEnabledProvider: Provider<Boolean>,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
@ -79,14 +67,6 @@ internal class SendStateFactory(
isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider,
)
}
private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientWalletListConverter()
}
private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientHistoryListConverter(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val sendSyncEditConverter by lazy(LazyThreadSafetyMode.NONE) {
SendSyncEditConverter(currentStateProvider = currentStateProvider)
}
@ -132,156 +112,6 @@ internal class SendStateFactory(
}
//endregion
//region recipient
fun onLoadedWalletsList(wallets: List<AvailableWallet?>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
wallets = recipientWalletListStateConverter.convert(wallets),
),
)
}
fun onLoadedHistoryList(txHistory: List<TxHistoryItem>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
recent = recipientHistoryListStateConverter.convert(txHistory),
),
)
}
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
addressTextField = recipientState.addressTextField.copy(value = value),
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
),
)
}
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = recipientState.memoTextField?.value.orEmpty(),
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
Timber.e("Failed to validateWalletMemoUseCase: $it")
false
}
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isValidating = false,
addressTextField = recipientState.addressTextField.copy(
error = when {
!isValidAddress -> resourceReference(R.string.send_recipient_address_error)
isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet)
else -> null
},
isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet,
),
),
)
}
fun getOnRecipientAddressValidationStarted(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(isValidating = true),
)
}
fun getOnRecipientMemoValueChange(value: String): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(value = value),
),
)
}
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = value,
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
Timber.e("Failed to validateWalletMemoUseCase: $it")
false
}
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isValidating = false,
memoTextField = recipientState.memoTextField?.copy(
isError = value.isNotEmpty() && !isValidMemo,
isEnabled = true,
),
),
)
}
fun getOnXAddressMemoState(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = "",
isEnabled = false,
),
),
)
}
fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isNotValid = isAddressInWallet || !isValidAddress
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
recent = recipientState.recent.map { recent ->
recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY))
}.toPersistentList(),
wallets = recipientState.wallets.map { wallet ->
wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY))
}.toPersistentList(),
),
)
}
//endregion
//region send
fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()

View file

@ -1,20 +1,24 @@
package com.tangem.features.send.impl.presentation.state.confirm
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.minimalAmount
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
@ -23,6 +27,7 @@ import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.utils.getFiatString
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.lib.crypto.BlockchainUtils.isTezos
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
@ -46,6 +51,7 @@ internal class SendNotificationFactory(
private val clickIntents: SendClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val validateTransactionUseCase: ValidateTransactionUseCase,
) {
fun create(): Flow<ImmutableList<SendNotification>> = stateRouterProvider().currentState
@ -80,8 +86,9 @@ internal class SendNotificationFactory(
addFeeUnreachableNotification(feeState.feeSelectorState)
addExceedBalanceNotification(feeValue, sendingAmount)
addExceedsBalanceNotification(feeState.fee)
addDustWarningNotification(feeValue, sendingAmount)
addDustWarningNotificationForSpecificBlockchains(feeValue, sendingAmount)
addTransactionLimitErrorNotification(feeValue, sendingAmount)
// warnings
addExistentialWarningNotification(feeValue, amountValue)
addFeeCoverageNotification(
@ -92,6 +99,9 @@ internal class SendNotificationFactory(
addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce)
addTooHighNotification(feeState.feeSelectorState)
addTooLowNotification(feeState)
// blockchain specific
addCardanoNotifications(sendingAmount, feeState.fee, state)
}.toImmutableList()
}
@ -279,23 +289,35 @@ internal class SendNotificationFactory(
}
}
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
private suspend fun MutableList<SendNotification>.addDustWarningNotificationForSpecificBlockchains(
feeValue: BigDecimal,
sendingAmount: BigDecimal,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val dustValue = currencyChecksRepository.getDustValue(
userWalletProvider().walletId,
cryptoCurrencyStatus.currency.network,
) ?: return
val isCardano = BlockchainUtils.isCardano(cryptoCurrencyStatusProvider().currency.network.id.value)
if (checkDustLimits(feeAmount, receivedAmount, dustValue)) {
add(
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
)
if (!isCardano) {
addDustWarningNotification(feeValue, sendingAmount)
}
}
private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val change = when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
balance - (feeAmount + receivedAmount)
}
is CryptoCurrency.Token -> {
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
balance - feeAmount
}
}
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
return receivedAmount < dustValue || isChangeLowerThanDust
}
private fun MutableList<SendNotification>.addTooLowNotification(feeState: SendStates.FeeState) {
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
@ -397,13 +419,90 @@ internal class SendNotificationFactory(
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
}
private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
private suspend fun MutableList<SendNotification>.addCardanoNotifications(
sendingAmount: BigDecimal,
fee: Fee?,
state: SendUiState,
) {
val sendingCurrency = cryptoCurrencyStatusProvider().currency
if (!BlockchainUtils.isCardano(sendingCurrency.network.id.value)) return
val totalAmount = feeAmount + receivedAmount
val change = balance - totalAmount
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
return receivedAmount < dustValue || isChangeLowerThanDust
validateTransactionUseCase(
amount = sendingAmount.convertToAmount(sendingCurrency),
fee = fee ?: return,
memo = state.recipientState?.memoTextField?.value,
destination = requireNotNull(state.recipientState?.addressTextField?.value),
userWalletId = userWalletProvider().walletId,
network = sendingCurrency.network,
).fold(
ifLeft = {
addCardanoTransactionValidationError(
error = it as? BlockchainSdkError.Cardano ?: return@fold,
sendingCurrency = sendingCurrency,
)
},
ifRight = {
(fee as? Fee.CardanoToken)?.let {
add(
SendNotification.Cardano.MinAdaValueCharged(
tokenName = sendingCurrency.name,
minAdaValue = it.minAdaValue.parseBigDecimal(sendingCurrency.decimals),
),
)
}
},
)
}
private suspend fun MutableList<SendNotification>.addCardanoTransactionValidationError(
error: BlockchainSdkError.Cardano,
sendingCurrency: CryptoCurrency,
) {
when (error) {
BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> {
add(SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name))
}
BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> {
when (sendingCurrency) {
is CryptoCurrency.Coin -> SendNotification.Cardano.InsufficientBalanceToTransferCoin
is CryptoCurrency.Token -> {
SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)
}
}.let(::add)
}
BlockchainSdkError.Cardano.InsufficientRemainingBalance,
BlockchainSdkError.Cardano.InsufficientSendingAdaAmount,
-> {
val dustValue = currencyChecksRepository.getDustValue(
userWalletId = userWalletProvider().walletId,
network = sendingCurrency.network,
) ?: return
add(
SendNotification.Error.MinimumAmountError(
amount = dustValue.parseBigDecimal(sendingCurrency.decimals),
),
)
}
}
}
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
feeValue: BigDecimal,
sendingAmount: BigDecimal,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val dustValue = currencyChecksRepository.getDustValue(
userWalletProvider().walletId,
cryptoCurrencyStatus.currency.network,
) ?: return
if (checkDustLimits(feeValue, sendingAmount, dustValue)) {
add(
SendNotification.Error.MinimumAmountError(
amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals),
),
)
}
}
}

View file

@ -42,6 +42,7 @@ internal sealed class SendTextField {
val label: TextReference,
val isError: Boolean = false,
val error: TextReference? = null,
val isValuePasted: Boolean,
) : SendTextField()
data class RecipientMemo(
@ -54,6 +55,7 @@ internal sealed class SendTextField {
val error: TextReference? = null,
val disabledText: TextReference,
val isEnabled: Boolean,
val isValuePasted: Boolean,
) : SendTextField()
data class CustomFee(

View file

@ -30,6 +30,7 @@ internal object RecipientStatePreviewData {
label = stringReference("Recipient"),
isError = false,
error = null,
isValuePasted = false,
),
memoTextField = null,
recent = persistentListOf(

View file

@ -36,7 +36,7 @@ internal object SendClickIntentsStub : SendClickIntents {
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {}
override fun onRecipientMemoValueChange(value: String) {}
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {}
override fun feeReload() {}
@ -64,7 +64,8 @@ internal object SendClickIntentsStub : SendClickIntents {
reduceAmountBy: BigDecimal?,
reduceAmountTo: BigDecimal?,
clazz: Class<out SendNotification>,
) {}
) {
}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {}
}

View file

@ -0,0 +1,182 @@
package com.tangem.features.send.impl.presentation.state.recipient
import arrow.core.getOrElse
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import kotlinx.collections.immutable.toPersistentList
import timber.log.Timber
internal class RecipientSendFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
) {
private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientWalletListConverter()
}
private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientHistoryListConverter(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
fun onLoadedWalletsList(wallets: List<AvailableWallet?>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
wallets = recipientWalletListStateConverter.convert(wallets),
),
)
}
fun onLoadedHistoryList(txHistory: List<TxHistoryItem>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
recent = recipientHistoryListStateConverter.convert(txHistory),
),
)
}
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false, isValuePasted: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
addressTextField = recipientState.addressTextField.copy(value = value, isValuePasted = isValuePasted),
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
),
)
}
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = recipientState.memoTextField?.value.orEmpty(),
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
Timber.e("Failed to validateWalletMemoUseCase: $it")
false
}
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isValidating = false,
addressTextField = recipientState.addressTextField.copy(
error = when {
!isValidAddress -> resourceReference(R.string.send_recipient_address_error)
isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet)
else -> null
},
isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet,
),
),
)
}
fun getOnRecipientAddressValidationStarted(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(isValidating = true),
)
}
fun getOnRecipientMemoValueChange(value: String, isValuePasted: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = value,
isValuePasted = isValuePasted,
),
),
)
}
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = value,
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
Timber.e("Failed to validateWalletMemoUseCase: $it")
false
}
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isValidating = false,
memoTextField = recipientState.memoTextField?.copy(
isError = value.isNotEmpty() && !isValidMemo,
isEnabled = true,
),
),
)
}
fun getOnXAddressMemoState(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = "",
isEnabled = false,
),
),
)
}
fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isNotValid = isAddressInWallet || !isValidAddress
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
recent = recipientState.recent.map { recent ->
recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY))
}.toPersistentList(),
wallets = recipientState.wallets.map { wallet ->
wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY))
}.toPersistentList(),
),
)
}
}

View file

@ -24,6 +24,7 @@ internal class SendRecipientAddressFieldConverter(
error = resourceReference(R.string.send_recipient_address_error),
placeholder = resourceReference(R.string.send_enter_address_field),
label = resourceReference(R.string.send_recipient),
isValuePasted = false,
)
}
}

View file

@ -58,6 +58,7 @@ internal class SendRecipientMemoFieldConverter(
error = resourceReference(R.string.send_memo_destination_tag_error),
disabledText = resourceReference(R.string.send_additional_field_already_included),
isEnabled = true,
isValuePasted = false,
)
}

View file

@ -34,10 +34,10 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.utils.getFiatFormatted
import com.tangem.features.send.impl.presentation.utils.getFiatString
@Composable
@ -179,10 +179,10 @@ private fun SendingText(
}
if (feeFiat != null && sendingFiat != null) {
val sendingValue = getFiatFormatted(
value = sendingFiat,
currencySymbol = feeState.appCurrency.symbol,
currencyCode = feeState.appCurrency.code,
val sendingValue = BigDecimalFormatter.formatFiatAmount(
fiatAmount = sendingFiat,
fiatCurrencySymbol = feeState.appCurrency.symbol,
fiatCurrencyCode = feeState.appCurrency.code,
)
val feeValue = getFiatString(
value = feeState.fee?.amount?.value,

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.ui.amount
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -8,6 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData
@ -43,25 +45,12 @@ internal fun SendAmountContent(
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AmountFieldPreview_Light(
private fun AmountFieldPreview(
@PreviewParameter(AmountFieldPreviewProvider::class) amountState: SendStates.AmountState,
) {
TangemTheme {
SendAmountContent(
amountState = amountState,
isBalanceHiding = false,
clickIntents = SendClickIntentsStub,
)
}
}
@Preview
@Composable
private fun AmountFieldPreview_Dark(
@PreviewParameter(AmountFieldPreviewProvider::class) amountState: SendStates.AmountState,
) {
TangemTheme(isDark = true) {
TangemThemePreview {
SendAmountContent(
amountState = amountState,
isBalanceHiding = false,

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.ui.fee
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeType
@ -107,21 +109,12 @@ private fun FooterText(onReadMoreClick: () -> Unit) {
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SendSpeedSelectorPreview_Light(
private fun SendSpeedSelectorPreview(
@PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState,
) {
TangemTheme {
SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub)
}
}
@Preview
@Composable
private fun SendSpeedSelectorPreview_Dark(
@PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState,
) {
TangemTheme(isDark = true) {
TangemThemePreview {
SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.ui.recipient
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
@ -27,6 +28,7 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
@ -183,28 +185,12 @@ private fun ListItemLoading(modifier: Modifier = Modifier) {
// region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ListItemWithIconPreview_Light(
private fun ListItemWithIconPreview(
@PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig,
) {
TangemTheme {
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,
subtitleEndOffset = config.subtitleEndOffset,
subtitleIconRes = config.iconRes,
onClick = {},
isLoading = config.isLoading,
)
}
}
@Preview
@Composable
private fun ListItemWithIconPreview_Dark(
@PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig,
) {
TangemTheme(isDark = true) {
TangemThemePreview {
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,

View file

@ -69,7 +69,7 @@ internal fun SendRecipientContent(
)
memoField(
memoField = memoField,
onMemoChange = clickIntents::onRecipientMemoValueChange,
onMemoChange = { clickIntents.onRecipientMemoValueChange(it, true) },
)
listHeaderItem(
titleRes = R.string.send_recipient_wallets_title,
@ -116,6 +116,7 @@ private fun LazyListScope.addressItem(
isError = isError,
isLoading = isValidating,
error = address.error,
isValuePasted = address.isValuePasted,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
@ -141,6 +142,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM
isError = memoField.isError,
error = memoField.error,
isReadOnly = !memoField.isEnabled,
isValuePasted = memoField.isValuePasted,
)
}
}

View file

@ -30,6 +30,7 @@ internal fun TextFieldWithPaste(
error: TextReference? = null,
isError: Boolean = false,
isReadOnly: Boolean = false,
isValuePasted: Boolean = false,
) {
val (title, color) = when {
isError && error != null -> error to TangemTheme.colors.text.warning
@ -63,6 +64,7 @@ internal fun TextFieldWithPaste(
placeholderColor = placeholderColor,
onValueChange = onValueChange,
readOnly = isReadOnly,
isValuePasted = isValuePasted,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing6),

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.ui.send
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -16,6 +17,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.presentation.state.SendStates
@ -84,11 +86,10 @@ internal fun AmountBlock(
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AmountBlockPreview_Light(
@PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState,
) {
TangemTheme {
private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState) {
TangemThemePreview {
AmountBlock(
amountState = value,
isClickDisabled = false,
@ -98,21 +99,6 @@ private fun AmountBlockPreview_Light(
}
}
@Preview
@Composable
private fun AmountBlockPreview_Dark(
@PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState,
) {
TangemTheme(isDark = true) {
AmountBlock(
amountState = value,
isClickDisabled = true,
isEditingDisabled = false,
onClick = {},
)
}
}
private class AmountBlockPreviewProvider : PreviewParameterProvider<SendStates.AmountState> {
override val values: Sequence<SendStates.AmountState>
get() = sequenceOf(

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.ui.send
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.R
@ -110,21 +112,10 @@ private fun BoxScope.FeeError(feeSelectorState: FeeSelectorState) {
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun FeeBlockPreview_Light(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) {
TangemTheme {
FeeBlock(
feeState = value,
isClickDisabled = true,
onClick = {},
)
}
}
@Preview
@Composable
private fun FeeBlockPreview_Dark(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) {
TangemTheme(isDark = true) {
private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) {
TangemThemePreview {
FeeBlock(
feeState = value,
isClickDisabled = true,

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.ui.send
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
@ -97,26 +99,12 @@ private fun MemoBlock(memo: SendTextField.RecipientMemo?) {
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun RecipientBlockPreview_Light(
private fun RecipientBlockPreview(
@PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState,
) {
TangemTheme {
RecipientBlock(
recipientState = value,
isClickDisabled = true,
isEditingDisabled = false,
onClick = {},
)
}
}
@Preview
@Composable
private fun RecipientBlockPreview_Dark(
@PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState,
) {
TangemTheme(isDark = true) {
TangemThemePreview {
RecipientBlock(
recipientState = value,
isClickDisabled = true,

View file

@ -8,11 +8,8 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN
import com.tangem.domain.appcurrency.model.AppCurrency
import java.math.BigDecimal
import java.math.RoundingMode
private const val FIAT_DECIMALS = 2
private const val CRYPTO_FEE_DECIMALS = 6
private const val FEE_MINIMUM_VALUE = 0.01
internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? {
if (amount == null) return null
@ -37,27 +34,9 @@ internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency
internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String {
if (value == null || rate == null) return EMPTY_BALANCE_SIGN
val feeValue = value.multiply(rate)
return getFiatFormatted(feeValue, appCurrency.code, appCurrency.symbol)
}
internal fun getFiatFormatted(value: BigDecimal?, currencyCode: String, currencySymbol: String): String {
val scaled = value?.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO
return if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) {
buildString {
append(BigDecimalFormatter.CAN_BE_LOWER_SIGN)
append(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = BigDecimal(FEE_MINIMUM_VALUE),
fiatCurrencyCode = currencyCode,
fiatCurrencySymbol = currencySymbol,
),
)
}
} else {
BigDecimalFormatter.formatFiatAmount(
fiatAmount = value,
fiatCurrencyCode = currencyCode,
fiatCurrencySymbol = currencySymbol,
)
}
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeValue,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}

View file

@ -39,7 +39,7 @@ internal interface SendClickIntents {
// region Recipient
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null)
fun onRecipientMemoValueChange(value: String)
fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false)
// endregion
// region Fee

View file

@ -30,10 +30,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
@ -53,6 +50,7 @@ import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendFactory
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -100,6 +98,7 @@ internal class SendViewModel @Inject constructor(
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
validateTransactionUseCase: ValidateTransactionUseCase,
currencyChecksRepository: CurrencyChecksRepository,
isFeeApproximateUseCase: IsFeeApproximateUseCase,
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
@ -133,10 +132,16 @@ internal class SendViewModel @Inject constructor(
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
validateWalletMemoUseCase = validateWalletMemoUseCase,
isTapHelpPreviewEnabledProvider = Provider { isTapHelpPreviewEnabled },
)
private val recipientStateFactory = RecipientSendFactory(
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
validateWalletMemoUseCase = validateWalletMemoUseCase,
)
private val amountStateFactory = AmountStateFactory(
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
@ -178,6 +183,7 @@ internal class SendViewModel @Inject constructor(
clickIntents = this,
analyticsEventHandler = analyticsEventHandler,
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
validateTransactionUseCase = validateTransactionUseCase,
)
private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
@ -412,9 +418,9 @@ internal class SendViewModel @Inject constructor(
.orEmpty()
}.onSuccess { result ->
userWallets = result
uiState = stateFactory.onLoadedWalletsList(wallets = userWallets)
uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
}.onFailure {
uiState = stateFactory.onLoadedWalletsList(wallets = emptyList())
uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList())
}
}
}
@ -454,7 +460,7 @@ internal class SendViewModel @Inject constructor(
userWalletId = userWalletId,
currency = cryptoCurrency,
).getOrElse { emptyList() }
uiState = stateFactory.onLoadedHistoryList(txHistory = txHistoryList)
uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
}
private fun onStateActive() {
@ -588,7 +594,7 @@ internal class SendViewModel @Inject constructor(
)
}
// endregion
// endregion
// region amount state clicks
override fun onCurrencyChangeClick(isFiat: Boolean) {
@ -614,23 +620,24 @@ internal class SendViewModel @Inject constructor(
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
viewModelScope.launch {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.onRecipientAddressValueChange(value)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) }
autoNextFromRecipient(type, isValidAddress)
}
}.saveIn(addressValidationJobHolder)
}
override fun onRecipientMemoValueChange(value: String) {
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
viewModelScope.launch {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.getOnRecipientMemoValueChange(value)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
val recipientState = uiState.getRecipientState(stateRouter.isEditState)
val isValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty())
uiState = recipientStateFactory.getOnRecipientMemoValidState(value, isValidAddress)
}
}.saveIn(memoValidationJobHolder)
}
@ -649,16 +656,17 @@ internal class SendViewModel @Inject constructor(
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
uiState = stateFactory.onRecipientAddressValueChange(value, isXAddress = true)
uiState = stateFactory.getOnXAddressMemoState()
uiState =
recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true)
uiState = recipientStateFactory.getOnXAddressMemoState()
val isValidAddress = validateAddress(decodedAddress.address)
uiState = stateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
true
} ?: false
}
private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) {
uiState = stateFactory.getHiddenRecentListState(
uiState = recipientStateFactory.getHiddenRecentListState(
isAddressInWallet = isAddressInWallet,
isValidAddress = isValidAddress,
)

1
features/staking/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,16 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("kotlin-parcelize")
id("configuration")
}
android {
namespace = "com.tangem.features.staking.api"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.staking.api.featuretoggles
/**
* Staking feature toggles
*/
interface StakingFeatureToggles {
/** Availability of staking */
val isStakingEnabled: Boolean
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.staking.api.navigation
import androidx.fragment.app.Fragment
interface StakingRouter {
fun getEntryFragment(): Fragment
}

1
features/staking/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,62 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.staking.impl"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.appCompat)
implementation(deps.androidx.paging.runtime)
/** Other dependencies */
implementation(deps.kotlin.immutable.collections)
implementation(deps.material)
implementation(deps.arrow.core)
implementation(deps.lifecycle.compose)
implementation(deps.jodatime)
implementation(deps.timber)
/** Compose */
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.material3)
implementation(deps.compose.material)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.constraintLayout)
/** Tangem SDKs */
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain)
/** Core modules */
implementation(projects.core.featuretoggles)
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.navigation)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
/** Common */
implementation(projects.common)
/** Libs */
implementation(projects.libs.crypto)
/** Feature modules */
implementation(projects.features.staking.api)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.staking.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.features.staking.impl.featuretoggles.DefaultStakingFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* DI module provides implementation of [StakingFeatureToggles]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object StakingFeatureTogglesModule {
@Provides
@Singleton
fun provideStakingFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles {
return DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.staking.impl.di
import com.tangem.features.staking.impl.navigation.DefaultStakingRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
/**
* DI module provides implementation of [StakingRouter]
*/
@Module
@InstallIn(ActivityComponent::class)
internal object StakingRouterModule {
@Provides
@ActivityScoped
fun provideStakingRouter(): StakingRouter {
return DefaultStakingRouter()
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.staking.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
/**
* Default implementation of staking feature toggles
*
* @property featureTogglesManager manager for getting information about the availability of feature toggles
*/
internal class DefaultStakingFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : StakingFeatureToggles {
override val isStakingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_ENABLED")
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.staking.impl.navigation
import androidx.fragment.app.Fragment
internal class DefaultStakingRouter : InnerStakingRouter {
override fun getEntryFragment(): Fragment = TODO()
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.staking.impl.navigation
import com.tangem.features.staking.api.navigation.StakingRouter
interface InnerStakingRouter : StakingRouter

View file

@ -31,6 +31,7 @@ dependencies {
/** Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)

View file

@ -8,6 +8,7 @@ import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
@ -20,11 +21,10 @@ import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.api.SwapRepository
@ -48,7 +48,7 @@ internal class DefaultSwapRepository @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val walletsStateHolder: WalletsStateHolder,
private val userWalletsListManager: UserWalletsListManager,
private val errorsDataConverter: ErrorsDataConverter,
private val dataSignatureVerifier: DataSignatureVerifier,
moshi: Moshi,
@ -249,6 +249,7 @@ internal class DefaultSwapRepository @Inject constructor(
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
fromAddress: String,
toNetwork: String,
fromAmount: String,
fromDecimals: Int,
@ -266,6 +267,7 @@ internal class DefaultSwapRepository @Inject constructor(
fromContractAddress = fromContractAddress,
fromNetwork = fromNetwork,
toContractAddress = toContractAddress,
fromAddress = fromAddress,
toNetwork = toNetwork,
fromAmount = fromAmount,
fromDecimals = fromDecimals,
@ -389,8 +391,8 @@ internal class DefaultSwapRepository @Inject constructor(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = requireNotNull(
walletsStateHolder.userWalletsListManager
?.selectedUserWalletSync
userWalletsListManager
.selectedUserWalletSync
?.scanResponse
?.derivationStyleProvider,
),

View file

@ -1,8 +1,8 @@
package com.tangem.feature.swap.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.converter.Converter

View file

@ -8,7 +8,7 @@ import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.swap.DefaultSwapRepository
import com.tangem.feature.swap.DefaultSwapTransactionRepository
import com.tangem.feature.swap.converters.ErrorsDataConverter
@ -33,7 +33,7 @@ internal class SwapDataModule {
coroutineDispatcher: CoroutineDispatcherProvider,
dataSignature: DataSignatureVerifier,
walletManagerFacade: WalletManagersFacade,
walletsStateHolder: WalletsStateHolder,
userWalletsListManager: UserWalletsListManager,
errorsDataConverter: ErrorsDataConverter,
@NetworkMoshi moshi: Moshi,
): SwapRepository {
@ -42,7 +42,7 @@ internal class SwapDataModule {
tangemExpressApi = tangemExpressApi,
coroutineDispatcher = coroutineDispatcher,
walletManagersFacade = walletManagerFacade,
walletsStateHolder = walletsStateHolder,
userWalletsListManager = userWalletsListManager,
errorsDataConverter = errorsDataConverter,
dataSignatureVerifier = dataSignature,
moshi = moshi,

View file

@ -58,6 +58,7 @@ interface SwapRepository {
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
fromAddress: String,
toNetwork: String,
fromAmount: String,
fromDecimals: Int,

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.domain.transaction)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.demo)
implementation(projects.domain.card)
implementation(projects.domain.appCurrency.models)
@ -46,5 +47,6 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.timber)
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
implementation(deps.moshi)
}

View file

@ -9,4 +9,13 @@ sealed class Warning {
data class MinAmountWarning(val dustValue: BigDecimal) : Warning()
data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning()
sealed class Cardano : Warning() {
data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Cardano()
data object InsufficientBalanceToTransferCoin : Cardano()
data class InsufficientBalanceToTransferToken(val tokenName: String) : Cardano()
}
}

View file

@ -26,4 +26,6 @@ sealed class SwapTransactionState {
data object UnknownError : SwapTransactionState()
data class ExpressError(val dataError: DataError) : SwapTransactionState()
data object DemoMode : SwapTransactionState()
}

View file

@ -5,12 +5,15 @@ import arrow.core.getOrElse
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.common.extensions.minimalAmount
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -20,7 +23,10 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -34,12 +40,12 @@ import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import com.tangem.utils.toFiatString
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.firstOrNull
@ -59,6 +65,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val quotesRepository: QuotesRepository,
private val dispatcher: CoroutineDispatcherProvider,
private val swapTransactionRepository: SwapTransactionRepository,
@ -66,6 +73,8 @@ internal class SwapInteractorImpl @Inject constructor(
private val appCurrencyRepository: AppCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val initialToCurrencyResolver: InitialToCurrencyResolver,
private val demoConfig: DemoConfig,
private val transactionRepository: TransactionRepository,
) : SwapInteractor {
private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) {
@ -343,6 +352,7 @@ internal class SwapInteractorImpl @Inject constructor(
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
txFee = TxFeeState.Empty,
transactionFee = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
)
}
@ -372,6 +382,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenStatus: CryptoCurrencyStatus,
amount: SwapAmount,
feeState: TxFeeState,
minAdaValue: BigDecimal?,
): List<Warning> {
val fromToken = fromTokenStatus.currency
val userWalletId = getSelectedWallet()?.walletId ?: return emptyList()
@ -379,6 +390,13 @@ internal class SwapInteractorImpl @Inject constructor(
manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken)
manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount)
manageReduceAmountWarning(warnings, fromTokenStatus, amount)
manageCardanoTransactionValidationWarnings(
warnings = warnings,
fromToken = fromToken,
amount = amount,
userWalletId = userWalletId,
minAdaValue = minAdaValue,
)
return warnings
}
@ -407,23 +425,35 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenStatus: CryptoCurrencyStatus,
amount: SwapAmount,
) {
if (BlockchainUtils.isCardano(fromTokenStatus.currency.network.id.value)) return
val fee = when (feeState) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue
is TxFeeState.SingleFeeState -> feeState.fee.feeValue
}
val dust = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network)
val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO
if (dust != null &&
!balance.isNullOrZero() &&
amount.value < balance
) {
val change = balance - (amount.value + fee)
val isChangeLowerThanDust = change < dust && change != BigDecimal.ZERO
val isShowWarning = amount.value + fee < dust || isChangeLowerThanDust
if (isShowWarning) {
warnings.add(Warning.MinAmountWarning(dust))
val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) ?: return
val change = when (fromTokenStatus.currency) {
is CryptoCurrency.Coin -> {
val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO
balance - (fee + amount.value)
}
is CryptoCurrency.Token -> {
val nativeTokenBalance = userWalletManager.getNativeTokenBalance(
fromTokenStatus.currency.network.id.value,
fromTokenStatus.currency.network.derivationPath.value,
)
nativeTokenBalance?.value?.minus(fee) ?: BigDecimal.ZERO
}
}
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
if (amount.value < dustValue || isChangeLowerThanDust) {
warnings.add(Warning.MinAmountWarning(dustValue))
}
}
@ -438,6 +468,72 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun manageCardanoTransactionValidationWarnings(
warnings: MutableList<Warning>,
fromToken: CryptoCurrency,
amount: SwapAmount,
userWalletId: UserWalletId,
minAdaValue: BigDecimal?,
) {
transactionRepository.validateTransaction(
amount = amount.value.convertToAmount(fromToken),
fee = null,
memo = null,
destination = getTokenAddress(fromToken),
userWalletId = userWalletId,
network = fromToken.network,
)
.fold(
onFailure = {
addCardanoTransactionValidationError(
warnings = warnings,
error = it as? BlockchainSdkError.Cardano ?: return@fold,
fromToken = fromToken,
userWalletId = userWalletId,
)
},
onSuccess = {
minAdaValue?.let {
warnings.add(
Warning.Cardano.MinAdaValueCharged(
tokenName = fromToken.name,
minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals),
),
)
}
},
)
}
private suspend fun addCardanoTransactionValidationError(
warnings: MutableList<Warning>,
error: BlockchainSdkError.Cardano,
fromToken: CryptoCurrency,
userWalletId: UserWalletId,
) {
when (error) {
BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> {
Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name)
}
BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> {
when (fromToken) {
is CryptoCurrency.Coin -> Warning.Cardano.InsufficientBalanceToTransferCoin
is CryptoCurrency.Token -> {
Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name)
}
}
}
BlockchainSdkError.Cardano.InsufficientRemainingBalance,
BlockchainSdkError.Cardano.InsufficientSendingAdaAmount,
-> {
val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromToken.network) ?: return
Warning.MinAmountWarning(dustValue)
}
}
.let(warnings::add) // add warning to the list
}
override suspend fun onSwap(
swapProvider: SwapProvider,
swapData: SwapDataModel?,
@ -473,6 +569,7 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToGet = currencyToGet.currency,
amountToSwap = amountToSwap,
fee = fee,
userWalletId = requireNotNull(getSelectedWallet()).walletId,
)
}
}
@ -523,29 +620,35 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToGet: CryptoCurrency,
amountToSwap: String,
fee: TxFee,
userWalletId: UserWalletId,
): SwapTransactionState {
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
val amount = SwapAmount(amountDecimal, currencyToSend.decimals)
val derivationPath = currencyToSend.network.derivationPath.value
val result = transactionManager.sendTransaction(
txData = SwapTxData(
networkId = networkId,
amountToSend = amountDecimal,
currencyToSend = swapCurrencyConverter.convert(currencyToSend),
feeAmount = fee.feeValue,
gasLimit = fee.gasLimit,
destinationAddress = swapData.transaction.txTo,
dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData,
val txData = createTransactionUseCase(
amount = amount.value.convertToAmount(currencyToSend),
fee = getFeeForTransaction(
fee = fee,
blockchain = Blockchain.fromId(currencyToSend.network.id.value),
),
memo = null,
destination = swapData.transaction.txTo,
userWalletId = userWalletId,
network = currencyToSend.network,
hash = (swapData.transaction as ExpressTransactionModel.DEX).txData,
isSwap = true,
derivationPath = derivationPath,
analyticsData = AnalyticsData(
feeType = fee.feeType.getNameForAnalytics(),
tokenSymbol = currencyToSend.symbol,
),
).getOrElse {
Timber.e(it)
return SwapTransactionState.UnknownError
}
val result = sendTransactionUseCase(
txData = txData,
userWallet = requireNotNull(getSelectedWallet()),
network = currencyToSend.network,
)
return when (result) {
is SendTxResult.Success -> {
return result.fold(
ifRight = {
storeLastCryptoCurrencyId(currencyToGet)
SwapTransactionState.TxSent(
fromAmount = amountFormatter.formatSwapAmountToUI(
@ -561,13 +664,18 @@ internal class SwapInteractorImpl @Inject constructor(
txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(),
timestamp = System.currentTimeMillis(),
)
}
SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled
is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTxResult.NetworkError -> SwapTransactionState.NetworkError
is SendTxResult.UnknownError -> SwapTransactionState.UnknownError
}
},
ifLeft = {
when (it) {
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
else -> SwapTransactionState.UnknownError
}
},
)
}
@Suppress("LongMethod")
@ -583,13 +691,14 @@ internal class SwapInteractorImpl @Inject constructor(
fromContractAddress = currencyToSend.currency.getContractAddress(),
fromNetwork = currencyToSend.currency.network.backendId,
toContractAddress = currencyToGet.currency.getContractAddress(),
fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(),
toNetwork = currencyToGet.currency.network.backendId,
fromAmount = amount.toStringWithRightOffset(),
fromDecimals = amount.decimals,
toDecimals = currencyToGet.currency.decimals,
providerId = swapProvider.providerId,
rateType = RateType.FLOAT,
toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "",
toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(),
refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value,
refundExtraId = null, // currently always null
).getOrElse { return SwapTransactionState.ExpressError(it) }
@ -601,10 +710,11 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToSend.currency.network.backendId,
exchangeDataCex.txExtraId,
)
if (txExtras == null && exchangeDataCex.txExtraId != null) {
val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError
if (txExtras == null && exchangeDataCex.txExtraId != null && !demoConfig.isDemoCardId(cardId)) {
return SwapTransactionState.UnknownError
}
val txData = walletManagersFacade.createTransaction(
val txData = createTransactionUseCase(
amount = amount.value.convertToAmount(currencyToSend.currency),
fee = getFeeForTransaction(
fee = txFee,
@ -614,12 +724,13 @@ internal class SwapInteractorImpl @Inject constructor(
destination = exchangeDataCex.txTo,
userWalletId = userWalletId,
network = currencyToSend.currency.network,
)?.copy(
extras = txExtras,
)
).getOrElse {
Timber.e(it)
return SwapTransactionState.UnknownError
}.copy(extras = txExtras)
val result = sendTransactionUseCase(
requireNotNull(txData),
txData = txData,
userWallet = requireNotNull(getSelectedWallet()),
network = currencyToSend.currency.network,
)
@ -634,6 +745,7 @@ internal class SwapInteractorImpl @Inject constructor(
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
else -> SwapTransactionState.UnknownError
}
},
@ -818,8 +930,16 @@ internal class SwapInteractorImpl @Inject constructor(
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
return coroutineScope {
val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId ->
getUnhandledFee(
amount = amount.value,
userWalletId = userWalletId,
cryptoCurrency = fromToken,
)
}
val txFee = if (provider.type == ExchangeProviderType.CEX) {
getFeeForCex(amount, fromTokenStatus)
getFeeForCex(txFeeResult, fromTokenStatus)
} else {
TxFeeState.Empty
}
@ -858,11 +978,13 @@ internal class SwapInteractorImpl @Inject constructor(
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
txFee = txFee,
transactionFee = txFeeResult?.getOrNull(),
includeFeeInAmount = includeFeeInAmount,
)
}
}
@Suppress("LongMethod")
private suspend fun getQuotesState(
exchangeProviderType: ExchangeProviderType,
quoteDataModel: Either<DataError, QuoteModel>,
@ -873,6 +995,7 @@ internal class SwapInteractorImpl @Inject constructor(
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
txFee: TxFeeState,
transactionFee: TransactionFee?,
includeFeeInAmount: IncludeFeeInAmount,
): SwapState {
return quoteDataModel.fold(
@ -886,7 +1009,12 @@ internal class SwapInteractorImpl @Inject constructor(
swapData = null,
txFeeState = txFee,
).copy(
warnings = manageWarnings(fromToken, amount, txFee),
warnings = manageWarnings(
fromTokenStatus = fromToken,
amount = amount,
feeState = txFee,
minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue,
),
)
when (exchangeProviderType) {
@ -1048,13 +1176,14 @@ internal class SwapInteractorImpl @Inject constructor(
fromContractAddress = fromToken.currency.getContractAddress(),
fromNetwork = fromToken.currency.network.backendId,
toContractAddress = toToken.currency.getContractAddress(),
fromAddress = fromToken.value.networkAddress?.defaultAddress?.value.orEmpty(),
toNetwork = toToken.currency.network.backendId,
fromAmount = amount.toStringWithRightOffset(),
fromDecimals = amount.decimals,
toDecimals = toToken.currency.decimals,
providerId = provider.providerId,
rateType = RateType.FLOAT,
toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "",
toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(),
).fold(
ifRight = { swapData ->
val feeData = transactionManager.getFee(
@ -1089,7 +1218,14 @@ internal class SwapInteractorImpl @Inject constructor(
)
swapState.copy(
permissionState = PermissionDataState.Empty,
warnings = manageWarnings(fromToken, amount, txFeeState),
warnings = manageWarnings(
fromToken,
amount,
txFeeState,
(feeData as? ProxyFees.SingleFee)?.let {
(it.singleFee as? ProxyFee.CardanoToken)?.minAdaValue
},
),
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = true,
isBalanceEnough = isBalanceIncludeFeeEnough,
@ -1155,23 +1291,26 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
private suspend fun getFeeForCex(amount: SwapAmount, fromToken: CryptoCurrencyStatus): TxFeeState {
getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId ->
val txFeeResult = estimateFeeUseCase(
amount = amount.value,
userWalletId = userWalletId,
cryptoCurrency = fromToken.currency,
).firstOrNull()
return txFeeResult?.fold(
ifLeft = {
TxFeeState.Empty
},
ifRight = { txFee ->
txFee.toTxFeeState(fromToken.currency)
},
) ?: TxFeeState.Empty
}
return TxFeeState.Empty
private suspend fun getFeeForCex(
txFeeResult: Either<GetFeeError, TransactionFee>?,
fromToken: CryptoCurrencyStatus,
): TxFeeState {
return txFeeResult?.fold(
ifLeft = { TxFeeState.Empty },
ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency) },
) ?: TxFeeState.Empty
}
private suspend fun getUnhandledFee(
amount: BigDecimal,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Either<GetFeeError, TransactionFee>? {
return estimateFeeUseCase(
amount = amount,
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
).firstOrNull()
}
@Suppress("LongParameterList", "LongMethod")

View file

@ -11,9 +11,10 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.*
import com.tangem.feature.swap.domain.api.SwapRepository
@ -40,6 +41,7 @@ class SwapDomainModule {
@SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
@SwapScope sendTransactionUseCase: SendTransactionUseCase,
@SwapScope createTransactionUseCase: CreateTransactionUseCase,
quotesRepository: QuotesRepository,
swapTransactionRepository: SwapTransactionRepository,
appCurrencyRepository: AppCurrencyRepository,
@ -48,6 +50,7 @@ class SwapDomainModule {
coroutineDispatcherProvider: CoroutineDispatcherProvider,
initialToCurrencyResolver: InitialToCurrencyResolver,
currenciesRepository: CurrenciesRepository,
transactionRepository: TransactionRepository,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@ -57,6 +60,7 @@ class SwapDomainModule {
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase,
sendTransactionUseCase = sendTransactionUseCase,
createTransactionUseCase = createTransactionUseCase,
quotesRepository = quotesRepository,
walletManagersFacade = walletManagersFacade,
dispatcher = coroutineDispatcherProvider,
@ -65,6 +69,8 @@ class SwapDomainModule {
currencyChecksRepository = currencyChecksRepository,
currenciesRepository = currenciesRepository,
initialToCurrencyResolver = initialToCurrencyResolver,
demoConfig = DemoConfig(),
transactionRepository = transactionRepository,
)
}
@ -79,8 +85,8 @@ class SwapDomainModule {
@SwapScope
@Provides
@Singleton
fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder)
fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ -106,13 +112,11 @@ class SwapDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCardTokensListUseCase {
return GetCardTokensListUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
dispatchers = dispatchers,
)
}
@ -122,6 +126,15 @@ class SwapDomainModule {
return IsDemoCardUseCase(config = DemoConfig())
}
@SwapScope
@Provides
@Singleton
fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase {
return CreateTransactionUseCase(
transactionRepository = transactionRepository,
)
}
@SwapScope
@Provides
@Singleton

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
@ -56,6 +57,9 @@ dependencies {
implementation(projects.features.swap.api)
implementation(projects.features.tokendetails.api)
/** Libs */
implementation(projects.libs.crypto)
/** Other libraries */
implementation(deps.compose.shimmer)
implementation(deps.compose.accompanist.webView)
@ -67,5 +71,4 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -20,7 +20,6 @@ data class SwapStateHolder(
val alert: SwapWarning.GenericWarning? = null,
val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED,
val providerState: ProviderState,
val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01
val fee: FeeItemState = FeeItemState.Empty,
val permissionState: SwapPermissionState = SwapPermissionState.Empty,
@ -111,6 +110,7 @@ sealed interface SwapWarning {
val type: GenericWarningType = GenericWarningType.OTHER,
val onClick: () -> Unit,
) : SwapWarning
data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning
data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning
data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning
@ -118,6 +118,16 @@ sealed interface SwapWarning {
data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning
data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning
data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning
sealed interface Cardano : SwapWarning {
val notificationConfig: NotificationConfig
data class MinAdaValueCharged(override val notificationConfig: NotificationConfig) : Cardano
data class InsufficientBalanceToTransferCoin(override val notificationConfig: NotificationConfig) : Cardano
data class InsufficientBalanceToTransferToken(override val notificationConfig: NotificationConfig) : Cardano
}
}
enum class GenericWarningType {

View file

@ -15,7 +15,6 @@ data class UiActions(
val onBackClicked: () -> Unit,
val onMaxAmountSelected: () -> Unit,
val onReduceAmount: (SwapAmount) -> Unit,
val onReduceAmountIgnoreClick: () -> Unit,
val openPermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
// region new actions

View file

@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.navigation.ReduxNavController
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.feature.swap.router.CustomTabsManager
import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
@ -25,7 +25,7 @@ import javax.inject.Inject
class SwapFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var reduxNavController: ReduxNavController

View file

@ -23,6 +23,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
import com.tangem.feature.swap.models.states.FeeItemState
@ -168,7 +169,7 @@ private fun ChooseFeeBottomSheetContent_Preview() {
),
).toImmutableList()
Column {
TangemTheme(isDark = true) {
TangemThemePreview(isDark = true) {
ChooseFeeBottomSheetContent(
ChooseFeeBottomSheetConfig(
selectedFee = FeeType.NORMAL,
@ -183,7 +184,7 @@ private fun ChooseFeeBottomSheetContent_Preview() {
SpacerH24()
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
ChooseFeeBottomSheetContent(
ChooseFeeBottomSheetConfig(
selectedFee = FeeType.NORMAL,

View file

@ -18,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
@ -128,7 +129,7 @@ private fun ChooseProviderBottomSheet_Preview() {
alertText = stringReference("Unavailable"),
),
)
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
ChooseProviderBottomSheetContent(
ChooseProviderBottomSheetConfig(
selectedProviderId = "1",

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.components.rows.SimpleActionRow
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.models.states.FeeItemState
@ -63,13 +64,13 @@ private fun FeeItemPreview() {
onClick = {},
)
Column {
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
FeeItem(state = state)
}
SpacerH24()
TangemTheme(isDark = true) {
TangemThemePreview(isDark = true) {
FeeItem(state = state)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -27,6 +28,7 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
@ -390,25 +392,12 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) {
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ProviderItemPreview_Light(
private fun ProviderItemPreview(
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<ProviderState, Boolean>,
) {
TangemTheme {
ProviderItem(
modifier = Modifier.background(TangemTheme.colors.background.action),
state = state.first,
isSelected = state.second,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ProviderItemPreview_Dark(
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<ProviderState, Boolean>,
) {
TangemTheme(isDark = true) {
TangemThemePreview {
ProviderItem(
modifier = Modifier.background(TangemTheme.colors.background.action),
state = state.first,

View file

@ -88,7 +88,6 @@ internal class StateBuilder(
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
providerState = ProviderState.Empty(),
shouldShowMaxAmount = false,
reduceAmountIgnore = false,
priceImpact = PriceImpact.Empty(),
)
}
@ -217,7 +216,6 @@ internal class StateBuilder(
val warnings = getWarningsForSuccessState(
quoteModel = quoteModel,
fromToken = fromToken,
ignoreAmountReduce = uiStateHolder.reduceAmountIgnore,
selectedFeeType = selectedFeeType,
)
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
@ -317,11 +315,10 @@ internal class StateBuilder(
private fun getWarningsForSuccessState(
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
ignoreAmountReduce: Boolean,
selectedFeeType: FeeType,
): List<SwapWarning> {
val warnings = mutableListOf<SwapWarning>()
maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce)
maybeAddDomainWarnings(quoteModel, warnings)
maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType)
@ -357,11 +354,7 @@ internal class StateBuilder(
}
}
private fun maybeAddDomainWarnings(
quoteModel: SwapState.QuotesLoadedState,
warnings: MutableList<SwapWarning>,
ignoreAmountReduce: Boolean,
) {
private fun maybeAddDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList<SwapWarning>) {
quoteModel.warnings.forEach {
when (it) {
is Warning.ExistentialDepositWarning -> {
@ -399,23 +392,29 @@ internal class StateBuilder(
)
}
is Warning.ReduceAmountWarning -> {
if (!ignoreAmountReduce) {
warnings.add(
SwapWarning.ReduceAmount(
notificationConfig = createReduceAmountNotificationConfig(
amount = it.tezosFeeThreshold.toPlainString(),
onConfirmClick = {
val fromAmount = quoteModel.fromTokenInfo.tokenAmount
val patchedAmount = fromAmount.copy(
value = fromAmount.value - it.tezosFeeThreshold,
)
actions.onReduceAmount(patchedAmount)
},
onDismissClick = actions.onReduceAmountIgnoreClick,
),
warnings.add(
SwapWarning.ReduceAmount(
notificationConfig = createReduceAmountNotificationConfig(
amount = it.tezosFeeThreshold.toPlainString(),
onConfirmClick = {
val fromAmount = quoteModel.fromTokenInfo.tokenAmount
val patchedAmount = fromAmount.copy(
value = fromAmount.value - it.tezosFeeThreshold,
)
actions.onReduceAmount(patchedAmount)
},
),
)
}
),
)
}
Warning.Cardano.InsufficientBalanceToTransferCoin -> {
warnings.add(createInsufficientBalanceToTransferCoin())
}
is Warning.Cardano.InsufficientBalanceToTransferToken -> {
warnings.add(createInsufficientBalanceToTransferToken(tokenName = it.tokenName))
}
is Warning.Cardano.MinAdaValueCharged -> {
warnings.add(createMinAdaValueCharged(minAdaValue = it.minAdaValue, tokenName = it.tokenName))
}
}
}
@ -1015,6 +1014,18 @@ internal class StateBuilder(
)
}
fun createDemoModeAlert(uiState: SwapStateHolder, onAlertClick: () -> Unit): SwapStateHolder {
return uiState.copy(
alert = SwapWarning.GenericWarning(
title = resourceReference(id = R.string.warning_demo_mode_title),
message = resourceReference(id = R.string.warning_demo_mode_message),
onClick = onAlertClick,
type = GenericWarningType.OTHER,
),
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
)
}
private fun getProviderErrorMessage(dataError: DataError): TextReference? {
return when (dataError) {
is DataError.SwapsAreUnavailableNowError -> resourceReference(
@ -1374,20 +1385,14 @@ internal class StateBuilder(
)
}
private fun createReduceAmountNotificationConfig(
amount: String,
onConfirmClick: () -> Unit,
onDismissClick: () -> Unit,
): NotificationConfig {
private fun createReduceAmountNotificationConfig(amount: String, onConfirmClick: () -> Unit): NotificationConfig {
return NotificationConfig(
title = resourceReference(R.string.send_notification_high_fee_title),
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)),
iconResId = R.drawable.img_attention_20,
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)),
onPrimaryClick = onConfirmClick,
secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore),
onSecondaryClick = onDismissClick,
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)),
onClick = onConfirmClick,
),
)
}
@ -1431,6 +1436,42 @@ internal class StateBuilder(
iconResId = R.drawable.img_attention_20,
)
}
private fun createMinAdaValueCharged(minAdaValue: String, tokenName: String): SwapWarning {
return SwapWarning.Cardano.MinAdaValueCharged(
NotificationConfig(
title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title),
subtitle = resourceReference(
id = R.string.cardano_coin_will_be_send_with_token_description,
formatArgs = wrappedList(minAdaValue, tokenName),
),
iconResId = R.drawable.img_attention_20,
),
)
}
private fun createInsufficientBalanceToTransferCoin(): SwapWarning {
return SwapWarning.Cardano.InsufficientBalanceToTransferCoin(
NotificationConfig(
title = resourceReference(id = R.string.cardano_max_amount_has_token_title),
subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description),
iconResId = R.drawable.img_attention_20,
),
)
}
private fun createInsufficientBalanceToTransferToken(tokenName: String): SwapWarning {
return SwapWarning.Cardano.InsufficientBalanceToTransferToken(
NotificationConfig(
title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title),
subtitle = resourceReference(
id = R.string.cardano_insufficient_balance_to_send_token_description,
formatArgs = wrappedList(tokenName),
),
iconResId = R.drawable.img_attention_20,
),
)
}
// end region
private fun getShortAddressValue(fullAddress: String): String {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -17,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.ApprovePermissionButton
import com.tangem.feature.swap.models.ApproveType
import com.tangem.feature.swap.models.CancelPermissionButton
@ -291,17 +293,10 @@ private fun getTitleForApproveType(approveType: ApproveType): String = when (app
// region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AgreementBottomSheet_InLightTheme() {
TangemTheme(isDark = false) {
SwapPermissionBottomSheetContent(content = previewData)
}
}
@Preview
@Composable
private fun Preview_AgreementBottomSheet_InDarkTheme() {
TangemTheme(isDark = true) {
private fun Preview_AgreementBottomSheet() {
TangemThemePreview {
SwapPermissionBottomSheetContent(content = previewData)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -30,6 +31,7 @@ import com.tangem.core.ui.extensions.getActiveIconResByCoinId
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.domain.models.ui.PriceImpact
import com.tangem.feature.swap.models.*
@ -389,7 +391,8 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
},
)
}
else -> {}
is SwapWarning.Cardano -> Notification(config = warning.notificationConfig)
SwapWarning.InsufficientFunds -> Unit
}
SpacerH8()
}
@ -497,7 +500,6 @@ private val state = SwapStateHolder(
providerState = ProviderState.Loading(),
priceImpact = PriceImpact.Empty(),
shouldShowMaxAmount = true,
reduceAmountIgnore = false,
tosState = TosState(
tosLink = LegalState(
title = stringReference("Terms of Use"),
@ -513,9 +515,10 @@ private val state = SwapStateHolder(
)
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SwapScreenContentPreview() {
TangemTheme(isDark = false) {
TangemThemePreview {
SwapScreenContent(state = state, modifier = Modifier)
}
}

View file

@ -27,6 +27,7 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.ImmutableList
@ -303,7 +304,7 @@ private val title = TokenToSelectState.Title(
@Preview
@Composable
private fun TokenScreenPreview() {
TangemTheme(isDark = false) {
TangemThemePreview {
SwapSelectTokenScreen(
state = SwapSelectTokenStateHolder(
availableTokens = listOf(title, token, token, token).toImmutableList(),
@ -320,7 +321,7 @@ private fun TokenScreenPreview() {
@Preview
@Composable
private fun EmptyTokensListPreview() {
TangemTheme(isDark = false) {
TangemThemePreview {
EmptyTokensList()
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -18,6 +19,7 @@ import com.tangem.core.ui.components.inputrow.InputRowImage
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.models.SwapSuccessStateHolder
import com.tangem.feature.swap.presentation.R
@ -167,19 +169,11 @@ private val state = SwapSuccessStateHolder(
)
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_Success_InLightTheme() {
TangemTheme(isDark = false) {
private fun Preview_Success() {
TangemThemePreview {
SwapSuccessScreen(state) {}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview_Success_InDarkTheme() {
TangemTheme(isDark = true) {
SwapSuccessScreen(state) {}
}
}
// endregion preview

View file

@ -38,6 +38,7 @@ import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.feature.swap.domain.models.ui.PriceImpact
import com.tangem.feature.swap.models.TransactionCardType
@ -498,7 +499,7 @@ private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: In
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
private fun Preview_TransactionCard_InLightTheme() {
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
TransactionCardPreview()
}
}
@ -506,7 +507,7 @@ private fun Preview_TransactionCard_InLightTheme() {
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
private fun Preview_TransactionCardWithPriceImpact_InLightTheme() {
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
TransactionCardPreviewWithPriceImpact()
}
}
@ -514,7 +515,7 @@ private fun Preview_TransactionCardWithPriceImpact_InLightTheme() {
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
private fun Preview_TransactionCardWithoutPriceImpact_InLightTheme() {
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
TransactionCardPreviewWithoutPriceImpact()
}
}
@ -522,7 +523,7 @@ private fun Preview_TransactionCardWithoutPriceImpact_InLightTheme() {
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
private fun Preview_TransactionCard_InDarkTheme() {
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
TransactionCardPreview()
}
}
@ -530,7 +531,7 @@ private fun Preview_TransactionCard_InDarkTheme() {
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
private fun Preview_TransactionCardWithPriceImpact_InDarkTheme() {
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
TransactionCardPreviewWithPriceImpact()
}
}
@ -538,7 +539,7 @@ private fun Preview_TransactionCardWithPriceImpact_InDarkTheme() {
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() {
TangemTheme(isDark = false) {
TangemThemePreview(isDark = false) {
TransactionCardPreviewWithoutPriceImpact()
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@ -14,6 +15,7 @@ import com.google.accompanist.web.rememberWebViewState
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig
@Composable
@ -55,21 +57,10 @@ private fun WebViewBottomSheetContent(content: WebViewBottomSheetConfig) {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_WebViewBottomSheetContent_InLightTheme() {
TangemTheme(isDark = false) {
WebViewBottomSheetContent(
content = WebViewBottomSheetConfig(
url = "https://tangem.com/en/",
),
)
}
}
@Preview
@Composable
private fun Preview_WebViewBottomSheetContent_InDarkTheme() {
TangemTheme(isDark = true) {
private fun Preview_WebViewBottomSheetContent() {
TangemThemePreview {
WebViewBottomSheetContent(
content = WebViewBottomSheetConfig(
url = "https://tangem.com/en/",

View file

@ -540,6 +540,12 @@ internal class SwapViewModel @Inject constructor(
is SwapTransactionState.UserCancelled -> {
startLoadingQuotesFromLastState()
}
is SwapTransactionState.DemoMode -> {
startLoadingQuotesFromLastState()
uiState = stateBuilder.createDemoModeAlert(uiState) {
uiState = stateBuilder.clearAlert(uiState)
}
}
else -> {
startLoadingQuotesFromLastState()
uiState = stateBuilder.createErrorTransaction(uiState, it) {
@ -881,12 +887,6 @@ internal class SwapViewModel @Inject constructor(
},
onMaxAmountSelected = ::onMaxAmountClicked,
onReduceAmount = ::onReduceAmountClicked,
onReduceAmountIgnoreClick = {
uiState = uiState.copy(
reduceAmountIgnore = true,
warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount },
)
},
openPermissionBottomSheet = {
singleTaskScheduler.cancelTask()
analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked)

View file

@ -0,0 +1,9 @@
package com.tangem.features.tester.api
/**
* Interface for app restarter
*/
interface AppRestarter {
fun restart()
}

View file

@ -0,0 +1,12 @@
package com.tangem.feature.tester
import android.app.Activity
/**
* Wraps the main activity class to avoid type erasure issues during injection.
*
* @property clazz activity class
*/
class ActivityClassWrapper(
val clazz: Class<out Activity>,
)

View file

@ -0,0 +1,27 @@
package com.tangem.feature.tester.apprestarter
import android.app.Activity
import android.content.Context
import android.content.Intent
import com.tangem.feature.tester.ActivityClassWrapper
import com.tangem.features.tester.api.AppRestarter
/**
* Entity that kills the process and restarts the main activity
* @property context Activity context
*/
internal class DefaultAppRestarter(
private val context: Context,
private val activityClassWrapper: ActivityClassWrapper,
) : AppRestarter {
override fun restart() {
if (context !is Activity) return
context.finish()
context.startActivity(
Intent(context, activityClassWrapper.clazz).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP },
)
Runtime.getRuntime().exit(0)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.feature.tester.di
import android.content.Context
import com.tangem.feature.tester.ActivityClassWrapper
import com.tangem.feature.tester.apprestarter.DefaultAppRestarter
import com.tangem.features.tester.api.AppRestarter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
internal object RestarterModule {
@Provides
@ActivityScoped
fun provideAppRestarter(
@ActivityContext context: Context,
activityClassWrapper: ActivityClassWrapper,
): AppRestarter {
return DefaultAppRestarter(context, activityClassWrapper)
}
}

View file

@ -6,10 +6,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel
import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen
@ -18,6 +18,7 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState
import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.feature.tester.presentation.navigation.TesterScreen
import com.tangem.features.tester.api.AppRestarter
import com.tangem.features.tester.api.TesterRouter
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -27,12 +28,15 @@ import javax.inject.Inject
internal class TesterActivity : ComposeActivity() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
override lateinit var uiDependencies: UiDependencies
/** Router for inner feature navigation */
@Inject
lateinit var testerRouter: TesterRouter
@Inject
lateinit var appRestarter: AppRestarter
private val innerTesterRouter: InnerTesterRouter
get() = requireNotNull(testerRouter as? InnerTesterRouter) {
"TesterRouter must be InnerTesterRouter for tester feature"
@ -66,7 +70,7 @@ internal class TesterActivity : ComposeActivity() {
composable(route = TesterScreen.FEATURE_TOGGLES.name) {
val viewModel = hiltViewModel<FeatureTogglesViewModel>().apply {
setupNavigation(innerTesterRouter)
setupInteractions(innerTesterRouter, appRestarter)
}
FeatureTogglesScreen(state = viewModel.uiState)

View file

@ -3,15 +3,16 @@ package com.tangem.feature.tester.presentation.actions
import com.tangem.domain.apptheme.model.AppThemeMode
internal data class TesterActionsContentState(
val onBackClick: () -> Unit,
val hideAllCurrenciesConfig: HideAllCurrenciesConfig,
val toggleAppThemeConfig: ToggleAppThemeConfig,
val onBackClick: () -> Unit,
val onApplyChangesClick: () -> Unit,
)
internal sealed class HideAllCurrenciesConfig {
data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesConfig()
object Progress : HideAllCurrenciesConfig()
data object Progress : HideAllCurrenciesConfig()
}
internal data class ToggleAppThemeConfig(

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tester.presentation.actions
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -12,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.feature.tester.impl.R
@ -76,26 +78,20 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) {
) {
TesterActionsScreen(
state = TesterActionsContentState(
onBackClick = {},
hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable {},
toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT) {},
onBackClick = {},
onApplyChangesClick = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TesterActionsScreenPreview_Light() {
TangemTheme {
TesterActionsScreenSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TesterActionsScreenPreview_Dark() {
TangemTheme(isDark = true) {
private fun TesterActionsScreenPreview() {
TangemThemePreview {
TesterActionsScreenSample()
}
}

View file

@ -31,9 +31,10 @@ internal class TesterActionsViewModel @Inject constructor(
private val initialState: TesterActionsContentState
get() = TesterActionsContentState(
onBackClick = { /* no-op */ },
hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this::hideAllCurrencies),
toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT, this::toggleAppTheme),
onBackClick = { /* no-op */ },
onApplyChangesClick = { /* no-op */ },
)
init {

View file

@ -8,9 +8,11 @@ import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatur
* @property featureToggles feature toggles list
* @property onBackClick the lambda to be invoked when back button is pressed
* @property onToggleValueChange the lambda to be invoked when switch button is pressed
* @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed
*/
internal data class FeatureTogglesContentState(
val featureToggles: List<TesterFeatureToggle>,
val onBackClick: () -> Unit,
val onToggleValueChange: (String, Boolean) -> Unit,
val onBackClick: () -> Unit,
val onApplyChangesClick: () -> Unit,
)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tester.presentation.featuretoggles.ui
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -18,8 +19,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
@ -49,6 +52,14 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) },
)
}
item {
PrimaryButton(
text = stringResource(id = R.string.apply_changes),
onClick = state.onApplyChangesClick,
modifier = Modifier.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
)
}
}
}
@ -81,26 +92,10 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewFeatureTogglesScreen_InLightTheme() {
TangemTheme(isDark = false) {
FeatureTogglesScreen(
state = FeatureTogglesContentState(
featureToggles = listOf(
TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true),
TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false),
),
onToggleValueChange = { _, _ -> },
onBackClick = {},
),
)
}
}
@Preview
@Composable
private fun PreviewFeatureTogglesScreen_InDarkTheme() {
TangemTheme(isDark = true) {
private fun PreviewFeatureTogglesScreen() {
TangemThemePreview {
FeatureTogglesScreen(
state = FeatureTogglesContentState(
featureToggles = listOf(
@ -109,6 +104,7 @@ private fun PreviewFeatureTogglesScreen_InDarkTheme() {
),
onToggleValueChange = { _, _ -> },
onBackClick = {},
onApplyChangesClick = {},
),
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.features.tester.api.AppRestarter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
@ -38,16 +39,20 @@ internal class FeatureTogglesViewModel @Inject constructor(
"Feature toggle manager must be mutable (debug build type)"
}
/** Setup navigation state property by router [router] */
fun setupNavigation(router: InnerTesterRouter) {
uiState = uiState.copy(onBackClick = router::back)
/** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */
fun setupInteractions(router: InnerTesterRouter, appRestarter: AppRestarter) {
uiState = uiState.copy(
onBackClick = router::back,
onApplyChangesClick = appRestarter::restart,
)
}
private fun initState(): FeatureTogglesContentState {
return FeatureTogglesContentState(
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
onBackClick = {},
onToggleValueChange = ::onToggleValueChange,
onBackClick = {},
onApplyChangesClick = {},
)
}

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