diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 6e5f3a779f..40b4c0c73c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -366,7 +366,10 @@ class TradeCryptoMiddleware { ) } } - val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } @@ -416,7 +419,10 @@ class TradeCryptoMiddleware { is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") } - val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 2350350cd0..d8e209f0ad 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -12,7 +12,9 @@ object BigDecimalFormatter { private const val TEMP_CURRENCY_CODE = "USD" - fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String { + fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: String, decimals: Int): String { + if (cryptoAmount == null) return EMPTY_BALANCE_SIGN + val formatter = NumberFormat.getNumberInstance().apply { maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) minimumFractionDigits = 2 @@ -23,11 +25,13 @@ object BigDecimalFormatter { } fun formatFiatAmount( - fiatAmount: BigDecimal, + fiatAmount: BigDecimal?, fiatCurrencyCode: String, fiatCurrencySymbol: String, locale: Locale = Locale.getDefault(), ): String { + if (fiatAmount == null) return EMPTY_BALANCE_SIGN + val formatterCurrency = getCurrency(fiatCurrencyCode) val formatter = NumberFormat.getCurrencyInstance(locale).apply { currency = formatterCurrency diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt index 692027bfe8..5a44a6614d 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt @@ -8,5 +8,6 @@ interface SendRouter { companion object { const val CRYPTO_CURRENCY_KEY = "send_crypto_currency" + const val USER_WALLET_ID_KEY = "send_user_wallet_id" } } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index c0cfcb3894..9c3a10e456 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.material) implementation(deps.arrow.core) + implementation(deps.tangem.card.core) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -38,6 +39,7 @@ dependencies { implementation(projects.core.utils) /** Domain modules */ + implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 334bdc5aeb..d33b82b860 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -4,10 +4,14 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.screen.ComposeBottomSheetFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.features.send.impl.presentation.send.state.SendUiState import com.tangem.features.send.impl.presentation.send.ui.SendScreen +import com.tangem.features.send.impl.presentation.send.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -24,13 +28,16 @@ internal class SendFragment : ComposeBottomSheetFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - SystemBarsEffect { - setSystemBarsColor(color = Color.Transparent) + val viewModel = hiltViewModel() + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + + SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) } + BackHandler { dismiss() } + + when (val state = viewModel.uiState) { + is SendUiState.Content -> SendScreen(state) + SendUiState.Dismiss -> dismiss() } - BackHandler { - dismiss() - } - SendScreen() } companion object { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt new file mode 100644 index 0000000000..c1a17d6011 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.impl.presentation.send.state + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class SendAmountStateConverter( + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val userWalletProvider: Provider, + private val clickIntents: SendClickIntents, + private val iconStateConverter: CryptoCurrencyToIconStateConverter, + private val sendAmountFieldConverter: SendAmountFieldConverter, +) : Converter, SendUiState> { + + override fun convert(value: Either): SendUiState { + val userWallet = userWalletProvider() ?: return currentStateProvider() + val appCurrency = appCurrencyProvider() + return value.fold( + ifLeft = { + // TODO add error handling + currentStateProvider() + }, + ifRight = { + val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol) + val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals) + SendUiState.Content.AmountState( + cryptoCurrencyStatus = it, + walletName = userWallet.name, + walletBalance = "$crypto ($fiat)", + tokenIconState = iconStateConverter.convert(it), + appCurrency = appCurrency, + amountTextField = sendAmountFieldConverter.convert(Unit), + isFiatValue = false, + clickIntents = clickIntents, + segmentedButtonConfig = persistentListOf( + SendAmountSegmentedButtonsConfig( + title = stringReference(it.currency.symbol), + iconState = iconStateConverter.convert(it), + isFiat = false, + ), + SendAmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconState = iconStateConverter.convert(it), + isFiat = true, + ), + ), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt new file mode 100644 index 0000000000..bfca56ad1d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.features.send.impl.presentation.send.state + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldChangeConverter +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents + +internal class SendStateFactory( + private val clickIntents: SendClickIntents, + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val userWalletProvider: Provider, +) { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } + + private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } + + private val amountStateConverter by lazy { + SendAmountStateConverter( + currentStateProvider = currentStateProvider, + appCurrencyProvider = appCurrencyProvider, + clickIntents = clickIntents, + iconStateConverter = iconStateConverter, + userWalletProvider = userWalletProvider, + sendAmountFieldConverter = amountFieldConverter, + ) + } + + fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + + fun getAmountState(cryptoCurrencyStatus: Either): SendUiState { + return amountStateConverter.convert(cryptoCurrencyStatus) + } + + fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + + fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) + + fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState { + val state = currentStateProvider() + val amountState = state as? SendUiState.Content.AmountState ?: return state + + return if (amountState.isFiatValue == isFiat) { + state + } else { + return state.copy(isFiatValue = isFiat) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt index 10df3f1966..0c48ca14b0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList /** @@ -16,42 +17,54 @@ internal sealed class SendUiState { /** States with content */ sealed class Content : SendUiState() { - abstract val nextButtonEnabled: Boolean + /** Is primary button enabled */ + abstract val isPrimaryButtonEnabled: Boolean + + /** Click intents */ + abstract val clickIntents: SendClickIntents /** Initial state */ data class Initial( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, ) : Content() /** Amount state */ data class AmountState( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, val walletName: String, val walletBalance: String, val tokenIconState: TokenIconState, val cryptoCurrencyStatus: CryptoCurrencyStatus, val appCurrency: AppCurrency, val isFiatValue: Boolean, - val segmentedButtonConfig: PersistentList, + val segmentedButtonConfig: PersistentList, val amountTextField: SendTextField.Amount, ) : Content() // todo [REDACTED_JIRA] /** Recipient state */ data class RecipientState( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, ) : Content() // todo [REDACTED_JIRA] /** Fee and speed state */ data class FeeState( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, ) : Content() // todo [REDACTED_JIRA] /** Send state */ data class SendState( - override val nextButtonEnabled: Boolean = true, + override val isPrimaryButtonEnabled: Boolean = true, + override val clickIntents: SendClickIntents, ) : Content() } + + /** Dismiss screen */ + object Dismiss : SendUiState() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt new file mode 100644 index 0000000000..0775989341 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt @@ -0,0 +1,97 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import com.tangem.common.Provider +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.utils.converter.Converter +import java.text.DecimalFormatSymbols +import java.text.NumberFormat + +internal class SendAmountFieldChangeConverter( + private val currentStateProvider: Provider, +) : Converter { + override fun convert(value: String): SendUiState { + val state = currentStateProvider() + + if ( + state !is SendUiState.Content.AmountState || + value.checkDecimalSeparatorDuplicate() + ) { + return state + } + + if (value.isEmpty()) return state.emptyState() + + val fiatRate = state.cryptoCurrencyStatus.value.fiatRate + + val trimmedValue = value.trim() + + val cryptoValue = if (state.isFiatValue) { + if (value.isNotBlank()) { + trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() + } else { + DEFAULT_VALUE + } + } else { + trimmedValue + } + + val fiatValue = if (!state.isFiatValue) { + if (value.isNotBlank()) { + trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() + } else { + DEFAULT_VALUE + } + } else { + trimmedValue + } + + val isExceedBalance = value.checkExceedBalance(state) + return state.copy( + amountTextField = state.amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + ), + isPrimaryButtonEnabled = !isExceedBalance, + ) + } + + private fun SendUiState.Content.AmountState.emptyState(): SendUiState { + return copy( + amountTextField = amountTextField.copy( + value = if (!isFiatValue) "" else DEFAULT_VALUE, + fiatValue = if (isFiatValue) "" else DEFAULT_VALUE, + isError = false, + ), + isPrimaryButtonEnabled = false, + ) + } + + private fun String.checkDecimalSeparatorDuplicate(): Boolean { + val regex = "[\\.\\,]".toRegex() + val decimalSeparatorCount = regex.findAll(this).count() + + return decimalSeparatorCount > 1 + } + + private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean { + val currencyStatus = state.cryptoCurrencyStatus.value + return if (state.isFiatValue) { + toBigDecimal() > currencyStatus.fiatAmount + } else { + toBigDecimal() > currencyStatus.amount + } + } + + private fun String.trim(): String { + var trimmedValue = this + if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1) + + val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString() + return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar) + } + + companion object { + private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt new file mode 100644 index 0000000000..8f09a10a04 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import java.text.NumberFormat + +internal class SendAmountFieldConverter( + private val clickIntents: SendClickIntents, +) : Converter { + + override fun convert(value: Unit): SendTextField.Amount { + return SendTextField.Amount( + value = "", + fiatValue = DEFAULT_VALUE, + onValueChange = clickIntents::onAmountValueChange, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + label = TextReference.Str(""), + placeholder = TextReference.Str(DEFAULT_VALUE), + isError = false, + error = TextReference.Res(R.string.send_insufficient_funds), + ) + } + + companion object { + private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt new file mode 100644 index 0000000000..8f6dbcabc9 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt @@ -0,0 +1,109 @@ +package com.tangem.features.send.impl.presentation.send.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.currency.fiaticon.FiatIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.send.state.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.features.send.impl.presentation.send.ui.amount.AmountFieldContainer + +@Composable +internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + Text( + text = stringResource(R.string.common_send), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .align(CenterHorizontally), + ) + AmountFieldContainer(amountState = amountState) + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) { + SegmentedButtons( + modifier = Modifier + .height(TangemTheme.dimens.size40) + .weight(1f), + config = amountState.segmentedButtonConfig, + onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) }, + ) { + SendAmountCurrencyButton(it) + } + SecondaryButton( + text = stringResource(R.string.send_max_amount), + onClick = amountState.clickIntents::onMaxValueClick, + size = TangemButtonSize.Text, + shape = RoundedCornerShape(TangemTheme.dimens.radius26), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .height(TangemTheme.dimens.size40), + ) + } + } +} + +@Composable +private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) { + Row( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = TangemTheme.dimens.spacing10, + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (button.isFiat) { + FiatIcon( + url = button.iconUrl, + modifier = Modifier + .size(TangemTheme.dimens.size18), + ) + } else { + button.iconState?.let { + TokenIcon( + state = it, + shouldDisplayNetwork = false, + modifier = Modifier + .size(TangemTheme.dimens.size18), + ) + } + } + Text( + text = button.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt index 9604a47dfd..cf453d487a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt @@ -76,7 +76,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: PrimaryButtonIconEnd( text = stringResource(textId), iconResId = R.drawable.ic_tangem_24, - enabled = uiState.nextButtonEnabled, + enabled = uiState.isPrimaryButtonEnabled, onClick = { // todo add next click }, @@ -84,7 +84,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: } else { PrimaryButton( text = stringResource(textId), - enabled = uiState.nextButtonEnabled, + enabled = uiState.isPrimaryButtonEnabled, onClick = { // todo add next click }, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt index 61351bd875..1e52cf291a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.send.state.SendUiState @Composable -fun SendScreen() { +internal fun SendScreen(uiState: SendUiState.Content) { Column( modifier = Modifier .imePadding() @@ -37,13 +37,17 @@ fun SendScreen() { .weight(1f) .scrollable(state = rememberScrollState(), orientation = Orientation.Vertical), ) { - SendScreenContent() + SendScreenContent(uiState) } - SendNavigationButtons(uiState = SendUiState.Content.Initial()) + SendNavigationButtons(uiState) } } @Composable -fun SendScreenContent() { - // todo work in progress +private fun SendScreenContent(uiState: SendUiState.Content) { + when (uiState) { + is SendUiState.Content.AmountState -> SendAmountContent(uiState) + else -> { /* [REDACTED_TODO_COMMENT]*/ + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt new file mode 100644 index 0000000000..3e6885543a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.features.send.impl.presentation.send.viewmodel + +interface SendClickIntents { + + fun onNextClick() + + fun onPrevClick() + + fun onAmountValueChange(value: String) + + fun onCurrencyChangeClick(isFiat: Boolean) + + fun onMaxValueClick() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt new file mode 100644 index 0000000000..5bc0189731 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt @@ -0,0 +1,155 @@ +package com.tangem.features.send.impl.presentation.send.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.* +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.presentation.send.state.SendStateFactory +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class SendViewModel @Inject constructor( + private val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { + + private val userWalletId: UserWalletId = savedStateHandle.get(SendRouter.USER_WALLET_ID_KEY) + ?.let { stringValue -> UserWalletId(stringValue) } + ?: error("This screen can't open without `UserWalletId`") + + private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + ?: error("This screen can't open without `CryptoCurrency`") + + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + + private val stateFactory = SendStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + ) + + var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) + private set + + private var userWallet: UserWallet? = null + + private var balanceJobHolder = JobHolder() + + override fun onCreate(owner: LifecycleOwner) { + subscribeOnCurrencyStatusUpdates(owner) + } + + private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { + viewModelScope.launch(dispatchers.io) { + getUserWalletUseCase(userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + getCurrencyStatusUpdates(owner, wallet) + }, + ifLeft = { + // TODO add error handling + return@launch + }, + ) + } + } + + private fun getCurrencyStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) { + val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = isSingleWallet, + ) + .flowWithLifecycle(owner.lifecycle) + .conflate() + .distinctUntilChanged() + .onEach { either -> + uiState = stateFactory.getAmountState( + cryptoCurrencyStatus = either, + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(balanceJobHolder) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + // region screen state navigation + override fun onNextClick() { + when (uiState) { + is SendUiState.Content.AmountState -> onRecipientStateClick() + is SendUiState.Content.RecipientState -> onFeeStateClick() + else -> { + // todo implement + } + } + } + + override fun onPrevClick() { + // todo implement + } + + private fun onRecipientStateClick() { + stateFactory.getOnReceiveState() + } + + private fun onFeeStateClick() { + // todo implement + } + // endregion + + // region amount state clicks + override fun onCurrencyChangeClick(isFiat: Boolean) { + uiState = stateFactory.getOnCurrencyChangedState(isFiat) + } + + override fun onAmountValueChange(value: String) { + uiState = stateFactory.getOnAmountValueChange(value) + } + + override fun onMaxValueClick() { + val amountState = uiState as? SendUiState.Content.AmountState ?: return + + val amount = if (amountState.isFiatValue) { + amountState.cryptoCurrencyStatus.value.fiatAmount + } else { + amountState.cryptoCurrencyStatus.value.amount + } + onAmountValueChange(amount?.toPlainString() ?: "0.00") + } + // endregion +} \ No newline at end of file