From 8a948333fefc268e3aac95fd64166ca40b134099 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Oct 2023 15:29:33 +0400 Subject: [PATCH 01/11] Updated on 2026-08-14 --- .../managetokens/state/SearchBarState.kt | 11 ++ .../managetokens/ui/components/SearchBar.kt | 143 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt new file mode 100644 index 0000000000..d64808239e --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt @@ -0,0 +1,11 @@ +package com.tangem.managetokens.presentation.managetokens.state + +/** + * SearchBar state. + */ +internal data class SearchBarState( + val query: String, + val onQueryChange: (String) -> Unit, + val active: Boolean, + val onActiveChange: (Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt new file mode 100644 index 0000000000..6040c96260 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt @@ -0,0 +1,143 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +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.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.SearchBarState + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun TokensSearchBar(state: SearchBarState, modifier: Modifier = Modifier) { + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + TextField( + value = state.query, + onValueChange = state.onQueryChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), + keyboardActions = KeyboardActions( + onSearch = { + keyboardController?.hide() + focusManager.clearFocus() + }, + ), + singleLine = true, + maxLines = 1, + textStyle = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.primary1, + ), + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_search_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier.clickable { state.onActiveChange(true) }, + ) + }, + trailingIcon = { + if (state.query.isNotEmpty() || state.active) { + IconButton( + onClick = { + if (state.query.isNotEmpty()) { + state.onQueryChange("") + } + focusManager.clearFocus() + keyboardController?.hide() + state.onActiveChange(false) + }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } + }, + placeholder = { + Text( + text = stringResource(R.string.manage_tokens_search_placeholder), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) + }, + shape = RoundedCornerShape(TangemTheme.dimens.radius36), + colors = searchbarTextFieldColors(), + modifier = modifier + .fillMaxWidth() + .onFocusChanged { + if (it.isFocused) { + state.onActiveChange(true) + } else { + state.onActiveChange(false) + } + }, + ) +} + +@Composable +private fun searchbarTextFieldColors(): TextFieldColors { + return TextFieldDefaults.textFieldColors( + backgroundColor = TangemTheme.colors.field.primary, + textColor = TangemTheme.colors.text.primary1, + cursorColor = TangemTheme.colors.icon.primary1, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) +} + +@Preview +@Composable +private fun Preview_TokensSearchBar_Light( + @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) { + TokensSearchBar(state) + } +} + +private class SearchBarkConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + SearchBarState( + query = "BTC", + onQueryChange = {}, + active = true, + onActiveChange = {}, + ), + SearchBarState( + query = "", + onQueryChange = {}, + active = false, + onActiveChange = {}, + ), + ), +) \ No newline at end of file From 96f5aa8238a5e57af1100a4d4913014affdbb8a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Nov 2023 21:52:01 +0300 Subject: [PATCH 02/11] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 2 +- core/ui/build.gradle.kts | 1 + .../appbar/AppBarWithBackButtonAndIcon.kt | 103 +++++++++ .../fields/AmountVisualTransformation.kt | 32 +++ .../core/ui/utils/DateTimeFormatters.kt | 13 +- .../com/tangem/core/ui/utils/DateUtils.kt | 33 +++ .../main/res/drawable/ic_qrcode_scan_24.xml | 13 ++ .../DefaultWalletManagersFacade.kt | 5 +- .../walletmanager/WalletManagersFacade.kt | 7 + features/send/impl/build.gradle.kts | 13 +- .../send/impl/presentation/SendFragment.kt | 44 ++-- .../presentation/domain/AvailableWallet.kt | 15 ++ .../domain/SendRecipientListContent.kt | 21 ++ .../presentation/state/SendStateFactory.kt | 133 ++++++++++-- .../impl/presentation/state/SendUiState.kt | 102 ++++----- .../impl/presentation/state/StateRouter.kt | 39 ++++ .../state/amount/SendAmountStateConverter.kt | 73 +++---- .../fields/SendAmountFieldChangeConverter.kt | 60 +++--- .../state/fields/SendAmountFieldConverter.kt | 1 - .../state/fields/SendTextField.kt | 37 +++- .../SendRecipientAddressFieldConverter.kt | 31 +++ .../recipient/SendRecipientListConverter.kt | 113 ++++++++++ .../SendRecipientMemoFieldConverter.kt | 51 +++++ .../recipient/SendRecipientStateConverter.kt | 30 +++ .../presentation/ui/SendNavigationButtons.kt | 48 +++-- .../send/impl/presentation/ui/SendScreen.kt | 96 ++++++--- .../presentation/ui/amount/AmountField.kt | 31 +-- .../ui/amount/AmountFieldContainer.kt | 10 +- .../ui/{ => amount}/SendAmountContent.kt | 29 +-- .../presentation/ui/common/FooterContainer.kt | 38 ++++ .../ui/recipient/ListItemWithIcon.kt | 135 +++++++----- .../ui/recipient/SendRecipientContent.kt | 199 ++++++++++++++++++ .../presentation/ui/recipient/TextFields.kt | 50 +++-- .../viewmodel/AddressVerification.kt | 18 ++ .../viewmodel/MemoVerification.kt | 46 ++++ .../viewmodel/SendClickIntents.kt | 12 ++ .../presentation/viewmodel/SendViewModel.kt | 167 +++++++++++++-- .../TokenDetailsTxHistoryItemFlowConverter.kt | 32 +-- gradle/dependencies.toml | 4 +- 40 files changed, 1503 insertions(+), 388 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt create mode 100644 core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt rename features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/{ => amount}/SendAmountContent.kt (75%) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2af2ae2c68..52aa5dfca4 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -395,7 +395,7 @@ Сумма к получению %1$s Адрес Код назначения - %1$s в %2$s + %1$s в %2$s Введите адрес Адрес совпадает с адресом кошелька Недопустимый Tag. Он не будет добавлен в транзакцию. @@ -427,7 +427,7 @@ Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению. Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. Сумма резерва не может быть менее %1$s. - Пожалуйста, пополните свой баланс, чтобы продолжить. + Пожалуйста, пополните свой баланс, чтобы продолжить. Увеличение комиссии Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. Комиссия превышает баланс diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 020371fd62..09f3ec83d1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -395,7 +395,7 @@ The recipient will receive %1$s Address Destination Tag - %1$s at %2$s + %1$s at %2$s Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 5868a39017..ab9b42cd1f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { /** Project - Core */ implementation(projects.core.res) + implementation(projects.core.utils) /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt new file mode 100644 index 0000000000..73509da21f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -0,0 +1,103 @@ +package com.tangem.core.ui.components.appbar + +import androidx.annotation.DrawableRes +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun AppBarWithBackButtonAndIcon( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + text: String? = null, + @DrawableRes backIconRes: Int? = null, + @DrawableRes iconRes: Int? = null, + onIconClick: (() -> Unit)? = null, + backgroundColor: Color = TangemTheme.colors.background.secondary, +) { + Row( + modifier = modifier + .background(color = backgroundColor) + .fillMaxWidth() + .padding(all = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(backIconRes ?: R.drawable.ic_back_24), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size24) + .clickable { onBackClick() }, + tint = TangemTheme.colors.icon.primary1, + ) + AnimatedContent( + targetState = text, + modifier = Modifier.weight(1f), + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + label = "Toolbar title change", + ) { + if (!it.isNullOrBlank()) { + Text( + text = it, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.subtitle1, + ) + } + } + AnimatedContent( + targetState = iconRes, + transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) }, + label = "Toolbar icon change", + ) { + if (onIconClick != null && it != null) { + Icon( + painter = painterResource(it), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size24) + .clickable { onIconClick() }, + tint = TangemTheme.colors.icon.primary1, + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +private fun PreviewAppBarWithBackButtonAndIconInLightTheme() { + TangemTheme(isDark = false) { + AppBarWithBackButtonAndIcon( + text = "Title", + iconRes = R.drawable.ic_qrcode_scan_24, + onBackClick = {}, + onIconClick = {}, + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { + TangemTheme(isDark = true) { + AppBarWithBackButtonAndIcon( + text = "Title", + iconRes = R.drawable.ic_qrcode_scan_24, + onBackClick = {}, + onIconClick = {}, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt new file mode 100644 index 0000000000..89e4fa09ce --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt @@ -0,0 +1,32 @@ +package com.tangem.core.ui.components.fields + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +class AmountVisualTransformation( + private val symbol: String, +) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return TransformedText( + buildAnnotatedString { + append(text) + if (text.isNotBlank()) { + append(" ") + append(symbol) + } + }, + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return text.length + } + + override fun transformedToOriginal(offset: Int): Int { + return text.length + } + }, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 80e9488f0a..dc27c1641a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -9,12 +9,14 @@ import java.util.Locale @Suppress("MagicNumber") object DateTimeFormatters { + private const val DDMMYYYY = "dd.MM.yyyy" + /** * Two SS means, SHORT style for date and time. * If pattern contains "a", it means time is in 12 hour format. * [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) */ - val timeFormatter by lazy { + val timeFormatter: DateTimeFormatter by lazy { val is12HourFormat = DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a") if (is12HourFormat) { DateTimeFormatterBuilder() @@ -35,7 +37,7 @@ object DateTimeFormatters { } } - val dateFormatter by lazy { + val dateFormatter: DateTimeFormatter by lazy { DateTimeFormatterBuilder() .appendDayOfMonth(1) .appendLiteral(' ') @@ -46,6 +48,13 @@ object DateTimeFormatters { .withLocale(Locale.getDefault()) } + val dateDDMMYYYY: DateTimeFormatter by lazy { + DateTimeFormatterBuilder() + .appendPattern(DDMMYYYY) + .toFormatter() + .withLocale(Locale.getDefault()) + } + fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String { return formatter.print(time) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt new file mode 100644 index 0000000000..b0237e897a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.utils + +import android.text.format.DateUtils +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatter + +/** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date. + */ +fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + DateTimeFormatters.formatDate(formatter = formatter, date = localDate) + } +} + +/** + * Returns formatted time according to [formatter]. + */ +fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String { + return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault())) +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml b/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml new file mode 100644 index 0000000000..7cde4123e0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index f9bc744566..bc1bf00b14 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -323,6 +323,10 @@ class DefaultWalletManagersFacade( } override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ + return getAddresses(userWalletId, network).sortedBy { it.type } + } + + override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
{ val blockchain = Blockchain.fromId(network.id.value) return getOrCreateWalletManager( @@ -332,7 +336,6 @@ class DefaultWalletManagersFacade( ) ?.wallet ?.addresses - ?.sortedBy { it.type } .orEmpty() } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index e4817280a7..31345d6f99 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -114,6 +114,13 @@ interface WalletManagersFacade { */ suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
+ /** Returns list of all addresses for all currencies in selected wallet + * + * @param userWalletId selected wallet id + * @param network required to create wallet manager + */ + suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
+ /** * Returns info about rent if wallet manager implemented [RentProvider], otherwise null * diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 9c3a10e456..65f362206d 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -14,12 +14,14 @@ 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.tangem.card.core) + implementation(deps.lifecycle.compose) + implementation(deps.jodatime) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -29,6 +31,12 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) + implementation(deps.compose.constraintLayout) + + /** Tangem SDKs */ + implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) /** Common */ implementation(projects.common) @@ -40,12 +48,15 @@ dependencies { /** Domain modules */ implementation(projects.domain.models) + implementation(projects.domain.legacy) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) /** Feature modules */ implementation(projects.features.send.api) 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 61136054f5..2790caab93 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 @@ -1,43 +1,53 @@ package com.tangem.features.send.impl.presentation -import androidx.activity.compose.BackHandler +import android.os.Bundle 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 androidx.fragment.app.viewModels import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.screen.ComposeBottomSheetFragment +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.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint +import java.lang.ref.WeakReference import javax.inject.Inject /** * Send fragment */ @AndroidEntryPoint -internal class SendFragment : ComposeBottomSheetFragment() { +internal class SendFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder - override val expandedHeightFraction: Float = 1f + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + viewModel.setRouter( + StateRouter( + fragmentManager = WeakReference(parentFragmentManager), + ), + ) + } @Composable override fun ScreenContent(modifier: Modifier) { - 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() + val systemBarsColor = TangemTheme.colors.background.tertiary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) } + SendScreen(viewModel.uiState) + } + + override fun onDestroy() { + lifecycle.removeObserver(viewModel) + super.onDestroy() } companion object { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt new file mode 100644 index 0000000000..69e56bfc62 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt @@ -0,0 +1,15 @@ +package com.tangem.features.send.impl.presentation.domain + +import androidx.compose.runtime.Immutable + +/** + * Available wallet to send + * + * @property name wallet name + * @property address blockchain address + */ +@Immutable +data class AvailableWallet( + val name: String, + val address: String, +) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt new file mode 100644 index 0000000000..6cb3516497 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.impl.presentation.domain + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal sealed class SendRecipientListContent { + data class Item( + val id: String, + val title: TextReference, + val subtitle: TextReference, + val info: TextReference? = null, + @DrawableRes val subtitleIconRes: Int? = null, + ) : SendRecipientListContent() + + data class Wallets( + val list: PersistentList, + ) : SendRecipientListContent() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index d60ad8e28b..87ac690720 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -1,22 +1,35 @@ package com.tangem.features.send.impl.presentation.state -import arrow.core.Either +import androidx.paging.PagingData +import com.tangem.blockchain.common.address.Address import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference 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.txhistory.models.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet +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.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter +import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet +import com.tangem.features.send.impl.presentation.viewmodel.validateMemo +import com.tangem.features.send.impl.presentation.viewmodel.verifyAddress +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update internal class SendStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, + private val userWalletProvider: Provider, + private val walletAddressesProvider: Provider>, private val appCurrencyProvider: Provider, - private val userWalletProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -27,33 +40,129 @@ internal class SendStateFactory( private val amountStateConverter by lazy { SendAmountStateConverter( - currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - clickIntents = clickIntents, iconStateConverter = iconStateConverter, userWalletProvider = userWalletProvider, sendAmountFieldConverter = amountFieldConverter, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) - - fun getAmountState(cryptoCurrencyStatus: Either): SendUiState { - return amountStateConverter.convert(cryptoCurrencyStatus) + private val recipientStateConverter by lazy { + SendRecipientStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) } - fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + private val recipientListStateConverter by lazy { + SendRecipientListConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + // region UI states + fun getInitialState(): SendUiState = SendUiState( + clickIntents = clickIntents, + currentState = MutableStateFlow(SendUiStateType.Amount), + ) + + fun getReadyState(): SendUiState = currentStateProvider().copy( + amountState = amountStateConverter.convert(Unit), + recipientState = recipientStateConverter.convert(Unit), + feeState = SendStates.FeeState(), + ) + //endregion + + //region amount state clicks fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState { val state = currentStateProvider() - val amountState = state as? SendUiState.Content.AmountState ?: return state + val amountState = state.amountState ?: return state return if (amountState.isFiatValue == isFiat) { state } else { - return state.copy(isFiatValue = isFiat) + return state.copy(amountState = amountState.copy(isFiatValue = isFiat)) } } + //endregion + + //region recipient + fun onLoadedRecipientList(wallets: List, txHistory: PagingData) { + recipientListStateConverter.convert( + wallets = wallets, + txHistory = txHistory, + ) + } + + fun getOnRecipientAddressValueChangeState(value: String): SendUiState { + val state = currentStateProvider() + val recipientState = state.recipientState ?: return state + + val isValidMemo = validateMemo( + memo = value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + val isAddressInWallet = isNotAddressInWallet( + walletAddresses = walletAddressesProvider(), + address = recipientState.addressTextField.value.value, + ) + val isValidAddress = verifyAddress( + address = recipientState.addressTextField.value.value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + + recipientState.addressTextField.update { + it.copy( + value = value, + error = when { + !isValidAddress -> TextReference.Res(R.string.send_recipient_address_error) + !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) + else -> null + }, + isError = !isValidAddress || !isAddressInWallet, + ) + } + return state.copy( + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet, + ), + ) + } + + fun getOnRecipientMemoValueChangeState(value: String): SendUiState { + val state = currentStateProvider() + val recipientState = state.recipientState ?: return state + + val isValidMemo = validateMemo( + memo = value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + val isAddressInWallet = isNotAddressInWallet( + walletAddresses = walletAddressesProvider(), + address = recipientState.addressTextField.value.value, + ) + val isValidAddress = verifyAddress( + address = recipientState.addressTextField.value.value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + + // todo add memo validation error text + recipientState.memoTextField?.update { + it.copy( + value = value, + error = TextReference.Res(R.string.send_memo_destination_tag_error), + isError = !isValidMemo, + ) + } + return state.copy( + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet, + ), + ) + } + //endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index f4c9014b85..6baeb27cfb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -1,71 +1,77 @@ package com.tangem.features.send.impl.presentation.state import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.paging.PagingData 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.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList +import kotlinx.coroutines.flow.MutableStateFlow /** * Ui states of the send screen */ @Immutable -internal sealed class SendUiState { +internal data class SendUiState( + val clickIntents: SendClickIntents, + val amountState: SendStates.AmountState? = null, + val recipientState: SendStates.RecipientState? = null, + val feeState: SendStates.FeeState? = null, + val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), + val currentState: MutableStateFlow, +) - /** States with content */ - sealed class Content : SendUiState() { +@Stable +internal sealed class SendStates { - /** Is primary button enabled */ - abstract val isPrimaryButtonEnabled: Boolean + abstract val type: SendUiStateType - /** Click intents */ - abstract val clickIntents: SendClickIntents + /** Amount state */ + data class AmountState( + override val type: SendUiStateType = SendUiStateType.Amount, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrency: AppCurrency, + val walletName: String, + val walletBalance: String, + val tokenIconState: TokenIconState, + val isFiatValue: Boolean, + val segmentedButtonConfig: PersistentList, + val amountTextField: MutableStateFlow, + val isPrimaryButtonEnabled: Boolean, + ) : SendStates() - /** Initial state */ - data class Initial( - override val isPrimaryButtonEnabled: Boolean = false, - override val clickIntents: SendClickIntents, - ) : Content() + /** Recipient state */ + data class RecipientState( + override val type: SendUiStateType = SendUiStateType.Recipient, + val addressTextField: MutableStateFlow, + val memoTextField: MutableStateFlow?, + val recipients: MutableStateFlow> = MutableStateFlow(PagingData.empty()), + val network: String, + val isPrimaryButtonEnabled: Boolean, + ) : SendStates() - /** Amount state */ - data class AmountState( - 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 amountTextField: SendTextField.Amount, - ) : Content() + // todo [REDACTED_JIRA] + /** Fee and speed state */ + data class FeeState( + override val type: SendUiStateType = SendUiStateType.Fee, + ) : SendStates() - // todo [REDACTED_JIRA] - /** Recipient state */ - data class RecipientState( - override val isPrimaryButtonEnabled: Boolean = false, - override val clickIntents: SendClickIntents, - ) : Content() + // todo [REDACTED_JIRA] + /** Send state */ + data class SendState( + val isSuccess: Boolean, + ) +} - // todo [REDACTED_JIRA] - /** Fee and speed state */ - data class FeeState( - override val isPrimaryButtonEnabled: Boolean = false, - override val clickIntents: SendClickIntents, - ) : Content() - - // todo [REDACTED_JIRA] - /** Send state */ - data class SendState( - override val isPrimaryButtonEnabled: Boolean = true, - override val clickIntents: SendClickIntents, - ) : Content() - } - - /** Dismiss screen */ - object Dismiss : SendUiState() +enum class SendUiStateType { + Amount, + Recipient, + Fee, + Send, + Done, } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt new file mode 100644 index 0000000000..e7526fdf04 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -0,0 +1,39 @@ +package com.tangem.features.send.impl.presentation.state + +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import java.lang.ref.WeakReference + +internal class StateRouter( + private val fragmentManager: WeakReference, +) { + var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Amount) + + fun onBackClick() { + fragmentManager.get()?.popBackStack() + } + + fun onNextClick() { + when (currentState.value) { + SendUiStateType.Amount -> { + currentState.update { SendUiStateType.Recipient } + } + SendUiStateType.Recipient -> { + currentState.update { SendUiStateType.Fee } + } + else -> { + // todo implement + } + } + } + + fun onPrevClick() { + when (currentState.value) { + SendUiStateType.Amount -> onBackClick() + SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } + SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } + else -> onBackClick() + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index 3545aeccc6..346cd114aa 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -1,64 +1,55 @@ package com.tangem.features.send.impl.presentation.state.amount -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.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow internal class SendAmountStateConverter( - private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val userWalletProvider: Provider, - private val clickIntents: SendClickIntents, + private val userWalletProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, private val iconStateConverter: CryptoCurrencyToIconStateConverter, private val sendAmountFieldConverter: SendAmountFieldConverter, -) : Converter, SendUiState> { +) : Converter { - override fun convert(value: Either): SendUiState { - val userWallet = userWalletProvider() ?: return currentStateProvider() + override fun convert(value: Unit): SendStates.AmountState { + val userWallet = userWalletProvider() 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, - ), - ), - ) - }, + val status = cryptoCurrencyStatusProvider() + val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol) + val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) + + return SendStates.AmountState( + appCurrency = appCurrency, + cryptoCurrencyStatus = status, + walletName = userWallet.name, + walletBalance = "$crypto ($fiat)", + tokenIconState = iconStateConverter.convert(status), + amountTextField = MutableStateFlow(sendAmountFieldConverter.convert(Unit)), + isFiatValue = false, + isPrimaryButtonEnabled = false, + segmentedButtonConfig = persistentListOf( + SendAmountSegmentedButtonsConfig( + title = stringReference(status.currency.symbol), + iconState = iconStateConverter.convert(status), + isFiat = false, + ), + SendAmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconState = iconStateConverter.convert(status), + isFiat = true, + ), + ), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 5c91a7489e..cacb180097 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -1,8 +1,11 @@ package com.tangem.features.send.impl.presentation.state.fields import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.update import java.text.DecimalFormatSymbols import java.text.NumberFormat @@ -11,21 +14,14 @@ internal class SendAmountFieldChangeConverter( ) : Converter { override fun convert(value: String): SendUiState { val state = currentStateProvider() + val amountState = state.amountState ?: return state - if ( - state !is SendUiState.Content.AmountState || - value.checkDecimalSeparatorDuplicate() - ) { - return state - } - + if (value.checkDecimalSeparatorDuplicate()) return state if (value.isEmpty()) return state.emptyState() - val fiatRate = state.cryptoCurrencyStatus.value.fiatRate - + val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate val trimmedValue = value.trim() - - val cryptoValue = if (state.isFiatValue) { + val cryptoValue = if (amountState.isFiatValue) { if (value.isNotBlank()) { trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() } else { @@ -35,7 +31,7 @@ internal class SendAmountFieldChangeConverter( trimmedValue } - val fiatValue = if (!state.isFiatValue) { + val fiatValue = if (!amountState.isFiatValue) { if (value.isNotBlank()) { trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() } else { @@ -45,37 +41,48 @@ internal class SendAmountFieldChangeConverter( trimmedValue } - val isExceedBalance = value.checkExceedBalance(state) - return state.copy( - amountTextField = state.amountTextField.copy( + val isExceedBalance = value.checkExceedBalance(amountState.cryptoCurrencyStatus, amountState) + amountState.amountTextField.update { + it.copy( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, + ) + } + return state.copy( + amountState = amountState.copy( + isPrimaryButtonEnabled = !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, + private fun SendUiState.emptyState(): SendUiState { + amountState?.amountTextField?.update { + it.copy( + value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE, + fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE, isError = false, + ) + } + return copy( + amountState = amountState?.copy( + isPrimaryButtonEnabled = false, ), - isPrimaryButtonEnabled = false, ) } private fun String.checkDecimalSeparatorDuplicate(): Boolean { - val regex = "[\\.\\,]".toRegex() + val regex = TRIM_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 + private fun String.checkExceedBalance( + cryptoCurrencyStatus: CryptoCurrencyStatus, + state: SendStates.AmountState, + ): Boolean { + val currencyStatus = cryptoCurrencyStatus.value return if (state.isFiatValue) { toBigDecimal() > currencyStatus.fiatAmount } else { @@ -88,10 +95,11 @@ internal class SendAmountFieldChangeConverter( if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1) val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString() - return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar) + return trimmedValue.replace(TRIM_REGEX.toRegex(), separatorChar) } companion object { private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + private const val TRIM_REGEX = "[.,]" } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index d6577f14c2..4b027a624d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -22,7 +22,6 @@ internal class SendAmountFieldConverter( imeAction = ImeAction.Next, keyboardType = KeyboardType.Number, ), - label = TextReference.Str(""), placeholder = TextReference.Str(DEFAULT_VALUE), isError = false, error = TextReference.Res(R.string.send_insufficient_funds), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index f62f981b67..456359a16c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -16,20 +16,43 @@ internal sealed class SendTextField { /** Keyboard options */ abstract val keyboardOptions: KeyboardOptions - /** Label */ - abstract val label: TextReference - - /** Placeholder (hint) */ - abstract val placeholder: TextReference + // /** Placeholder (hint) */ + // abstract val placeholder: TextReference data class Amount( override val value: String, override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, - override val label: TextReference, - override val placeholder: TextReference, + val placeholder: TextReference, val fiatValue: String, val isError: Boolean, val error: TextReference, ) : SendTextField() + + data class RecipientAddress( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + ) : SendTextField() + + data class RecipientMemo( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + ) : SendTextField() + + data class CustomFee( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val label: TextReference? = null, + ) : SendTextField() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt new file mode 100644 index 0000000000..75cd69aa6a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +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.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow + +internal class SendRecipientAddressFieldConverter( + private val clickIntents: SendClickIntents, +) : Converter> { + + override fun convert(value: Unit): MutableStateFlow { + return MutableStateFlow( + SendTextField.RecipientAddress( + value = "", + onValueChange = clickIntents::onRecipientAddressValueChange, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + placeholder = TextReference.Res(R.string.send_enter_address_field), + label = TextReference.Res(R.string.send_recipient), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt new file mode 100644 index 0000000000..da8a97622d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt @@ -0,0 +1,113 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import androidx.paging.* +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.utils.toFormattedCurrencyString +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.update + +internal class SendRecipientListConverter( + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) { + + fun convert(wallets: List, txHistory: PagingData) { + val filteredWallets = wallets.filterNotNull() + .groupBy { item -> item.name } + .values.flatten() + .mapIndexed { index, item -> + item.copy( + name = "${item.name} ${index.inc()}", + ) + } + + currentStateProvider().recipientList.update { + txHistory.filter { item -> + val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer + val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User + val isSingleAddress = if (item.isOutgoing) { + item.destinationType is TxHistoryItem.DestinationType.Single + } else { + item.sourceType is TxHistoryItem.SourceType.Single + } + isTransfer && isSingleAddress && isNotContract + }.map { tx -> + SendRecipientListContent.Item( + id = tx.txHash, + title = tx.extractAddress(), + subtitle = TextReference.Str(tx.getAmount()), + info = tx.extractTimestamp(), + subtitleIconRes = tx.extractIconRes(), + ) + }.insertWallets(filteredWallets) + } + } + + private fun PagingData.insertWallets( + wallets: List, + ): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + return@insertSeparators when { + before == null && after is SendRecipientListContent.Item -> { + SendRecipientListContent.Wallets( + wallets.map { + SendRecipientListContent.Item( + id = it.address, + title = TextReference.Str(it.address), + subtitle = TextReference.Str(it.name), + ) + }.toPersistentList(), + ) + } + else -> null + } + } + } + + private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) { + when (val destination = destinationType) { + is TxHistoryItem.DestinationType.Multiple -> TextReference.Res( + R.string.transaction_history_multiple_addresses, + ) + is TxHistoryItem.DestinationType.Single -> TextReference.Str(destination.addressType.address) + } + } else { + when (val source = sourceType) { + is TxHistoryItem.SourceType.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.SourceType.Single -> TextReference.Str(source.address) + } + } + + private fun TxHistoryItem.extractIconRes() = if (isOutgoing) { + R.drawable.ic_arrow_up_24 + } else { + R.drawable.ic_arrow_down_24 + } + + private fun TxHistoryItem.getAmount(): String { + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + return amount.toFormattedCurrencyString( + currency = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } + + private fun TxHistoryItem.extractTimestamp(): TextReference { + val date = timestampInMillis.toDateFormat( + formatter = DateTimeFormatters.dateDDMMYYYY, + ) + val time = timestampInMillis.toTimeFormat() + return TextReference.Res(R.string.send_date_format, wrappedList(date, time)) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt new file mode 100644 index 0000000000..d1890038cb --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -0,0 +1,51 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow + +internal class SendRecipientMemoFieldConverter( + private val clickIntents: SendClickIntents, + private val cryptoCurrencyStatus: Provider, +) : Converter> { + + fun convertOrNull(): MutableStateFlow? { + val cryptoCurrency = cryptoCurrencyStatus().currency + + return when (cryptoCurrency.network.id.value) { + Blockchain.XRP.id -> convert(R.string.send_destination_tag_field) + Blockchain.Binance.id, + Blockchain.TON.id, + Blockchain.Cosmos.id, + Blockchain.TerraV1.id, + Blockchain.TerraV2.id, + Blockchain.Stellar.id, + -> convert(R.string.send_extras_hint_memo) + else -> null + } + } + + override fun convert(value: Int): MutableStateFlow { + return MutableStateFlow( + SendTextField.RecipientMemo( + value = "", + onValueChange = clickIntents::onRecipientMemoValueChange, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Text, + ), + placeholder = TextReference.Res(R.string.send_optional_field), + label = TextReference.Res(value), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt new file mode 100644 index 0000000000..ebdafea549 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt @@ -0,0 +1,30 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter + +internal class SendRecipientStateConverter( + private val clickIntents: SendClickIntents, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) } + private val memoFieldConverter by lazy { + SendRecipientMemoFieldConverter( + clickIntents, + cryptoCurrencyStatusProvider, + ) + } + + override fun convert(value: Unit): SendStates.RecipientState { + return SendStates.RecipientState( + addressTextField = addressFieldConverter.convert(Unit), + memoTextField = memoFieldConverter.convertOrNull(), + network = cryptoCurrencyStatusProvider().currency.network.name, + isPrimaryButtonEnabled = false, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 599b46babc..30ca727485 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource @@ -19,9 +20,10 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType @Composable -internal fun SendNavigationButtons(uiState: SendUiState.Content) { +internal fun SendNavigationButtons(uiState: SendUiState) { Row( modifier = Modifier .fillMaxWidth() @@ -38,9 +40,11 @@ internal fun SendNavigationButtons(uiState: SendUiState.Content) { } @Composable -private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) { +private fun SendSecondaryNavigationButton(uiState: SendUiState) { + val currentState = uiState.currentState.collectAsState() AnimatedVisibility( - visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState, + visible = currentState.value == SendUiStateType.Recipient || + currentState.value == SendUiStateType.Fee, ) { Icon( modifier = Modifier @@ -48,46 +52,52 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) { .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) .background(TangemTheme.colors.button.secondary) .clickable { - // todo add prev click + uiState.clickIntents.onPrevClick() } .padding(TangemTheme.dimens.spacing12), painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, contentDescription = null, ) } } @Composable -private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) { - val buttonTextId = when (uiState) { - is SendUiState.Content.AmountState, - is SendUiState.Content.RecipientState, - is SendUiState.Content.FeeState, +private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) { + val currentState = uiState.currentState.collectAsState() + + val buttonTextId = when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Recipient, + SendUiStateType.Fee, -> R.string.common_next - is SendUiState.Content.SendState -> R.string.common_send + SendUiStateType.Send -> R.string.common_send else -> R.string.common_close } + + val isButtonEnabled = when (currentState.value) { + SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false + SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false + else -> true + } + AnimatedContent( targetState = buttonTextId, label = "Update send screen state", modifier = modifier, ) { textId -> - if (uiState is SendUiState.Content.SendState) { + if (currentState.value == SendUiStateType.Send) { PrimaryButtonIconEnd( text = stringResource(textId), iconResId = R.drawable.ic_tangem_24, - enabled = uiState.isPrimaryButtonEnabled, - onClick = { - // todo add next click - }, + enabled = isButtonEnabled, + onClick = uiState.clickIntents::onNextClick, ) } else { PrimaryButton( text = stringResource(textId), - enabled = uiState.isPrimaryButtonEnabled, - onClick = { - // todo add next click - }, + enabled = isButtonEnabled, + onClick = uiState.clickIntents::onNextClick, ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 34df63ae5a..93a0f62d57 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -1,53 +1,95 @@ package com.tangem.features.send.impl.presentation.ui +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R 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.ui.amount.SendAmountContent +import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent @Composable -internal fun SendScreen(uiState: SendUiState.Content) { +internal fun SendScreen(uiState: SendUiState) { + val currentState = uiState.currentState.collectAsStateWithLifecycle() + BackHandler { uiState.clickIntents.onPrevClick() } Column( modifier = Modifier + .fillMaxSize() + .systemBarsPadding() .imePadding() - .background( - color = TangemTheme.colors.background.tertiary, - shape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius24, - topEnd = TangemTheme.dimens.radius24, - ), - ), + .background(color = TangemTheme.colors.background.tertiary), horizontalAlignment = Alignment.CenterHorizontally, ) { - TangemBottomSheetDraggableHeader( - color = TangemTheme.colors.background.tertiary, - ) - Box( - modifier = Modifier - .weight(1f) - .scrollable(state = rememberScrollState(), orientation = Orientation.Vertical), - ) { - SendScreenContent(uiState) + val titleRes = when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Send, + -> R.string.common_send + SendUiStateType.Recipient -> R.string.send_recipient + SendUiStateType.Fee -> R.string.common_fee_selector_title + SendUiStateType.Done -> null } + val iconRes = when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Recipient, + -> R.drawable.ic_qrcode_scan_24 + else -> null + } + + AppBarWithBackButtonAndIcon( + text = titleRes?.let { stringResource(it) }, + onBackClick = uiState.clickIntents::onBackClick, + onIconClick = uiState.clickIntents::onQrCodeScanClick, + backIconRes = R.drawable.ic_close_24, + iconRes = iconRes, + backgroundColor = TangemTheme.colors.background.tertiary, + ) + SendScreenContent( + uiState = uiState, + currentState = currentState, + modifier = Modifier + .weight(1f), + ) SendNavigationButtons(uiState) } } @Composable -private fun SendScreenContent(uiState: SendUiState.Content) { - when (uiState) { - is SendUiState.Content.AmountState -> SendAmountContent(uiState) - else -> { /* [REDACTED_TODO_COMMENT]*/ +private fun SendScreenContent( + uiState: SendUiState, + currentState: State, + modifier: Modifier = Modifier, +) { + val recipientList = uiState.recipientList.collectAsLazyPagingItems() + AnimatedContent( + targetState = currentState.value, + label = "Send Scree Navigation", + modifier = modifier, + ) { state -> + when (state) { + SendUiStateType.Amount -> SendAmountContent( + uiState.amountState, + uiState.clickIntents, + ) + SendUiStateType.Recipient -> SendRecipientContent( + uiState.recipientState, + uiState.clickIntents, + recipientList, + ) + 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/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 5cd1c54f34..2fedacc5c1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -18,12 +18,8 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.OffsetMapping -import androidx.compose.ui.text.input.TransformedText -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.fields.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -140,29 +136,4 @@ private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: M textAlign = TextAlign.Center, ) } -} - -private class AmountVisualTransformation( - private val symbol: String, -) : VisualTransformation { - override fun filter(text: AnnotatedString): TransformedText { - return TransformedText( - buildAnnotatedString { - append(text) - if (text.isNotBlank()) { - append(" ") - append(symbol) - } - }, - object : OffsetMapping { - override fun originalToTransformed(offset: Int): Int { - return text.length - } - - override fun transformedToOriginal(offset: Int): Int { - return text.length - } - }, - ) - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index 0bb6d13de0..363392865b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -11,12 +11,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendStates @Composable -internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) { +internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) { + val amountTextField = amountState.amountTextField.collectAsStateWithLifecycle() Column( modifier = modifier .fillMaxWidth() @@ -52,10 +54,10 @@ internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, .align(Alignment.CenterHorizontally), ) AmountField( - sendField = amountState.amountTextField, + sendField = amountTextField.value, + isFiat = amountState.isFiatValue, cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol, fiatSymbol = amountState.appCurrency.symbol, - isFiat = amountState.isFiatValue, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt similarity index 75% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 0cc832db0c..d400239936 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui +package com.tangem.features.send.impl.presentation.ui.amount import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -6,10 +6,8 @@ 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 @@ -18,29 +16,20 @@ 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.state.SendStates import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.features.send.impl.presentation.ui.amount.AmountFieldContainer +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @Composable -internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) { +internal fun SendAmountContent(amountState: SendStates.AmountState?, clickIntents: SendClickIntents) { + if (amountState == null) return 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, @@ -49,16 +38,16 @@ internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) { ) { SegmentedButtons( modifier = Modifier - .height(TangemTheme.dimens.size40) - .weight(1f), + .weight(1f) + .height(TangemTheme.dimens.size40), config = amountState.segmentedButtonConfig, - onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) }, + onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) }, ) { SendAmountCurrencyButton(it) } SecondaryButton( text = stringResource(R.string.send_max_amount), - onClick = amountState.clickIntents::onMaxValueClick, + onClick = clickIntents::onMaxValueClick, size = TangemButtonSize.Text, shape = RoundedCornerShape(TangemTheme.dimens.radius26), modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt new file mode 100644 index 0000000000..ce0fbcee3b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt @@ -0,0 +1,38 @@ +package com.tangem.features.send.impl.presentation.ui.common + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.res.TangemTheme + +/** + * Container for footer info below the text field + * + * @param modifier of component + * @param footer text + * @param footerTopPadding padding between footer and field + * @param content field content + */ +@Composable +internal fun FooterContainer( + modifier: Modifier = Modifier, + footer: String? = null, + footerTopPadding: Dp = TangemTheme.dimens.spacing8, + content: @Composable () -> Unit, +) { + Column(modifier = modifier) { + content() + footer?.let { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = footerTopPadding), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index 2c3bfff4b4..f7c2cd613f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -13,9 +13,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility import com.tangem.core.ui.components.MiddleEllipsisText import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.res.TangemTheme @@ -28,75 +32,98 @@ import com.tangem.features.send.impl.R * @param subtitle subtitle * @param onClick click listener * @param modifier modifier + * @param info info * @param subtitleIconRes icon */ +@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod") @Composable fun ListItemWithIcon( title: String, subtitle: String, onClick: () -> Unit, modifier: Modifier = Modifier, + info: String? = null, @DrawableRes subtitleIconRes: Int? = null, ) { - Row( + ConstraintLayout( modifier = modifier .fillMaxWidth() - .background(TangemTheme.colors.background.action) .clickable { onClick() } - .padding( - vertical = TangemTheme.dimens.spacing8, - horizontal = TangemTheme.dimens.spacing12, - ), + .padding(horizontal = TangemTheme.dimens.spacing12), ) { + val (iconRef, titleRef, subtitleRef, subtitleIconRef, infoRef) = createRefs() + + val spacing2 = TangemTheme.dimens.spacing2 + val spacing8 = TangemTheme.dimens.spacing8 + val spacing10 = TangemTheme.dimens.spacing10 + val spacing12 = TangemTheme.dimens.spacing12 IdentIcon( address = title, modifier = Modifier .size(TangemTheme.dimens.size40) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)), + .clip(RoundedCornerShape(TangemTheme.dimens.radius20)) + .constrainAs(iconRef) { + start.linkTo(parent.start) + top.linkTo(parent.top, margin = spacing8) + bottom.linkTo(parent.bottom, margin = spacing8) + }, ) - Column( + MiddleEllipsisText( + text = title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Justify, modifier = Modifier - .fillMaxWidth() - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing2, - bottom = TangemTheme.dimens.spacing2, - ), - ) { - MiddleEllipsisText( - text = title, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Justify, - modifier = Modifier.fillMaxSize(), + .constrainAs(titleRef) { + start.linkTo(iconRef.end, margin = spacing12) + end.linkTo(parent.end) + top.linkTo(parent.top, margin = spacing10) + width = Dimension.fillToConstraints + }, + ) + Icon( + painter = painterResource(id = subtitleIconRes ?: R.drawable.ic_arrow_down_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .background(TangemTheme.colors.background.tertiary, CircleShape) + .constrainAs(subtitleIconRef) { + start.linkTo(iconRef.end, margin = spacing12) + top.linkTo(titleRef.bottom) + bottom.linkTo(parent.bottom, margin = spacing10) + visibility = if (subtitleIconRes == null) Visibility.Gone else Visibility.Visible + }, + ) + Text( + text = subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .constrainAs(subtitleRef) { + start.linkTo(subtitleIconRef.end, margin = spacing2, goneMargin = spacing12) + end.linkTo(infoRef.start) + top.linkTo(titleRef.bottom) + bottom.linkTo(parent.bottom, margin = spacing10) + width = Dimension.fillToConstraints + }, + ) + info?.let { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + modifier = Modifier + .constrainAs(infoRef) { + start.linkTo(subtitleRef.end, goneMargin = spacing12) + end.linkTo(parent.end) + top.linkTo(titleRef.bottom) + bottom.linkTo(parent.bottom, margin = spacing10) + }, ) - Row { - subtitleIconRes?.let { iconRes -> - Icon( - painter = painterResource(id = iconRes), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .size(TangemTheme.dimens.size16) - .background(TangemTheme.colors.background.tertiary, CircleShape) - .padding(TangemTheme.dimens.spacing3), - ) - } - Text( - text = subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - modifier = Modifier - .then( - if (subtitleIconRes != null) { - Modifier.padding(start = TangemTheme.dimens.spacing4) - } else { - Modifier - }, - ), - ) - } } } } @@ -111,6 +138,7 @@ private fun ListItemWithIconPreview_Light( ListItemWithIcon( title = config.title, subtitle = config.subtitle, + info = config.info, subtitleIconRes = config.iconRes, onClick = {}, ) @@ -126,6 +154,7 @@ private fun ListItemWithIconPreview_Dark( ListItemWithIcon( title = config.title, subtitle = config.subtitle, + info = config.info, subtitleIconRes = config.iconRes, onClick = {}, ) @@ -135,6 +164,7 @@ private fun ListItemWithIconPreview_Dark( private data class ListItemWithIconPreviewConfig( val title: String, val subtitle: String, + val info: String? = null, val iconRes: Int? = null, ) @@ -142,7 +172,14 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid collection = listOf( ListItemWithIconPreviewConfig( title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", - subtitle = "Wallet", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + iconRes = R.drawable.ic_arrow_down_24, + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "1 BTC", + info = "0.0.0000 at 00:00", iconRes = R.drawable.ic_arrow_down_24, ), ListItemWithIconPreviewConfig( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt new file mode 100644 index 0000000000..ada2667527 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -0,0 +1,199 @@ +package com.tangem.features.send.impl.presentation.ui.recipient + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +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.domain.SendRecipientListContent +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" +private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY" +private const val MY_WALLETS_HEADER_KEY = "MY_WALLETS_HEADER_KEY" + +@Composable +internal fun SendRecipientContent( + uiState: SendStates.RecipientState?, + clickIntents: SendClickIntents, + recipientList: LazyPagingItems, +) { + if (uiState == null) return + val address = uiState.addressTextField.collectAsState().value + val memo = uiState.memoTextField?.collectAsState()?.value + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + item(key = ADDRESS_FIELD_KEY) { + TextFieldWithPasteAndIcon( + value = address.value, + label = address.label, + placeholder = address.placeholder, + footer = stringResource(R.string.send_recipient_address_footer, uiState.network), + onValueChange = address.onValueChange, + onPasteClick = clickIntents::onRecipientAddressValueChange, + singleLine = true, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing4), + isError = address.isError, + error = address.error, + ) + } + memo?.let { memoField -> + item(key = MEMO_FIELD_KEY) { + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = memoField.placeholder, + footer = stringResource(R.string.send_recipient_memo_footer), + onValueChange = memoField.onValueChange, + onPasteClick = clickIntents::onRecipientMemoValueChange, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), + isError = memoField.isError, + error = memoField.error, + ) + } + } + recipientListItem( + recipientList = recipientList, + clickIntents = clickIntents, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.recipientListItem( + recipientList: LazyPagingItems, + clickIntents: SendClickIntents, +) { + items( + count = recipientList.itemCount, + key = recipientList.itemKey { + when (it) { + is SendRecipientListContent.Wallets -> MY_WALLETS_HEADER_KEY + is SendRecipientListContent.Item -> it.id + } + }, + contentType = recipientList.itemContentType { it::class.java }, + ) { index -> + recipientList[index]?.let { item -> + when (item) { + is SendRecipientListContent.Wallets -> { + RecipientWalletListItem( + item = item, + clickIntents = clickIntents, + modifier = Modifier + .animateItemPlacement() + .padding(top = TangemTheme.dimens.spacing20) + .then( + if (index == 0) { + Modifier.clip( + RoundedCornerShape( + topEnd = TangemTheme.dimens.radius12, + topStart = TangemTheme.dimens.radius12, + ), + ) + } else { + Modifier + }, + ), + ) + } + is SendRecipientListContent.Item -> { + val title = item.title.resolveReference() + ListItemWithIcon( + title = item.title.resolveReference(), + subtitle = item.subtitle.resolveReference(), + info = item.info?.let { ", ${it.resolveReference()}" }, + subtitleIconRes = item.subtitleIconRes, + modifier = Modifier + .then( + if (index == recipientList.itemCount - 1) { + Modifier + .padding(bottom = TangemTheme.dimens.spacing20) + .clip( + RoundedCornerShape( + bottomEnd = TangemTheme.dimens.radius12, + bottomStart = TangemTheme.dimens.radius12, + ), + ) + } else { + Modifier + }, + ) + .background(TangemTheme.colors.background.action), + onClick = { clickIntents.onRecipientAddressValueChange(title) }, + ) + } + } + } + } +} + +@Composable +private fun RecipientWalletListItem( + item: SendRecipientListContent.Wallets, + clickIntents: SendClickIntents, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.action) + .padding(top = TangemTheme.dimens.spacing12), + ) { + if (item.list.isNotEmpty()) { + Text( + text = stringResource(R.string.send_recipient_wallets_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing8, + ), + ) + } + item.list.forEachIndexed { _, wallet -> + val title = wallet.title.resolveReference() + ListItemWithIcon( + title = wallet.title.resolveReference(), + subtitle = wallet.subtitle.resolveReference(), + onClick = { clickIntents.onRecipientAddressValueChange(title) }, + ) + } + Text( + text = stringResource(R.string.send_recent_transactions), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt index 1b9118851a..3a2ed57e1a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt @@ -23,14 +23,15 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.TextReference 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.ui.common.FooterContainer @Composable internal fun TextFieldWithPasteAndIcon( @@ -42,7 +43,14 @@ internal fun TextFieldWithPasteAndIcon( modifier: Modifier = Modifier, footer: String? = null, singleLine: Boolean = false, + error: TextReference? = null, + isError: Boolean = false, ) { + val (title, color) = if (isError && error != null) { + error to TangemTheme.colors.text.warning + } else { + label to TangemTheme.colors.text.secondary + } FooterContainer(modifier, footer) { Column( modifier = Modifier @@ -53,9 +61,9 @@ internal fun TextFieldWithPasteAndIcon( ), ) { Text( - text = label.resolveReference(), + text = title.resolveReference(), style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, + color = color, modifier = Modifier .padding( start = TangemTheme.dimens.spacing12, @@ -114,7 +122,14 @@ internal fun TextFieldWithPaste( onPasteClick: (String) -> Unit, modifier: Modifier = Modifier, footer: String? = null, + error: TextReference? = null, + isError: Boolean = false, ) { + val (title, color) = if (isError && error != null) { + error to TangemTheme.colors.text.warning + } else { + label to TangemTheme.colors.text.secondary + } FooterContainer(modifier, footer) { Row( modifier = Modifier @@ -129,9 +144,9 @@ internal fun TextFieldWithPaste( .padding(TangemTheme.dimens.spacing12), ) { Text( - text = label.resolveReference(), + text = title.resolveReference(), style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, + color = color, ) SimpleTextField( value = value, @@ -160,6 +175,7 @@ internal fun TextFieldWithInfo( modifier: Modifier = Modifier, info: TextReference? = null, footer: String? = null, + visualTransformation: VisualTransformation = VisualTransformation.None, ) { FooterContainer( footer = footer, @@ -189,6 +205,7 @@ internal fun TextFieldWithInfo( SimpleTextField( value = value, onValueChange = onValueChange, + visualTransformation = visualTransformation, modifier = Modifier .padding(top = TangemTheme.dimens.spacing6) .weight(1f), @@ -208,27 +225,6 @@ internal fun TextFieldWithInfo( } } -@Composable -private fun FooterContainer( - modifier: Modifier = Modifier, - footer: String? = null, - footerTopPadding: Dp = TangemTheme.dimens.spacing8, - content: @Composable () -> Unit, -) { - Column(modifier = modifier) { - content() - footer?.let { - Text( - text = it, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(top = footerTopPadding), - ) - } - } -} - @Composable private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { val clipboardManager = LocalClipboardManager.current @@ -287,6 +283,7 @@ private fun SimpleTextField( modifier: Modifier = Modifier, placeholder: TextReference? = null, singleLine: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, ) { val focusRequester = remember { FocusRequester() } BasicTextField( @@ -295,6 +292,7 @@ private fun SimpleTextField( textStyle = TangemTheme.typography.body2, cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, + visualTransformation = visualTransformation, decorationBox = { textValue -> Box { if (value.isBlank() && placeholder != null) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt new file mode 100644 index 0000000000..3dd89596a1 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt @@ -0,0 +1,18 @@ +package com.tangem.features.send.impl.presentation.viewmodel + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.Address +import com.tangem.domain.tokens.model.CryptoCurrency + +internal fun verifyAddress(address: String, cryptoCurrency: CryptoCurrency?): Boolean { + if (address.isEmpty()) return true + val blockchain = cryptoCurrency?.let { + Blockchain.fromId(cryptoCurrency.id.rawNetworkId) + } ?: return false + + return blockchain.validateAddress(address) +} + +internal fun isNotAddressInWallet(walletAddresses: Set
, address: String): Boolean { + return walletAddresses.all { it.value != address } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt new file mode 100644 index 0000000000..a27ce13f23 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt @@ -0,0 +1,46 @@ +package com.tangem.features.send.impl.presentation.viewmodel + +import androidx.core.text.isDigitsOnly +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigInteger + +internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean { + if (cryptoCurrency == null) return false + return when (cryptoCurrency.network.id.value) { + Blockchain.XRP.id -> { + val tag = memo.toLongOrNull() + tag != null && tag <= XRP_TAG_MAX_NUMBER + } + Blockchain.Stellar.id -> { + isAssignableValue(memo) + } + else -> true + } +} + +private fun isAssignableValue(value: String): Boolean { + val memoType = when { + value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID + else -> XlmMemoType.TEXT + } + return when (memoType) { + XlmMemoType.TEXT -> { + // from org.stellar.sdk.MemoText + value.toByteArray().size <= XLM_MEMO_MAX_LENGTH + } + XlmMemoType.ID -> { + try { + // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo + value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() + } catch (ex: NumberFormatException) { + false + } + } + } +} + +private enum class XlmMemoType { TEXT, ID } + +private const val XRP_TAG_MAX_NUMBER = 4294967295 +private const val XLM_MEMO_MAX_LENGTH = 28 \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 118d166c69..03740cb51e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -2,13 +2,25 @@ package com.tangem.features.send.impl.presentation.viewmodel interface SendClickIntents { + fun onBackClick() + fun onNextClick() fun onPrevClick() + fun onQrCodeScanClick() + + // region Amount fun onAmountValueChange(value: String) fun onCurrencyChangeClick(isFiat: Boolean) fun onMaxValueClick() + // endregion + + // region Recipient + fun onRecipientAddressValueChange(value: String) + + fun onRecipientMemoValueChange(value: String) + // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 274feef98e..2bc9bb638a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -4,32 +4,53 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import androidx.paging.PagingData import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.xrp.XrpAddressService +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.Address import com.tangem.common.Provider import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendStateFactory import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter 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.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.properties.Delegates +@Suppress("LongParameterList") @HiltViewModel internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -42,26 +63,41 @@ internal class SendViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private var innerRouter: StateRouter by Delegates.notNull() + private val stateFactory = SendStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, + walletAddressesProvider = Provider { walletAddresses }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, ) var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) private set - private var userWallet: UserWallet? = null + private var userWallet: UserWallet by Delegates.notNull() + private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var walletAddresses = emptySet
() private var balanceJobHolder = JobHolder() + private var recipientsJobHolder = JobHolder() + private var walletAddressesJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { + getWalletAddresses() subscribeOnCurrencyStatusUpdates(owner) + getWalletsAndRecent() + } + + fun setRouter(router: StateRouter) { + innerRouter = router + uiState = uiState.copy(currentState = router.currentState) } private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { - viewModelScope.launch(dispatchers.io) { + viewModelScope.launch(dispatchers.main) { getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> userWallet = wallet @@ -86,11 +122,12 @@ internal class SendViewModel @Inject constructor( .conflate() .distinctUntilChanged() .onEach { either -> - uiState = stateFactory.getAmountState( - cryptoCurrencyStatus = either, - ) + either.onRight { + cryptoCurrencyStatus = it + uiState = stateFactory.getReadyState() + } } - .flowOn(dispatchers.io) + .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(balanceJobHolder) } @@ -107,27 +144,81 @@ internal class SendViewModel @Inject constructor( ) } - // region screen state navigation - override fun onNextClick() { - when (uiState) { - is SendUiState.Content.AmountState -> onRecipientStateClick() - is SendUiState.Content.RecipientState -> onFeeStateClick() - else -> { - // todo implement + private fun getWalletsAndRecent() { + combine( + flow = getUserWallets().conflate(), + flow2 = getTxHistory().conflate(), + ) { wallets, txHistory -> + stateFactory.onLoadedRecipientList( + wallets = wallets, + txHistory = txHistory, + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(recipientsJobHolder) + } + + private fun getUserWallets(): Flow> { + return getWalletsUseCase() + .distinctUntilChanged() + .map { userWallets -> + coroutineScope { + userWallets + .filterNot { it.walletId == userWalletId || it.isLocked } + .map { wallet -> + async(dispatchers.io) { + getCryptoCurrenciesUseCase(wallet.walletId) + .fold( + ifRight = { currencyItem -> + val walletCurrency = currencyItem.firstOrNull { + it.network.id == cryptoCurrency.network.id + } ?: return@fold null + val addresses = walletManagersFacade.getAddress( + userWalletId = wallet.walletId, + network = walletCurrency.network, + ) + return@fold AvailableWallet( + name = wallet.name, + address = addresses.first().value, + ) + }, + ifLeft = { null }, + ) + } + } + }.awaitAll() } + } + + private fun getTxHistory(): Flow> { + return flow { + txHistoryItemsUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifRight = { emitAll(it.distinctUntilChanged()) }, + ifLeft = {}, + ) } } - override fun onPrevClick() { - // todo implement + private fun getWalletAddresses() { + viewModelScope.launch(dispatchers.io) { + walletAddresses = walletManagersFacade.getAddresses( + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + }.saveIn(walletAddressesJobHolder) } - private fun onRecipientStateClick() { - stateFactory.getOnReceiveState() - } + // region screen state navigation + override fun onBackClick() = innerRouter.onBackClick() + override fun onNextClick() = innerRouter.onNextClick() + override fun onPrevClick() = innerRouter.onPrevClick() - private fun onFeeStateClick() { - // todo implement + override fun onQrCodeScanClick() { + // TODO Add QR code scanning } // endregion @@ -141,14 +232,44 @@ internal class SendViewModel @Inject constructor( } override fun onMaxValueClick() { - val amountState = uiState as? SendUiState.Content.AmountState ?: return - + val amountState = uiState.amountState ?: return val amount = if (amountState.isFiatValue) { amountState.cryptoCurrencyStatus.value.fiatAmount } else { amountState.cryptoCurrencyStatus.value.amount } - onAmountValueChange(amount?.toPlainString() ?: "0.00") + onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE) } // endregion + + // region recipient state clicks + override fun onRecipientAddressValueChange(value: String) { + if (!checkIfXrpAddressValue(value)) { + uiState = stateFactory.getOnRecipientAddressValueChangeState(value) + } + } + + override fun onRecipientMemoValueChange(value: String) { + if (!checkIfXrpAddressValue(value)) { + uiState = stateFactory.getOnRecipientMemoValueChangeState(value) + } + } + + private fun checkIfXrpAddressValue(value: String): Boolean { + if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) { + viewModelScope.launch(dispatchers.io) { + val result = XrpAddressService.decodeXAddress(value) + onRecipientAddressValueChange(result?.address.orEmpty()) + onRecipientMemoValueChange(result?.destinationTag.toString()) + } + return true + } + return false + } + // endregion + + companion object { + private const val XRP_X_ADDRESS = 'X' + private const val DEFAULT_VALUE = "0.00" + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt index 40261aefd2..819d32e1de 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -1,23 +1,19 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory -import android.text.format.DateUtils import androidx.paging.* import com.tangem.common.Provider import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState -import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isToday -import com.tangem.utils.extensions.isYesterday import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* -import org.joda.time.DateTime -import org.joda.time.DateTimeZone internal class TokenDetailsTxHistoryItemFlowConverter( private val currentStateProvider: Provider, @@ -101,7 +97,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter( ) { val txContent = txHistoryItemState.state as TransactionState.Content txHistoryItemState.copy( - state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()), + state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()), ) } else { txHistoryItemState @@ -117,26 +113,4 @@ internal class TokenDetailsTxHistoryItemFlowConverter( null } } - - /** - * If [this] timestamp is today or yesterday, returns relative date, - * otherwise returns formatting date. - */ - private fun Long.toDateFormat(): String { - val localDate = DateTime(this, DateTimeZone.getDefault()) - return if (localDate.isToday() || localDate.isYesterday()) { - DateUtils.getRelativeTimeSpanString( - this, - DateTime.now().millis, - DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() - } else { - DateTimeFormatters.formatDate(date = localDate) - } - } - - private fun String.toTimeFormat(): String { - return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault())) - } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6de5a3f3ab..86e6d89da1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -34,7 +34,7 @@ compose-navigation = "2.7.4" compose-accompanist = "0.30.1" compose-paging = "3.2.1" compose-reorderable = "0.9.6" -compoese-lifecycle-runtime = "2.6.2" +compose-lifecycle-runtime = "2.6.2" # endregion Compose # region Other libraries @@ -142,7 +142,7 @@ androidx-palette = { module = "androidx.palette:palette", version.ref = "android lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } -lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compoese-lifecycle-runtime" } +lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle-runtime" } androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } # region AndroidX From 4dc4b544d8646c5f4288caff34917ff930511c69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 14:42:48 +0400 Subject: [PATCH 03/11] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 70 +++++++++------- core/res/src/main/res/values/strings.xml | 80 +++++++++++-------- .../components/buttons/common/TangemButton.kt | 3 +- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../presentation/common/state/AlertState.kt | 42 ++++++++++ .../presentation/common/state/Event.kt | 8 ++ .../presentation/common/ui/EventEffect.kt | 18 +++++ .../common/ui/components/Alert.kt | 50 ++++++++++++ 8 files changed, 209 insertions(+), 63 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 52aa5dfca4..857c2f4ff1 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -11,8 +11,9 @@ Не удалось отправить письмо Причина: %s Не могу отправить транзакцию + Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. - Токены в сети Solana не поддерживаются этой картой из-за ограничений прошивки. + Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem @@ -57,10 +58,7 @@ Настройки карты Tangem Bot Чат - Оценить агента Отправить логи - Пожалуйста, выберите действие - Пожалуйста, оцените работу агента Принять Доступ запрещен Добавить @@ -89,12 +87,12 @@ Обменять Посмотреть историю транзакций Обозреватель - Скорость и комиссия Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции. - Медленно - По рынку - Быстро Свое + Быстро + По рынку + Медленно + Скорость и комиссия Сгенерировать адреса Импортировать Нравится @@ -144,7 +142,10 @@ Обязательное поле Количество знаков после запятой должно быть корректным числом не больше %d Своя деривация + Например m/00\'/0000\'/0\'/0/0 + Введите свою деривацию Знаков после запятой + Путь деривации По умолчанию Деривация по BIP44 Введенный путь деривации некорректен @@ -152,11 +153,15 @@ Название токена Не выбрано Сеть + Сеть + Вы можете добавить токен в ручную, если он не поддерживается Tangem Например, USDC Символ Символ токена Этот токен/сеть уже находится в вашем списке Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить + Остерегайтесь мошеннических токенов, они могут ничего не стоить + Токены могут быть созданы кем угодно Чат Код доступа Перед сканированием карты вам нужно будет ввести правильный код доступа. @@ -182,6 +187,13 @@ Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. Вы получаете Вы отправляете + Мои токены + У вас нет добавленных токенов. Добавьте токены для обмена + Недоступен для обмена с %s + Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами + Выберите провайдера + Лучший курс + Требуется разрешение Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. Скажите, пожалуйста, какая у вас карта? @@ -238,7 +250,9 @@ Рыночная капитализация Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. Голосовать + Кошелек не поддерживает выбранную монету Выберите кошелек + Кошелёк не поддерживает более одной сети Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -392,50 +406,50 @@ Приготовьте свою карту Сумма Вычесть из суммы отправки - Сумма к получению %1$s + Сумма к получению %s + %1$s в %2$s Адрес Код назначения - %1$s в %2$s Введите адрес Адрес совпадает с адресом кошелька Недопустимый Tag. Он не будет добавлен в транзакцию. Недопустимый Memo. Он не будет добавлен в транзакцию. Tag Memo - Последние - Убедитесь, что вы отправляете средства на адрес кошелька %1$s. Ошибки могут привести к потере ваших токенов. - Мемо/Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Включая комиссию Комиссия Низкая Нормальная Приоритетная + Лимит газа Цена газа Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. - Лимит газа - Максимальная сумма Всё + Максимальная сумма + Комиссия не превысит Максимальная cумма комиссии - Комиссия не превысит Сетевая комиссия - Покрытие сетевой комиссии Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии - Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции + Покрытие сетевой комиссии + Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса. - Недопустимая сумма - Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению. - Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. - Сумма резерва не может быть менее %1$s. - Пожалуйста, пополните свой баланс, чтобы продолжить. - Увеличение комиссии - Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. - Комиссия превышает баланс Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. + Комиссия превышает баланс + Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. + Увеличение комиссии + Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению + Недопустимая сумма + Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. + Пожалуйста, пополните свой баланс, чтобы продолжить + Сумма резерва не может быть менее %1$s + Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции Необязательное QR код содержит информацию о сумме отправки равной %s + Последние Получатель + Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов + Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки Отправка %s Всего @@ -650,7 +664,7 @@ Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. Некоторые сети недоступны - Это Testnet карта. Она не может обрабатывать транзакции и используется только в целях тестирования и разработки. + Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования Отказаться Вы не закончили резервное копирование. Хотите продолжить? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 09f3ec83d1..b26afb1dea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -11,8 +11,9 @@ Failed to send the email Reason: %s Can\'t send a transaction + The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. - Tokens in Solana network are not supported by this card due to firmware limitation. + Tokens in %1$s network are not supported by this card due to firmware limitation. Are you having difficulty scanning your card? This card is not designed to work with this app Go to settings to enable biometric authentication in the Tangem App @@ -55,10 +56,7 @@ Card Settings Tangem Bot Support - Rate agent Send logs - Please select an action - Please, rate the work of the agent Accept Access denied Add @@ -85,15 +83,15 @@ Enabled Error Exchange - Explore transaction history Explore + Explore transaction history Explorer - Speed and fee - Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority - Slow - Market - Fast + Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority. Custom + Fast + Market + Slow + Speed and fee Generate addresses Import Learn & Earn @@ -162,6 +160,8 @@ Token symbol This token/network has already been added to your list Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. + Be aware of adding scam tokens, they can cost nothing + Note that tokens can be created by anyone Chat Access code You will have to submit the correct access code before scanning the card @@ -187,6 +187,13 @@ You have used a card from another wallet. Tap the card associated with this wallet You Receive You Send + My tokens + You don\'t have any added tokens yet. Add tokens via Market to swap + Unavailable for swap from %s + Providers facilitate transactions, ensuring smooth and efficient token exchanges + Choose Provider + Best Rate + Permission Needed The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. Please tell us what card do you have @@ -224,6 +231,8 @@ Tokens Add Edit + Couldn’t find this token, you can add it manually + Coin market cap Blockchain the cryptocurrency was initially created Native network Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk. @@ -242,7 +251,9 @@ Coin market cap The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote + Wallet Incompatible with selected Coin Choose wallet + The wallet doesn\'t support more than one network You have to set up a single access code to protect all your wallets Protect You can set up an individual access code on each card later @@ -392,10 +403,10 @@ Get your card ready! Amount Subtract from send amount - The recipient will receive %1$s + The recipient will receive %s + %1$s at %2$s Address Destination Tag - %1$s at %2$s Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. @@ -408,47 +419,47 @@ Low Normal Priority - Network fee info unreachable Check your network connection + Network fee info unreachable + From **%s** Gas limit Gas Limit is auto-calculated; raise it during network congestion Gas price - Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed. - Maximum amount + Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed Max - Maximum fee amount + Maximum amount Max fee + Maximum fee amount Numbers only for Destination Tag Network fee - Network fee coverage Sending amount will be reduced to cover the selected fee level - Kindly be aware that your transaction may experience delays under specific fee settings + Network fee coverage + Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance. - Invalid amount - The included commission exceeds the transfer amount, leading to a negative value. - The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. - The balance amount must be at least %1$s. - Please top up your balance to continue. - Fee is increased - The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. - Fee exceeds balance The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. + Fee exceeds balance + The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. + Fee is increased + The included commission exceeds the transfer amount, leading to a negative value + Invalid amount + The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. + Please top up your balance to continue + The balance amount must be at least %1$s + Kindly be aware that your transaction may experience delays under specific fee settings Transaction delays are possible Optional - Please align your QR code with the square to scan it. Ensure you scan %s network address. - Recipient’s address scanned - Sending amount was changed - QR code contains information about the sending amount equal to %s - Change the entered amount? Change Decline + Recipient’s address scanned + Change the entered amount? + QR code contains information about the sending amount equal to %s + Please align your QR code with the square to scan it. Ensure you scan %s network address. Recent Recipient Not a valid address + Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens + A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets - Ensure that you are sending funds to an %1$s wallet address. Errors may result in the loss of your tokens. - A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds. Sending %s Total %1$s and %2$s will be sent @@ -456,6 +467,7 @@ %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Invalid address + Transaction sent Buy now I have a promo code… Tangem Wallet diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 8ce113a33f..ae762b2803 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.graphics.Shape @@ -108,7 +109,7 @@ private inline fun RowScope.ButtonContentContainer( if (showProgress) { progressIndicator() } else { - Column { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Row(horizontalArrangement = Arrangement.Center) { if (buttonIcon is TangemButtonIconPosition.Start) { icon(buttonIcon.iconResId) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index cb2f374b0c..2fc319885d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -55,6 +55,7 @@ data class TangemDimens internal constructor( val size20: Dp = 20.dp, val size24: Dp = 24.dp, val size28: Dp = 28.dp, + val size30: Dp = 30.dp, val size32: Dp = 32.dp, val size34: Dp = 34.dp, val size36: Dp = 36.dp, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt new file mode 100644 index 0000000000..767427f16d --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt @@ -0,0 +1,42 @@ +package com.tangem.managetokens.presentation.common.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.managetokens.impl.R + +internal sealed class AlertState { + + abstract val message: TextReference + + class DefaultAlert( + override val message: TextReference, + ) : AlertState() + + class TokenUnavailable( + val onUpvoteClick: () -> Unit, + ) : AlertState() { + override val message: TextReference = resourceReference(R.string.manage_tokens_unavailable_description) + val confirmButtonText: TextReference = resourceReference(R.string.common_close) + val dismissButtonText: TextReference = resourceReference(R.string.manage_tokens_unavailable_vote) + } + + object NonNative : AlertState() { + override val message: TextReference = resourceReference(R.string.manage_tokens_network_selector_non_native_info) + } + + object TokensUnsupported : AlertState() { + override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_message) + } + + object TokensUnsupportedCurve : AlertState() { + override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_curve_message) + } + + class TokensUnsupportedBlockchainByCard(val token: String) : AlertState() { + override val message: TextReference = resourceReference( + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, + formatArgs = wrappedList(token), + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt new file mode 100644 index 0000000000..4024d7a4d8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt @@ -0,0 +1,8 @@ +package com.tangem.managetokens.presentation.common.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface Event { + data class ShowAlert(val state: AlertState) : Event +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt new file mode 100644 index 0000000000..0bfc404f35 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt @@ -0,0 +1,18 @@ +package com.tangem.managetokens.presentation.common.ui + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.event.StateEvent +import com.tangem.managetokens.presentation.common.state.AlertState +import com.tangem.managetokens.presentation.common.state.Event + +@Composable +internal fun EventEffect(event: StateEvent, onAlertStateSet: (AlertState) -> Unit) { + com.tangem.core.ui.event.EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is Event.ShowAlert -> onAlertStateSet(value.state) + } + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt new file mode 100644 index 0000000000..27b83aca6b --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt @@ -0,0 +1,50 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.AlertState + +@Composable +internal fun Alert(state: AlertState, onDismiss: () -> Unit) { + when (state) { + is AlertState.DefaultAlert, + is AlertState.NonNative, + AlertState.TokensUnsupportedCurve, + AlertState.TokensUnsupported, + is AlertState.TokensUnsupportedBlockchainByCard, + -> DefaultAlert(state, onDismiss) + is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss) + } +} + +@Composable +private fun DefaultAlert(state: AlertState, onDismiss: () -> Unit) { + BasicDialog( + message = state.message.resolveReference(), + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} + +@Composable +private fun TokenUnavailableAlert(state: AlertState.TokenUnavailable, onDismiss: () -> Unit) { + BasicDialog( + message = state.message.resolveReference(), + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = onDismiss, + ), + dismissButton = DialogButton( + title = state.dismissButtonText.resolveReference(), + onClick = { state.onUpvoteClick() }, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file From d214ec6ec64619836104dc7e88569a49e37b08d0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Nov 2023 18:35:58 +0300 Subject: [PATCH 04/11] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/di/domain/TransactionDomainModule.kt | 24 ++ .../components/SettingsSwitchItem.kt | 2 +- core/res/src/main/res/values/strings.xml | 2 +- .../core/ui/components}/TangemSwitch.kt | 2 +- core/ui/src/main/res/drawable/ic_bird_24.xml | 9 + core/ui/src/main/res/drawable/ic_edit_24.xml | 7 + core/ui/src/main/res/drawable/ic_hare_24.xml | 9 + .../src/main/res/drawable/ic_tortoise_24.xml | 9 + .../DefaultWalletManagersFacade.kt | 19 ++ .../walletmanager/WalletManagersFacade.kt | 18 ++ domain/transaction/.gitignore | 1 + domain/transaction/build.gradle.kts | 23 ++ .../domain/transaction/error/GetFeeError.kt | 5 + .../transaction/usecase/GetFeeUseCase.kt | 65 +++++ features/send/impl/build.gradle.kts | 1 + .../presentation/state/SendStateFactory.kt | 43 ++- .../impl/presentation/state/SendUiState.kt | 6 +- .../impl/presentation/state/StateRouter.kt | 17 +- .../state/fee/FeeSelectorState.kt | 27 ++ .../state/fee/SendFeeCustomFieldConverter.kt | 61 ++++ .../state/fee/SendFeeStateConverter.kt | 17 ++ .../send/impl/presentation/ui/SendScreen.kt | 5 + .../presentation/ui/common/FooterContainer.kt | 5 +- .../ui/fee/SendCustomFeeEthereum.kt | 63 ++++ .../ui/fee/SendSpeedAndFeeContent.kt | 86 ++++++ .../presentation/ui/fee/SendSpeedSelector.kt | 271 ++++++++++++++++++ .../presentation/ui/fee/SendSpeedSubtract.kt | 66 +++++ .../presentation/ui/recipient/TextFields.kt | 9 +- .../viewmodel/SendClickIntents.kt | 10 + .../presentation/viewmodel/SendViewModel.kt | 98 +++++++ gradle/dependencies.toml | 2 +- settings.gradle.kts | 1 + 33 files changed, 963 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt rename {app/src/main/java/com/tangem/tap/features/details/ui/common => core/ui/src/main/java/com/tangem/core/ui/components}/TangemSwitch.kt (98%) create mode 100644 core/ui/src/main/res/drawable/ic_bird_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_edit_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_hare_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_tortoise_24.xml create mode 100644 domain/transaction/.gitignore create mode 100644 domain/transaction/build.gradle.kts create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a66725eb7..e3f7a4fcde 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.domain.appTheme.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.transaction) implementation(projects.common) implementation(projects.core.analytics) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt new file mode 100644 index 0000000000..5b68512d77 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal object TransactionDomainModule { + + @Provides + @ViewModelScoped + fun provideGetUseCase( + walletManagersFacade: WalletManagersFacade, + dispatchers: CoroutineDispatcherProvider, + ): GetFeeUseCase { + return GetFeeUseCase(walletManagersFacade, dispatchers) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index 78084743b7..74815a675f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item -import com.tangem.tap.features.details.ui.common.TangemSwitch +import com.tangem.core.ui.components.TangemSwitch @Composable internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b26afb1dea..6bd6a2445a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -421,7 +421,7 @@ Priority Check your network connection Network fee info unreachable - From **%s** + From Gas limit Gas Limit is auto-calculated; raise it during network congestion Gas price diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index 9839280049..c4d41ef91b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.details.ui.common +package com.tangem.core.ui.components import androidx.compose.animation.animateColor import androidx.compose.animation.core.* diff --git a/core/ui/src/main/res/drawable/ic_bird_24.xml b/core/ui/src/main/res/drawable/ic_bird_24.xml new file mode 100644 index 0000000000..234ea4f693 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_bird_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_edit_24.xml b/core/ui/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 0000000000..cc2504eb91 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_hare_24.xml b/core/ui/src/main/res/drawable/ic_hare_24.xml new file mode 100644 index 0000000000..e2714dc9d0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hare_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_tortoise_24.xml b/core/ui/src/main/res/drawable/ic_tortoise_24.xml new file mode 100644 index 0000000000..8dee215fd1 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tortoise_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index bc1bf00b14..a581d0d2fa 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -411,6 +412,24 @@ class DefaultWalletManagersFacade( } } + override suspend fun getFee( + amount: Amount, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): Result? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as? TransactionSender)?.getFee( + amount = amount, + destination = destination, + ) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 31345d6f99..8e830e0bdd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -2,10 +2,13 @@ package com.tangem.domain.walletmanager import arrow.core.Either import com.tangem.blockchain.blockchains.solana.RentProvider +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -143,4 +146,19 @@ interface WalletManagersFacade { network: Network, signedHashes: Int, ): Either + + /** + * Returns fee for transaction + * + * @param amount of transaction + * @param destination address + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun getFee( + amount: Amount, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): Result? } \ No newline at end of file diff --git a/domain/transaction/.gitignore b/domain/transaction/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/transaction/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts new file mode 100644 index 0000000000..8da795757a --- /dev/null +++ b/domain/transaction/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.transaction" +} + +dependencies { + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + implementation(projects.core.utils) + + implementation(deps.tangem.blockchain) + + implementation(projects.domain.legacy) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt new file mode 100644 index 0000000000..f08011b2d4 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.transaction.error + +sealed class GetFeeError { + object DataError : GetFeeError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt new file mode 100644 index 0000000000..dee4c3cdce --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -0,0 +1,65 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.math.BigDecimal + +/** + * Use case to get transaction fee + */ +class GetFeeUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val dispatcher: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + amount: BigDecimal, + destination: String, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Flow> { + return flow { + val result = walletManagersFacade.getFee( + amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), + destination = destination, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + + val maybeFee = when (result) { + is Result.Success -> result.data.right() + else -> GetFeeError.DataError.left() + } + emit(maybeFee) + }.flowOn(dispatcher.io) + } + + private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = amount, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 65f362206d..bb59a8e295 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 87ac690720..3bb47678a9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state import androidx.paging.PagingData import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference @@ -12,6 +13,10 @@ import com.tangem.domain.wallets.models.UserWallet 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.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fee.SendFeeCustomFieldConverter +import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter @@ -35,8 +40,13 @@ internal class SendStateFactory( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } - private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } + private val customFeeFieldConverter by lazy { + SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + ) + } private val amountStateConverter by lazy { SendAmountStateConverter( @@ -47,13 +57,17 @@ internal class SendStateFactory( cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - private val recipientStateConverter by lazy { SendRecipientStateConverter( clickIntents = clickIntents, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } + private val feeStateConverter by lazy { + SendFeeStateConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val recipientListStateConverter by lazy { SendRecipientListConverter( @@ -71,7 +85,7 @@ internal class SendStateFactory( fun getReadyState(): SendUiState = currentStateProvider().copy( amountState = amountStateConverter.convert(Unit), recipientState = recipientStateConverter.convert(Unit), - feeState = SendStates.FeeState(), + feeState = feeStateConverter.convert(Unit), ) //endregion @@ -164,5 +178,28 @@ internal class SendStateFactory( ), ) } + + fun onFeeOnLoadingState() { + currentStateProvider().feeState?.feeSelectorState?.update { + FeeSelectorState.Loading + } + } + + fun onFeeOnLoadedState(fees: TransactionFee) { + currentStateProvider().feeState?.feeSelectorState?.update { + FeeSelectorState.Content( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + } + } + //endregion + + //region fee + fun onFeeSelectedState(feeType: FeeType) { + currentStateProvider().feeState?.feeSelectorState?.update { + (it as? FeeSelectorState.Content)?.copy(selectedFee = feeType) ?: it + } + } //endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 6baeb27cfb..207e63064d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -8,6 +8,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList @@ -55,10 +56,13 @@ internal sealed class SendStates { val isPrimaryButtonEnabled: Boolean, ) : SendStates() - // todo [REDACTED_JIRA] /** Fee and speed state */ data class FeeState( override val type: SendUiStateType = SendUiStateType.Fee, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeSelectorState: MutableStateFlow = MutableStateFlow(FeeSelectorState.Empty), + val isSubtract: MutableStateFlow = MutableStateFlow(false), + val receivedAmount: MutableStateFlow = MutableStateFlow(""), ) : SendStates() // todo [REDACTED_JIRA] diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index e7526fdf04..e50ae05ae8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -16,15 +16,11 @@ internal class StateRouter( fun onNextClick() { when (currentState.value) { - SendUiStateType.Amount -> { - currentState.update { SendUiStateType.Recipient } - } - SendUiStateType.Recipient -> { - currentState.update { SendUiStateType.Fee } - } - else -> { - // todo implement - } + SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient } + SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee } + SendUiStateType.Fee -> currentState.update { SendUiStateType.Send } + SendUiStateType.Send -> currentState.update { SendUiStateType.Done } + SendUiStateType.Done -> onBackClick() } } @@ -33,7 +29,8 @@ internal class StateRouter( SendUiStateType.Amount -> onBackClick() SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } - else -> onBackClick() + SendUiStateType.Send -> currentState.update { SendUiStateType.Fee } + SendUiStateType.Done -> onBackClick() } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt new file mode 100644 index 0000000000..f920348ac8 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -0,0 +1,27 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import kotlinx.coroutines.flow.MutableStateFlow + +@Immutable +internal sealed class FeeSelectorState { + + object Loading : FeeSelectorState() + + object Empty : FeeSelectorState() + + data class Content( + val fees: TransactionFee, + val selectedFee: FeeType = FeeType.MARKET, + val customValues: MutableStateFlow> = MutableStateFlow(emptyList()), + ) : FeeSelectorState() +} + +enum class FeeType { + SLOW, + MARKET, + FAST, + CUSTOM, +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt new file mode 100644 index 0000000000..39851a417a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow + +internal class SendFeeCustomFieldConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, +) : Converter>> { + + override fun convert(value: Fee): MutableStateFlow> { + val ethereumFee = value as? Fee.Ethereum ?: return MutableStateFlow(emptyList()) + val appCurrency = appCurrencyProvider() + + val maxFeeFiat = BigDecimalFormatter.formatFiatAmount( + fiatAmount = ethereumFee.amount.value, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + + return MutableStateFlow( + listOf( + SendTextField.CustomFee( + value = ethereumFee.amount.value.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(0, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + label = TextReference.Str(maxFeeFiat), + ), + SendTextField.CustomFee( + value = ethereumFee.gasPrice.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(1, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + ), + SendTextField.CustomFee( + value = ethereumFee.gasLimit.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(2, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt new file mode 100644 index 0000000000..13a2ebe5c2 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.utils.converter.Converter + +internal class SendFeeStateConverter( + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + override fun convert(value: Unit): SendStates.FeeState { + return SendStates.FeeState( + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 93a0f62d57..c8f289df54 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -20,6 +20,7 @@ import com.tangem.features.send.impl.R 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.ui.amount.SendAmountContent +import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent @Composable @@ -89,6 +90,10 @@ private fun SendScreenContent( uiState.clickIntents, recipientList, ) + SendUiStateType.Fee -> SendSpeedAndFeeContent( + uiState.feeState, + uiState.clickIntents, + ) else -> { /* [REDACTED_TODO_COMMENT]*/ } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt index ce0fbcee3b..26b442e62a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.common +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -25,9 +26,9 @@ internal fun FooterContainer( ) { Column(modifier = modifier) { content() - footer?.let { + AnimatedVisibility(visible = footer != null) { Text( - text = it, + text = footer.orEmpty(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt new file mode 100644 index 0000000000..a96d8c5f1b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo + +private const val ETHEREUM_UNIT = "GWEI" + +@Composable +internal fun SendCustomFeeEthereum( + customValues: State>, + selectedFee: FeeType, + symbol: String, + modifier: Modifier = Modifier, +) { + val fee = customValues.value[0] + val gasPrice = customValues.value[1] + val gasLimit = customValues.value[2] + + if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = modifier, + ) { + TextFieldWithInfo( + value = fee.value, + label = stringResource(R.string.send_max_fee), + footer = stringResource(R.string.send_max_fee_footer), + info = fee.label, + visualTransformation = AmountVisualTransformation(symbol), + keyboardOptions = fee.keyboardOptions, + onValueChange = fee.onValueChange, + isSingleLine = true, + ) + TextFieldWithInfo( + value = gasPrice.value, + label = stringResource(R.string.send_gas_price), + footer = stringResource(R.string.send_gas_price_footer), + onValueChange = gasPrice.onValueChange, + visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + ) + TextFieldWithInfo( + value = gasLimit.value, + label = stringResource(R.string.send_gas_limit), + footer = stringResource(R.string.send_gas_limit_footer), + onValueChange = gasLimit.onValueChange, + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt new file mode 100644 index 0000000000..595d04491a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -0,0 +1,86 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" +private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { + if (state == null) return + val feeSendState = state.feeSelectorState.collectAsStateWithLifecycle() + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary) + .padding( + horizontal = TangemTheme.dimens.spacing16, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + item( + key = FEE_SELECTOR_KEY, + ) { + SendSpeedSelector( + state = feeSendState, + clickIntents = clickIntents, + ) + } + if (feeSendState.value is FeeSelectorState.Content) { + item( + key = FEE_CUSTOM_KEY, + ) { + AnimatedVisibility( + visible = feeSendState.value is FeeSelectorState.Content, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.tertiary), + ) { + val fee = feeSendState.value as FeeSelectorState.Content + val customValues = fee.customValues.collectAsStateWithLifecycle() + SendCustomFeeEthereum( + customValues = customValues, + selectedFee = fee.selectedFee, + symbol = state.cryptoCurrencyStatus.currency.symbol, + modifier = Modifier + .animateItemPlacement(), + ) + } + } + } + item { + val topPadding = (feeSendState.value as? FeeSelectorState.Content)?.let { state -> + if (state.selectedFee != FeeType.CUSTOM) { + TangemTheme.dimens.spacing8 + } else { + TangemTheme.dimens.spacing0 + } + } ?: TangemTheme.dimens.spacing0 + + SendSpeedSubtract( + receivingAmount = state.receivedAmount, + isSubtract = state.isSubtract, + onSelectClick = clickIntents::onSubtractSelect, + modifier = Modifier + .animateItemPlacement() + .padding( + top = topPadding, + bottom = TangemTheme.dimens.spacing12, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt new file mode 100644 index 0000000000..de69e011d6 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -0,0 +1,271 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.TextReference +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.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +@Suppress("LongMethod") +@Composable +internal fun SendSpeedSelector( + state: State, + clickIntents: SendClickIntents, + modifier: Modifier = Modifier, +) { + FooterContainer( + footer = stringResource(R.string.common_fee_selector_footer), + modifier = modifier, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + when (val selector = state.value) { + FeeSelectorState.Loading -> { + SendSpeedSelectorItemLoading() + SendSpeedSelectorItemLoading() + SendSpeedSelectorItemLoading() + } + is FeeSelectorState.Content -> { + when (selector.fees) { + is TransactionFee.Choosable -> { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_slow, + iconRes = R.drawable.ic_tortoise_24, + amount = TextReference.Str(selector.fees.minimum.amount.value.toString()), + symbol = TextReference.Str(selector.fees.minimum.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.SLOW, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + amount = TextReference.Str(selector.fees.normal.amount.value.toString()), + symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.MARKET, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_fast, + iconRes = R.drawable.ic_hare_24, + amount = TextReference.Str(selector.fees.priority.amount.value.toString()), + symbol = TextReference.Str(selector.fees.priority.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.FAST, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) }, + showDivider = selector.fees.normal is Fee.Ethereum, + ) + if (selector.fees.normal is Fee.Ethereum) { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_custom, + iconRes = R.drawable.ic_edit_24, + isSelected = selector.selectedFee == FeeType.CUSTOM, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) }, + showDivider = selector.fees.normal !is Fee.Ethereum, + ) + } + } + is TransactionFee.Single -> { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + isSelected = true, + amount = TextReference.Str(selector.fees.normal.amount.value.toString()), + symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol), + onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, + showDivider = false, + ) + } + } + } + FeeSelectorState.Empty -> Unit + } + } + } +} + +@Composable +private fun SendSpeedSelectorItemLoading() { + Row(modifier = Modifier.fillMaxWidth()) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + start = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size50, + height = TangemTheme.dimens.size12, + ), + ) + SpacerWMax() + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + end = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size90, + height = TangemTheme.dimens.size12, + ), + ) + } +} + +@Composable +private fun SendSpeedSelectorItem( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + amount: TextReference? = null, + symbol: TextReference? = null, + isSelected: Boolean = false, + showDivider: Boolean = true, +) { + val iconTint by animateColorAsState( + targetValue = if (isSelected) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.informative + }, + label = "Selector icon tint change", + ) + + val textStyle = if (isSelected) { + TangemTheme.typography.subtitle2 + } else { + TangemTheme.typography.body2 + } + + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onSelect() }, + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Icon( + painter = painterResource(iconRes), + tint = iconTint, + contentDescription = null, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) + Text( + text = stringResource(titleRes), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + if (amount != null && symbol != null) { + SelectorValueContent( + amount = amount, + symbol = symbol, + textStyle = textStyle, + ) + } + } + if (showDivider) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size1) + .padding(horizontal = TangemTheme.dimens.spacing12) + .background(TangemTheme.colors.stroke.primary) + .align(Alignment.BottomCenter), + ) + } + } +} + +@Composable +private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { + Text( + text = amount.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier + .weight(1f) + .padding( + start = TangemTheme.dimens.spacing4, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + Text( + text = symbol.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing1, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) +} + +//region preview +@Preview +@Composable +private fun FeeSelectorPreview_Light() { + TangemTheme { + SendSpeedSelectorItemLoading() + } +} + +@Preview +@Composable +private fun FeeSelectorPreview_Dark() { + TangemTheme(isDark = true) { + SendSpeedSelectorItemLoading() + } +} +//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt new file mode 100644 index 0000000000..fac746da5b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt @@ -0,0 +1,66 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import kotlinx.coroutines.flow.StateFlow + +@Composable +internal fun SendSpeedSubtract( + receivingAmount: StateFlow, + isSubtract: StateFlow, + onSelectClick: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val isSelected = isSubtract.collectAsStateWithLifecycle() + val footer = receivingAmount.collectAsStateWithLifecycle() + + val footerText = if (isSelected.value) { + stringResource(R.string.send_amount_substract_footer, footer.value) + } else { + null + } + + FooterContainer( + footer = footerText, + modifier = modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing20, + ), + ) { + Text( + text = stringResource(R.string.send_amount_substract), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing12), + ) + TangemSwitch( + checked = isSelected.value, + onCheckedChange = onSelectClick, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt index 3a2ed57e1a..f2a792f03e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -175,7 +176,9 @@ internal fun TextFieldWithInfo( modifier: Modifier = Modifier, info: TextReference? = null, footer: String? = null, + isSingleLine: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, ) { FooterContainer( footer = footer, @@ -206,6 +209,8 @@ internal fun TextFieldWithInfo( value = value, onValueChange = onValueChange, visualTransformation = visualTransformation, + singleLine = isSingleLine, + keyboardOptions = keyboardOptions, modifier = Modifier .padding(top = TangemTheme.dimens.spacing6) .weight(1f), @@ -284,15 +289,17 @@ private fun SimpleTextField( placeholder: TextReference? = null, singleLine: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, ) { val focusRequester = remember { FocusRequester() } BasicTextField( value = value, onValueChange = onValueChange, - textStyle = TangemTheme.typography.body2, + textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1), cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, decorationBox = { textValue -> Box { if (value.isBlank() && placeholder != null) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 03740cb51e..35df0a6614 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.viewmodel +import com.tangem.features.send.impl.presentation.state.fee.FeeType + interface SendClickIntents { fun onBackClick() @@ -23,4 +25,12 @@ interface SendClickIntents { fun onRecipientMemoValueChange(value: String) // endregion + + // region Fee + fun onFeeSelectorClick(feeType: FeeType) + + fun onCustomFeeValueChange(index: Int, value: String) + + fun onSubtractSelect(value: Boolean) + // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 2bc9bb638a..3e8bc7477f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -9,13 +9,16 @@ import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -27,7 +30,10 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendStateFactory 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.state.StateRouter +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -37,6 +43,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -50,6 +57,7 @@ internal class SendViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getFeeUseCase: GetFeeUseCase, private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -84,11 +92,13 @@ internal class SendViewModel @Inject constructor( private var balanceJobHolder = JobHolder() private var recipientsJobHolder = JobHolder() private var walletAddressesJobHolder = JobHolder() + private var feeJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { getWalletAddresses() subscribeOnCurrencyStatusUpdates(owner) getWalletsAndRecent() + getFee() } fun setRouter(router: StateRouter) { @@ -203,6 +213,38 @@ internal class SendViewModel @Inject constructor( } } + private fun getFee() { + viewModelScope.launch(dispatchers.main) { + uiState.currentState + .filter { it == SendUiStateType.Fee } + .onEach { + val amountState = uiState.amountState ?: return@onEach + val recipientState = uiState.recipientState ?: return@onEach + + stateFactory.onFeeOnLoadingState() + getFeeUseCase.invoke( + amount = amountState.amountTextField.value.value.toBigDecimal(), + destination = recipientState.addressTextField.value.value, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + .conflate() + .distinctUntilChanged() + .onEach { maybeFee -> + maybeFee.fold( + ifRight = { + stateFactory.onFeeOnLoadedState(it) + }, + ifLeft = { + // TODO add error handling + }, + ) + } + .launchIn(viewModelScope) + }.launchIn(viewModelScope) + }.saveIn(feeJobHolder) + } + private fun getWalletAddresses() { viewModelScope.launch(dispatchers.io) { walletAddresses = walletManagersFacade.getAddresses( @@ -268,6 +310,62 @@ internal class SendViewModel @Inject constructor( } // endregion + //region fee + override fun onFeeSelectorClick(feeType: FeeType) { + stateFactory.onFeeSelectedState(feeType) + updateReceiveAmount() + } + + override fun onCustomFeeValueChange(index: Int, value: String) { + uiState.feeState?.apply { + (feeSelectorState.value as? FeeSelectorState.Content)?.let { feeSelector -> + feeSelector.customValues.update { + it.toMutableList().apply { + set(index, it[index].copy(value = value)) + } + } + updateReceiveAmount() + } + } + } + + override fun onSubtractSelect(value: Boolean) { + uiState.feeState?.isSubtract?.update { value } + if (value) { + updateReceiveAmount() + } + } + + private fun updateReceiveAmount() { + uiState.feeState?.receivedAmount?.update { + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = calculateReceiveAmount(), + cryptoCurrency = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } + } + + private fun calculateReceiveAmount(): BigDecimal { + val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return BigDecimal.ZERO + val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO + + val fee = when (val selectedFee = feeState.fees) { + is TransactionFee.Choosable -> { + when (feeState.selectedFee) { + FeeType.SLOW -> selectedFee.minimum.amount.value + FeeType.MARKET -> selectedFee.normal.amount.value + FeeType.FAST -> selectedFee.priority.amount.value + FeeType.CUSTOM -> feeState.customValues.value.firstOrNull()?.value?.let { BigDecimal(it) } + } + } + is TransactionFee.Single -> selectedFee.normal.amount.value + } ?: BigDecimal.ZERO + + return BigDecimal(amount.value).minus(fee) + } + //endregion + companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 86e6d89da1..eed86ea134 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-384" +tangemBlockchainSdk = "develop-387" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-310" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds diff --git a/settings.gradle.kts b/settings.gradle.kts index 2bda17e30f..59ac1ba906 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -127,6 +127,7 @@ include(":domain:app-theme") include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") +include(":domain:transaction") // endregion Domain modules From fe8d2719a3f79dfab46141686550facf1d2af8cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Nov 2023 16:22:16 +0800 Subject: [PATCH 05/11] Updated on 2026-08-14 --- .../tokens/impl/presentation/ui/TokenItem.kt | 23 ++++++++++++------- .../currency/DefaultCurrencyIcon.kt | 15 +++++++++--- .../components/currency/fiaticon/FiatIcon.kt | 9 +++++--- .../currency/tokenicon/ContentIcon.kt | 13 +++++++---- .../utils/ImageBackgroundContrastChecker.kt | 8 +++---- .../managetokens/ui/components/TokenIcon.kt | 17 ++++++++++---- .../ui/amount/SendAmountContent.kt | 5 ++-- .../feature/swap/ui/SwapSelectTokenScreen.kt | 16 +++++++++---- .../tangem/feature/swap/ui/TransactionCard.kt | 17 ++++++++++---- .../tokendetails/ui/components/TokenIcon.kt | 13 ++++++++--- 10 files changed, 94 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt index 9107775b0a..bae044a2a6 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -25,7 +26,6 @@ import coil.request.ImageRequest import com.tangem.core.ui.components.CurrencyPlaceholderIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.tap.common.compose.extensions.toPx import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.wallet.R import kotlinx.coroutines.launch @@ -126,29 +126,37 @@ internal fun TokenItem(model: TokenItemState) { @Composable private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> Unit, modifier: Modifier = Modifier) { + val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size46.roundToPx() } val iconModifier = modifier .size(size = TangemTheme.dimens.size46) .clip(TangemTheme.shapes.roundedCorners8) - val screenBackgroundColor = TangemTheme.colors.background.primary.toArgb() + + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + var isBackgroundColorDefined by remember { mutableStateOf(false) } + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() val isDarkTheme = isSystemInDarkTheme() val coroutineScope = rememberCoroutineScope() SubcomposeAsyncImage( modifier = iconModifier, model = ImageRequest.Builder(context = LocalContext.current) - .size(size = TangemTheme.dimens.size46.toPx().toInt()) .data(data = iconUrl) + .size(size = pixelsSize) + .memoryCacheKey(key = iconUrl + pixelsSize) .crossfade(enable = true) .allowHardware(false) .listener( onSuccess = { _, result -> - if (isDarkTheme) { + if (!isBackgroundColorDefined && isDarkTheme) { coroutineScope.launch { val color = ImageBackgroundContrastChecker( drawable = result.drawable, - backgroundColor = screenBackgroundColor, - ).getContrastColorIfNeeded(isDarkTheme) + backgroundColor = itemBackgroundColor, + size = pixelsSize, + ).getContrastColor(isDarkTheme = true) onContrastCalculate(color) + iconBackgroundColor = color + isBackgroundColorDefined = true } } }, @@ -189,13 +197,12 @@ private fun Subtitle(isExpanded: Boolean, modifier: Modifier = Modifier) { } } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier.size(size = TangemTheme.dimens.size46)) { AnimatedContent( targetState = isExpanded, - transitionSpec = { fadeIn() + scaleIn() with scaleOut() + fadeOut() }, + transitionSpec = { (fadeIn() + scaleIn()).togetherWith(scaleOut() + fadeOut()) }, ) { isExpanded -> Icon( painter = painterResource( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt index b169176106..005053e2c8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt @@ -9,6 +9,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon @@ -19,16 +21,19 @@ import kotlinx.coroutines.launch @Composable internal inline fun DefaultCurrencyIcon( iconData: Any, + size: Dp, alpha: Float, colorFilter: ColorFilter?, crossinline errorIcon: @Composable () -> Unit, modifier: Modifier = Modifier, ) { var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + var isBackgroundColorDefined by remember { mutableStateOf(false) } val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() val isDarkTheme = isSystemInDarkTheme() val coroutineScope = rememberCoroutineScope() + val pixelsSize = with(LocalDensity.current) { size.roundToPx() } SubcomposeAsyncImage( modifier = modifier .background( @@ -38,17 +43,21 @@ internal inline fun DefaultCurrencyIcon( .clip(TangemTheme.shapes.roundedCorners8), model = ImageRequest.Builder(context = LocalContext.current) .data(iconData) + .size(size = pixelsSize) + .memoryCacheKey(key = iconData.toString() + pixelsSize) .crossfade(enable = true) - .allowHardware(false) + .allowHardware(enable = false) .listener( onSuccess = { _, result -> - if (isDarkTheme) { + if (!isBackgroundColorDefined && isDarkTheme) { coroutineScope.launch { val color = ImageBackgroundContrastChecker( drawable = result.drawable, backgroundColor = itemBackgroundColor, - ).getContrastColorIfNeeded(isDarkTheme) + size = pixelsSize, + ).getContrastColor(isDarkTheme = true) iconBackgroundColor = color + isBackgroundColorDefined = true } } }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt index 879ea4575a..3255b50af7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.DefaultCurrencyIcon @@ -18,21 +19,23 @@ import com.tangem.core.ui.components.currency.DefaultCurrencyIcon @Composable fun FiatIcon( url: String?, + size: Dp, modifier: Modifier = Modifier, @DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle, ) { val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url DefaultCurrencyIcon( - modifier = modifier, iconData = iconData, + size = size, + alpha = 1f, + colorFilter = null, errorIcon = { Image( painter = painterResource(id = fallbackResId), contentDescription = null, ) }, - alpha = 1f, - colorFilter = null, + modifier = modifier, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt index dd3e892301..cdf96d7b53 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.DefaultCurrencyIcon +import com.tangem.core.ui.res.TangemTheme @Composable internal fun ContentIcon( @@ -67,8 +68,10 @@ private fun CoinIcon( val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url DefaultCurrencyIcon( - modifier = modifier, iconData = iconData, + size = TangemTheme.dimens.size36, + alpha = alpha, + colorFilter = colorFilter, errorIcon = { Image( painter = painterResource(id = fallbackResId), @@ -77,8 +80,7 @@ private fun CoinIcon( contentDescription = null, ) }, - alpha = alpha, - colorFilter = colorFilter, + modifier = modifier, ) } @@ -94,11 +96,12 @@ private fun TokenIcon( errorIcon() } else { DefaultCurrencyIcon( - modifier = modifier, iconData = url, - errorIcon = errorIcon, + size = TangemTheme.dimens.size36, alpha = alpha, colorFilter = colorFilter, + errorIcon = errorIcon, + modifier = modifier, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt index 5fab6c4856..ed6cab9758 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt @@ -13,12 +13,12 @@ class ImageBackgroundContrastChecker( private val image: Bitmap, private val backgroundColor: Int, ) { - constructor(drawable: Drawable, backgroundColor: Int) : this( - image = drawable.toBitmap(), + constructor(drawable: Drawable, backgroundColor: Int, size: Int) : this( + image = drawable.toBitmap(width = size, height = size), backgroundColor = backgroundColor, ) - suspend fun getContrastColorIfNeeded(isDarkTheme: Boolean): Color { + suspend fun getContrastColor(isDarkTheme: Boolean): Color { return if (isLowContrast()) { if (isDarkTheme) Color.White else Color.Black } else { @@ -26,7 +26,7 @@ class ImageBackgroundContrastChecker( } } - suspend fun isLowContrast(): Boolean { + private suspend fun isLowContrast(): Boolean { val palette = generatePaletteAsync(bitmap = image) val color = palette.getDominantColor(backgroundColor) val contrast = ColorUtils.calculateContrast(color, backgroundColor) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt index bc7701328b..262ebcf808 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest @@ -25,8 +26,7 @@ import kotlinx.coroutines.launch @Composable internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { - val iconModifier = modifier - .size(TangemTheme.dimens.size36) + val iconModifier = modifier.size(TangemTheme.dimens.size36) if (state.iconReference != null) { DefaultCurrencyIcon( modifier = iconModifier, @@ -73,11 +73,14 @@ private inline fun DefaultCurrencyIcon( crossinline errorIcon: @Composable () -> Unit, modifier: Modifier = Modifier, ) { - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + var isBackgroundColorDefined by remember { mutableStateOf(false) } + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() val isDarkTheme = isSystemInDarkTheme() val coroutineScope = rememberCoroutineScope() + val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() } + SubcomposeAsyncImage( modifier = modifier .background( @@ -86,17 +89,21 @@ private inline fun DefaultCurrencyIcon( ), model = ImageRequest.Builder(context = LocalContext.current) .data(iconReference.getReference()) + .size(size = pixelsSize) + .memoryCacheKey(key = iconReference.getReference().toString() + pixelsSize) .crossfade(enable = true) .allowHardware(false) .listener( onSuccess = { _, result -> - if (isDarkTheme) { + if (!isBackgroundColorDefined && isDarkTheme) { coroutineScope.launch { val color = ImageBackgroundContrastChecker( drawable = result.drawable, backgroundColor = itemBackgroundColor, - ).getContrastColorIfNeeded(isDarkTheme) + size = pixelsSize, + ).getContrastColor(isDarkTheme = true) iconBackgroundColor = color + isBackgroundColorDefined = true } } }, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index d400239936..ba14154d01 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -15,6 +15,7 @@ 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.state.SendStates import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig @@ -72,8 +73,8 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) { if (button.isFiat) { FiatIcon( url = button.iconUrl, - modifier = Modifier - .size(TangemTheme.dimens.size18), + size = TangemTheme.dimens.size18, + modifier = Modifier.size(TangemTheme.dimens.size18), ) } else { button.iconState?.let { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 566447924f..80d9af4c8a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -217,6 +218,7 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo @Composable private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @DrawableRes iconPlaceholder: Int?) { var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + var isBackgroundColorDefined by remember { mutableStateOf(false) } val isDarkTheme = isSystemInDarkTheme() val coroutineScope = rememberCoroutineScope() @@ -231,29 +233,35 @@ private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @Drawa shape = TangemTheme.shapes.roundedCorners8, ), ) { - val iconModifier = Modifier - .size(TangemTheme.dimens.size40) + val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size40.roundToPx() } + val iconModifier = Modifier.size(TangemTheme.dimens.size40) + val colorFilter = if (!token.available) { val matrix = ColorMatrix().apply { setToSaturation(0f) } ColorFilter.colorMatrix(matrix) } else { null } + SubcomposeAsyncImage( modifier = iconModifier, model = ImageRequest.Builder(LocalContext.current) .data(data) + .size(size = pixelsSize) + .memoryCacheKey(key = data.toString() + pixelsSize) .crossfade(true) .allowHardware(false) .listener( onSuccess = { _, result -> - if (isDarkTheme) { + if (!isBackgroundColorDefined && isDarkTheme) { coroutineScope.launch { val color = ImageBackgroundContrastChecker( drawable = result.drawable, backgroundColor = screenBackgroundColor.toArgb(), - ).getContrastColorIfNeeded(isDarkTheme) + size = pixelsSize, + ).getContrastColor(isDarkTheme = true) iconBackgroundColor = color + isBackgroundColorDefined = true } } }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index bd57a551b1..3cbd35a23f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -292,8 +293,9 @@ private fun TokenIcon( @DrawableRes iconPlaceholder: Int? = null, @DrawableRes networkIconRes: Int? = null, ) { - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + var isBackgroundColorDefined by remember { mutableStateOf(false) } + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() val isDarkTheme = isSystemInDarkTheme() val coroutineScope = rememberCoroutineScope() @@ -310,13 +312,16 @@ private fun TokenIcon( shape = TangemTheme.shapes.roundedCorners8, ) - val data = tokenIconUrl.ifEmpty { - iconPlaceholder - } + val data = tokenIconUrl.ifEmpty { iconPlaceholder } + + val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() } + SubcomposeAsyncImage( modifier = tokenImageModifier, model = ImageRequest.Builder(LocalContext.current) .data(data) + .size(size = pixelsSize) + .memoryCacheKey(key = data.toString() + pixelsSize) .crossfade(true) .allowHardware(false) .listener( @@ -326,8 +331,10 @@ private fun TokenIcon( val color = ImageBackgroundContrastChecker( drawable = result.drawable, backgroundColor = itemBackgroundColor, - ).getContrastColorIfNeeded(isDarkTheme) + size = pixelsSize, + ).getContrastColor(true) iconBackgroundColor = color + isBackgroundColorDefined = true } } }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt index 2a11731a45..8bf7ed9b78 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest @@ -136,11 +137,13 @@ private inline fun DefaultCurrencyIcon( crossinline errorIcon: @Composable () -> Unit, modifier: Modifier = Modifier, ) { - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + var isBackgroundColorDefined by remember { mutableStateOf(false) } + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() val isDarkTheme = isSystemInDarkTheme() val coroutineScope = rememberCoroutineScope() + val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size48.roundToPx() } SubcomposeAsyncImage( modifier = modifier .background( @@ -149,17 +152,21 @@ private inline fun DefaultCurrencyIcon( ), model = ImageRequest.Builder(context = LocalContext.current) .data(iconData) + .size(pixelsSize) + .memoryCacheKey(iconData.toString() + pixelsSize) .crossfade(enable = true) .allowHardware(false) .listener( onSuccess = { _, result -> - if (isDarkTheme) { + if (!isBackgroundColorDefined && isDarkTheme) { coroutineScope.launch { val color = ImageBackgroundContrastChecker( drawable = result.drawable, backgroundColor = itemBackgroundColor, - ).getContrastColorIfNeeded(isDarkTheme) + size = pixelsSize, + ).getContrastColor(isDarkTheme = true) iconBackgroundColor = color + isBackgroundColorDefined = true } } }, From e40ce860a4a87237039ec0c75b3eb7313abeeb3d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Nov 2023 12:25:42 +0200 Subject: [PATCH 06/11] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- ...ProviderImpl.kt => DefaultAuthProvider.kt} | 2 +- .../auth/DefaultExpressAuthProvider.kt | 32 +++++++++++++++++++ .../tangem/tap/network/auth/di/AuthModule.kt | 20 ++++++++++-- .../datasource/api/express/ExpressApi.kt | 11 ++----- .../datasource/config/ConfigManagerImpl.kt | 1 + .../tangem/datasource/config/models/Config.kt | 1 + .../datasource/config/models/JsonModels.kt | 1 + .../com/tangem/datasource/di/NetworkModule.kt | 26 +++++++++++++++ .../tangem/datasource/utils/RequestHeader.kt | 7 ++++ .../tangem/lib/auth/ExpressAuthProvider.kt | 10 ++++++ .../sessionId/ExpressSessionIdGenerator.kt | 6 ++++ 12 files changed, 106 insertions(+), 13 deletions(-) rename app/src/main/java/com/tangem/tap/network/auth/{AuthProviderImpl.kt => DefaultAuthProvider.kt} (81%) create mode 100644 app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt create mode 100644 libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt create mode 100644 libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 2b49c08b92..ade38b689e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2b49c08b924c03322f35542a7482e4e9fb1928d3 +Subproject commit ade38b689e5e390cf4105f9dfa641b4f6912dcd0 diff --git a/app/src/main/java/com/tangem/tap/network/auth/AuthProviderImpl.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt similarity index 81% rename from app/src/main/java/com/tangem/tap/network/auth/AuthProviderImpl.kt rename to app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index d4bbdd61bd..27ac9a4336 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/AuthProviderImpl.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -4,7 +4,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.lib.auth.AuthProvider import com.tangem.tap.proxy.AppStateHolder -class AuthProviderImpl(private val appStateHolder: AppStateHolder) : AuthProvider { +internal class DefaultAuthProvider(private val appStateHolder: AppStateHolder) : AuthProvider { override fun getCardPublicKey(): String { return appStateHolder.scanResponse?.card?.cardPublicKey?.toHexString() ?: "" diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt new file mode 100644 index 0000000000..70d580f3c7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.network.auth + +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator +import java.util.UUID +import java.util.concurrent.atomic.AtomicReference + +internal class DefaultExpressAuthProvider( + private val userWalletsStore: UserWalletsStore, + private val configManager: ConfigManager, +) : ExpressAuthProvider, ExpressSessionIdGenerator { + + private var uuid = AtomicReference(UUID.randomUUID()) + + override fun getApiKey(): String { + return configManager.config.tangemExpressApiKey + } + + override fun getUserId(): String { + return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: "" + } + + override fun getSessionId(): String { + return uuid.get().toString() + } + + override fun generateNewSessionId() { + uuid = AtomicReference(UUID.randomUUID()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 6e9eabd1f1..fa9aa920a1 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -1,7 +1,11 @@ package com.tangem.tap.network.auth.di +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.lib.auth.AuthProvider -import com.tangem.tap.network.auth.AuthProviderImpl +import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.tap.network.auth.DefaultAuthProvider +import com.tangem.tap.network.auth.DefaultExpressAuthProvider import com.tangem.tap.proxy.AppStateHolder import dagger.Module import dagger.Provides @@ -16,6 +20,18 @@ class AuthModule { @Provides @Singleton fun provideAuthProvider(appStateHolder: AppStateHolder): AuthProvider { - return AuthProviderImpl(appStateHolder) + return DefaultAuthProvider(appStateHolder) + } + + @Provides + @Singleton + fun provideExpressAuthProvider( + userWalletsStore: UserWalletsStore, + configManager: ConfigManager, + ): ExpressAuthProvider { + return DefaultExpressAuthProvider( + userWalletsStore = userWalletsStore, + configManager = configManager, + ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt index 8574c38d72..0ed82c76d9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt @@ -6,7 +6,6 @@ import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.* import retrofit2.http.Body import retrofit2.http.GET -import retrofit2.http.Header import retrofit2.http.POST import retrofit2.http.Query import java.math.BigDecimal @@ -17,14 +16,8 @@ import java.math.BigDecimal @Suppress("LongParameterList") interface ExpressApi { - // TODO move first three params to retrofit interceptor @POST("assets") - suspend fun getAssets( - @Header("api-key") apiKey: String, - @Header("user-id") userId: String, - @Header("session-id") sessionId: String, - @Body body: AssetsRequestBody, - ): ApiResponse> + suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse> @POST("pairs") suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse> @@ -55,6 +48,6 @@ interface ExpressApi { @Query("toAddress") toAddress: String, ): ApiResponse - @GET("exchange-results") + @GET("exchange-result") suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 6bf2505649..bded21d266 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -106,6 +106,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { swapReferrerAccount = configValues.swapReferrerAccount, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, + tangemExpressApiKey = configValues.tangemExpressApiKey, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 2980e22216..d1f3e0848f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -19,4 +19,5 @@ data class Config( val swapReferrerAccount: SwapReferrerAccount? = null, val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, + val tangemExpressApiKey: String = "", ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 77d2c0808f..47210def4b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -40,6 +40,7 @@ class ConfigValueModel( val tangemComAuthorization: String?, val chiaFireAcademyApiKey: String?, val chiaTangemApiKey: String?, + val tangemExpressApiKey: String, ) data class AppsFlyer( diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index f3e245e0d6..d8b4a287eb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -3,12 +3,14 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory +import com.tangem.datasource.api.express.ExpressApi import com.tangem.datasource.api.promotion.PromotionApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers import com.tangem.lib.auth.AuthProvider +import com.tangem.lib.auth.ExpressAuthProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,6 +26,27 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) class NetworkModule { + @Provides + @Singleton + fun provideExpressApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + expressAuthProvider: ExpressAuthProvider, + ): ExpressApi { + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .baseUrl(DEV_EXPRESS_BASE_URL) + .client( + OkHttpClient.Builder() + .addHeaders(Express(expressAuthProvider)) + .addLoggers(context) + .build(), + ) + .build() + .create(ExpressApi::class.java) + } + @Provides @Singleton fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { @@ -74,6 +97,9 @@ class NetworkModule { } private companion object { + const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/" + const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" + const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index 5e5067746f..6a5fde6a53 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.utils import com.tangem.lib.auth.AuthProvider +import com.tangem.lib.auth.ExpressAuthProvider /** * Presentation of request header @@ -18,4 +19,10 @@ sealed class RequestHeader(vararg pairs: Pair String>) { "card_id" to { authProvider.getCardId() }, "card_public_key" to { authProvider.getCardPublicKey() }, ) + + class Express(expressAuthProvider: ExpressAuthProvider) : RequestHeader( + "api-key" to { expressAuthProvider.getApiKey() }, + "user-id" to { expressAuthProvider.getUserId() }, + "session-id" to { expressAuthProvider.getSessionId() }, + ) } \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt new file mode 100644 index 0000000000..4388254787 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt @@ -0,0 +1,10 @@ +package com.tangem.lib.auth + +interface ExpressAuthProvider { + + fun getApiKey(): String + + fun getUserId(): String + + fun getSessionId(): String +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt b/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt new file mode 100644 index 0000000000..be7270b4c7 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt @@ -0,0 +1,6 @@ +package com.tangem.lib.auth.sessionId + +interface ExpressSessionIdGenerator { + + fun generateNewSessionId() +} \ No newline at end of file From 2b4aa16605c1aedfdf0ee8d8d7d85a9de5a545f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Nov 2023 13:15:59 +0300 Subject: [PATCH 07/11] Updated on 2026-08-14 --- .../domain/SendRecipientListContent.kt | 1 + .../presentation/state/SendStateFactory.kt | 7 +- .../recipient/SendRecipientListConverter.kt | 71 +++++++++++-------- .../ui/recipient/SendRecipientContent.kt | 41 +++++++---- .../presentation/viewmodel/SendViewModel.kt | 20 +++++- 5 files changed, 93 insertions(+), 47 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt index 6cb3516497..7f7216e6e3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt @@ -17,5 +17,6 @@ internal sealed class SendRecipientListContent { data class Wallets( val list: PersistentList, + val isWalletsOnly: Boolean, ) : SendRecipientListContent() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 3bb47678a9..38583d0676 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -105,10 +105,15 @@ internal class SendStateFactory( //endregion //region recipient - fun onLoadedRecipientList(wallets: List, txHistory: PagingData) { + fun onLoadedRecipientList( + wallets: List, + txHistory: PagingData, + txHistoryCount: Int, + ) { recipientListStateConverter.convert( wallets = wallets, txHistory = txHistory, + txHistoryCount = txHistoryCount, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt index da8a97622d..8fd734e781 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt @@ -22,7 +22,7 @@ internal class SendRecipientListConverter( private val cryptoCurrencyStatusProvider: Provider, ) { - fun convert(wallets: List, txHistory: PagingData) { + fun convert(wallets: List, txHistory: PagingData, txHistoryCount: Int) { val filteredWallets = wallets.filterNotNull() .groupBy { item -> item.name } .values.flatten() @@ -32,49 +32,58 @@ internal class SendRecipientListConverter( ) } + val walletsItem = getWalletItems(filteredWallets, txHistoryCount) + currentStateProvider().recipientList.update { - txHistory.filter { item -> - val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer - val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User - val isSingleAddress = if (item.isOutgoing) { - item.destinationType is TxHistoryItem.DestinationType.Single - } else { - item.sourceType is TxHistoryItem.SourceType.Single - } - isTransfer && isSingleAddress && isNotContract - }.map { tx -> - SendRecipientListContent.Item( - id = tx.txHash, - title = tx.extractAddress(), - subtitle = TextReference.Str(tx.getAmount()), - info = tx.extractTimestamp(), - subtitleIconRes = tx.extractIconRes(), - ) - }.insertWallets(filteredWallets) + if (txHistoryCount == 0) { + PagingData.from(listOf(walletsItem)) + } else { + txHistory.filter { item -> + val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer + val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User + val isSingleAddress = if (item.isOutgoing) { + item.destinationType is TxHistoryItem.DestinationType.Single + } else { + item.sourceType is TxHistoryItem.SourceType.Single + } + isTransfer && isSingleAddress && isNotContract + }.map { tx -> + SendRecipientListContent.Item( + id = tx.txHash, + title = tx.extractAddress(), + subtitle = TextReference.Str(tx.getAmount()), + info = tx.extractTimestamp(), + subtitleIconRes = tx.extractIconRes(), + ) + }.insertWallets(walletsItem) + } } } private fun PagingData.insertWallets( - wallets: List, + wallets: SendRecipientListContent.Wallets, ): PagingData { return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> return@insertSeparators when { - before == null && after is SendRecipientListContent.Item -> { - SendRecipientListContent.Wallets( - wallets.map { - SendRecipientListContent.Item( - id = it.address, - title = TextReference.Str(it.address), - subtitle = TextReference.Str(it.name), - ) - }.toPersistentList(), - ) - } + before == null && after is SendRecipientListContent.Item -> wallets else -> null } } } + private fun getWalletItems(wallets: List, txHistoryCount: Int): SendRecipientListContent.Wallets { + return SendRecipientListContent.Wallets( + wallets.map { + SendRecipientListContent.Item( + id = it.address, + title = TextReference.Str(it.address), + subtitle = TextReference.Str(it.name), + ) + }.toPersistentList(), + isWalletsOnly = txHistoryCount == 0, + ) + } + private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) { when (val destination = destinationType) { is TxHistoryItem.DestinationType.Multiple -> TextReference.Res( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index ada2667527..30563155b4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -80,6 +80,7 @@ internal fun SendRecipientContent( } } +@Suppress("LongMethod") @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.recipientListItem( recipientList: LazyPagingItems, @@ -106,10 +107,17 @@ private fun LazyListScope.recipientListItem( .padding(top = TangemTheme.dimens.spacing20) .then( if (index == 0) { + val bottomRadius = if (item.isWalletsOnly) { + TangemTheme.dimens.radius12 + } else { + TangemTheme.dimens.radius0 + } Modifier.clip( RoundedCornerShape( topEnd = TangemTheme.dimens.radius12, topStart = TangemTheme.dimens.radius12, + bottomStart = bottomRadius, + bottomEnd = bottomRadius, ), ) } else { @@ -182,18 +190,25 @@ private fun RecipientWalletListItem( onClick = { clickIntents.onRecipientAddressValueChange(title) }, ) } - Text( - text = stringResource(R.string.send_recent_transactions), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - ) + if (!item.isWalletsOnly) { + val topPadding = if (item.list.isNotEmpty()) { + TangemTheme.dimens.spacing8 + } else { + TangemTheme.dimens.spacing0 + } + Text( + text = stringResource(R.string.send_recent_transactions), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding( + top = topPadding, + bottom = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) + } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 3e8bc7477f..02f2caefc6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet @@ -57,6 +58,7 @@ internal class SendViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getFeeUseCase: GetFeeUseCase, private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, @@ -97,7 +99,6 @@ internal class SendViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { getWalletAddresses() subscribeOnCurrencyStatusUpdates(owner) - getWalletsAndRecent() getFee() } @@ -134,6 +135,7 @@ internal class SendViewModel @Inject constructor( .onEach { either -> either.onRight { cryptoCurrencyStatus = it + getWalletsAndRecent() uiState = stateFactory.getReadyState() } } @@ -158,10 +160,12 @@ internal class SendViewModel @Inject constructor( combine( flow = getUserWallets().conflate(), flow2 = getTxHistory().conflate(), - ) { wallets, txHistory -> + flow3 = getTxHistoryCount().conflate(), + ) { wallets, txHistory, txHistoryCount -> stateFactory.onLoadedRecipientList( wallets = wallets, txHistory = txHistory, + txHistoryCount = txHistoryCount, ) } .flowOn(dispatchers.io) @@ -213,6 +217,18 @@ internal class SendViewModel @Inject constructor( } } + private fun getTxHistoryCount(): Flow { + return flow { + txHistoryItemsCountUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifRight = { emit(it) }, + ifLeft = { emit(0) }, + ) + } + } + private fun getFee() { viewModelScope.launch(dispatchers.main) { uiState.currentState From 7fdf9167eab5bbc0736ce5e02f38edfc8693a7e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Nov 2023 21:21:39 +0800 Subject: [PATCH 08/11] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 6 -- .../datasource/di/AppCurrencyDataModule.kt | 26 +---- .../datasource/di/AppThemeModeDataModule.kt | 29 ------ .../tangem/datasource/di/CardDataModule.kt | 34 ------- .../datasource/di/HiddenBalanceDataModule.kt | 32 ------ .../datasource/di/SettingsDataModule.kt | 50 ---------- .../tangem/datasource/di/WalletsDataModule.kt | 26 ----- .../datasource/local/AppPreferenceStorage.kt | 1 + .../appcurrency/BalanceHidingSettingsStore.kt | 13 --- .../appcurrency/SelectedAppCurrencyStore.kt | 15 --- .../BalanceStateHidingSettingsStore.kt | 19 ---- .../DefaultSelectedAppCurrencyStore.kt | 10 -- .../local/apptheme/AppThemeModeStore.kt | 13 --- .../apptheme/DefaultAppThemeModeStore.kt | 9 -- .../datasource/local/card/UsedCardsStore.kt | 21 ---- .../BooleanSharedPreferencesDataStore.kt | 16 --- .../IntSharedPreferencesDataStore.kt | 16 --- .../JsonSharedPreferencesDataStore.kt | 24 ----- .../LongSharedPreferencesDataStore.kt | 16 --- .../datastore/SharedPreferencesDataStore.kt | 98 ------------------- .../local/preferences/PreferencesDataStore.kt | 34 ++++++- .../local/preferences/PreferencesKeys.kt | 40 +++++++- .../local/settings/AppLaunchCountStore.kt | 21 ---- .../settings/AppRatingShowingCountStore.kt | 21 ---- .../settings/FundsFoundDateInMillisStore.kt | 21 ---- .../settings/UserInteractingStatusStore.kt | 21 ---- .../userwallet/ShouldSaveUserWalletStore.kt | 24 ----- data/app-currency/build.gradle.kts | 5 +- .../DefaultAppCurrencyRepository.kt | 31 +++--- .../appcurrency/di/AppCurrencyDataModule.kt | 14 +-- data/app-theme/build.gradle.kts | 3 + .../apptheme/DefaultAppThemeModeRepository.kt | 32 +++--- .../apptheme/di/AppThemeModeDataModule.kt | 10 +- data/balance-hiding/build.gradle.kts | 2 +- .../DefaultBalanceHidingRepository.kt | 43 ++++---- .../balancehiding/di/BalanceHidingModule.kt | 15 +-- data/card/build.gradle.kts | 2 + .../tangem/data/card/DefaultCardRepository.kt | 44 ++++----- .../com/tangem/data/card/di/CardDataModule.kt | 10 +- data/settings/build.gradle.kts | 19 ++-- .../settings/DefaultAppRatingRepository.kt | 84 ++++++---------- .../data/settings/di/SettingsDataModule.kt | 21 +--- data/tokens/build.gradle.kts | 3 + .../tangem/data/tokens/di/TokensDataModule.kt | 6 +- .../repository/DefaultQuotesRepository.kt | 23 +++-- data/wallets/build.gradle.kts | 4 +- .../data/wallets/DefaultWalletsRepository.kt | 25 +++-- .../data/wallets/di/WalletsDataModule.kt | 15 +-- .../repositories/AppRatingRepository.kt | 3 - .../wallets/repository/WalletsRepository.kt | 2 - 50 files changed, 256 insertions(+), 816 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/CardDataModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/HiddenBalanceDataModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/SettingsDataModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/BalanceHidingSettingsStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardsStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/BooleanSharedPreferencesDataStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/IntSharedPreferencesDataStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/JsonSharedPreferencesDataStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/LongSharedPreferencesDataStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/settings/AppLaunchCountStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/settings/AppRatingShowingCountStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/settings/FundsFoundDateInMillisStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/settings/UserInteractingStatusStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index c67f289238..420a6ff091 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -30,7 +30,6 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig -import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -209,9 +208,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var userTokensStore: UserTokensStore - @Inject - lateinit var appRatingRepository: AppRatingRepository - @Inject lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase @@ -248,10 +244,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { // TODO: Try to performance and user experience. // [REDACTED_JIRA] runBlocking { - walletsRepository.initialize() initUserWalletsListManager() featureTogglesManager.init() - appRatingRepository.initialize() // learn2earnInteractor.init() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt index e25e15471b..fade673123 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt @@ -1,18 +1,11 @@ package com.tangem.datasource.di -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore -import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore -import com.tangem.datasource.local.appcurrency.implementation.DefaultSelectedAppCurrencyStore -import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore import com.tangem.datasource.local.datastore.RuntimeDataStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -23,23 +16,6 @@ internal object AppCurrencyDataModule { @Provides @Singleton fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore { - return DefaultAvailableAppCurrenciesStore( - dataStore = RuntimeDataStore(), - ) - } - - @Provides - @Singleton - fun provideSelectedAppCurrencyStore( - @ApplicationContext context: Context, - @NetworkMoshi moshi: Moshi, - ): SelectedAppCurrencyStore { - return DefaultSelectedAppCurrencyStore( - dataStore = JsonSharedPreferencesDataStore( - preferencesName = "selected_app_currency", - context = context, - adapter = moshi.adapter(CurrenciesResponse.Currency::class.java), - ), - ) + return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore()) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt deleted file mode 100644 index a6e8020146..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.datasource.di - -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.datasource.local.apptheme.AppThemeModeStore -import com.tangem.datasource.local.apptheme.DefaultAppThemeModeStore -import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore -import com.tangem.domain.apptheme.model.AppThemeMode -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal object AppThemeModeDataModule { - - @Provides - fun provideAppThemeModeStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): AppThemeModeStore { - return DefaultAppThemeModeStore( - dataStore = JsonSharedPreferencesDataStore( - preferencesName = "app_theme", - context = context, - adapter = moshi.adapter(AppThemeMode::class.java), - ), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/CardDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/CardDataModule.kt deleted file mode 100644 index f06e5ac8e5..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/CardDataModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.datasource.di - -import android.content.Context -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.datasource.local.card.DefaultUsedCardsStore -import com.tangem.datasource.local.card.UsedCardInfo -import com.tangem.datasource.local.card.UsedCardsStore -import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object CardDataModule { - - @Provides - @Singleton - fun provideUsedCardsStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): UsedCardsStore { - return DefaultUsedCardsStore( - store = JsonSharedPreferencesDataStore( - preferencesName = "tapPrefs", - context = context, - adapter = moshi.adapter( - Types.newParameterizedType(List::class.java, UsedCardInfo::class.java), - ), - ), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/HiddenBalanceDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/HiddenBalanceDataModule.kt deleted file mode 100644 index 9b59f30477..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/HiddenBalanceDataModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.datasource.di - -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore -import com.tangem.datasource.local.appcurrency.implementation.BalanceStateHidingSettingsStore -import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore -import com.tangem.domain.balancehiding.BalanceHidingSettings -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal object HiddenBalanceDataModule { - - @Provides - fun provideHiddenBalanceStateStore( - @ApplicationContext context: Context, - @NetworkMoshi moshi: Moshi, - ): BalanceHidingSettingsStore { - return BalanceStateHidingSettingsStore( - dataStore = JsonSharedPreferencesDataStore( - preferencesName = "balance_hiding_settings", - context = context, - adapter = moshi.adapter(BalanceHidingSettings::class.java), - ), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SettingsDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SettingsDataModule.kt deleted file mode 100644 index ba757806d6..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SettingsDataModule.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.datasource.di - -import android.content.Context -import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore -import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore -import com.tangem.datasource.local.datastore.LongSharedPreferencesDataStore -import com.tangem.datasource.local.settings.* -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object SettingsDataModule { - - @Provides - @Singleton - fun provideAppLaunchCountStore(@ApplicationContext context: Context): AppLaunchCountStore { - return DefaultAppLaunchCountStore( - store = IntSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context), - ) - } - - @Provides - @Singleton - fun provideAppRatingShowingCountStore(@ApplicationContext context: Context): AppRatingShowingCountStore { - return DefaultAppRatingShowingCountStore( - store = IntSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context), - ) - } - - @Provides - @Singleton - fun provideFundsFoundDateInMillisStore(@ApplicationContext context: Context): FundsFoundDateInMillisStore { - return DefaultFundsFoundDateInMillisStore( - store = LongSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context), - ) - } - - @Provides - @Singleton - fun provideUserInteractingStatusStore(@ApplicationContext context: Context): UserInteractingStatusStore { - return DefaultUserInteractingStatusStore( - store = BooleanSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt deleted file mode 100644 index 61cdcacad3..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.datasource.di - -import android.content.Context -import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore -import com.tangem.datasource.local.settings.* -import com.tangem.datasource.local.userwallet.DefaultShouldSaveUserWalletStore -import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object WalletsDataModule { - - @Provides - @Singleton - fun provideShouldSaveUserWalletsStore(@ApplicationContext context: Context): ShouldSaveUserWalletStore { - return DefaultShouldSaveUserWalletStore( - store = BooleanSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt index 3e30c7344b..d56ca29fa0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt @@ -5,6 +5,7 @@ package com.tangem.datasource.local * [REDACTED_AUTHOR] */ +@Deprecated(message = "Use AppPreferencesStore", level = DeprecationLevel.WARNING) interface AppPreferenceStorage { /** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/BalanceHidingSettingsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/BalanceHidingSettingsStore.kt deleted file mode 100644 index a29714251b..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/BalanceHidingSettingsStore.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.datasource.local.appcurrency - -import com.tangem.domain.balancehiding.BalanceHidingSettings -import kotlinx.coroutines.flow.Flow - -interface BalanceHidingSettingsStore { - - fun get(): Flow - - suspend fun getSyncOrDefault(): BalanceHidingSettings - - suspend fun store(settings: BalanceHidingSettings) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt deleted file mode 100644 index e732ce8415..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.datasource.local.appcurrency - -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse -import kotlinx.coroutines.flow.Flow - -interface SelectedAppCurrencyStore { - - fun get(): Flow - - suspend fun getSyncOrNull(): CurrenciesResponse.Currency? - - suspend fun store(item: CurrenciesResponse.Currency) - - suspend fun isEmpty(): Boolean -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt deleted file mode 100644 index 0314d6549d..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.datasource.local.appcurrency.implementation - -import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import com.tangem.datasource.local.datastore.core.StringKeyDataStore -import com.tangem.domain.balancehiding.BalanceHidingSettings - -internal class BalanceStateHidingSettingsStore( - dataStore: StringKeyDataStore, -) : BalanceHidingSettingsStore, KeylessDataStoreDecorator(dataStore) { - - override suspend fun getSyncOrDefault(): BalanceHidingSettings { - return getSyncOrNull() ?: BalanceHidingSettings( - isHidingEnabledInSettings = false, - isBalanceHidden = false, - isBalanceHidingNotificationEnabled = true, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt deleted file mode 100644 index c79bf3045b..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.datasource.local.appcurrency.implementation - -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse -import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import com.tangem.datasource.local.datastore.core.StringKeyDataStore - -internal class DefaultSelectedAppCurrencyStore( - dataStore: StringKeyDataStore, -) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt deleted file mode 100644 index 78e1fd514c..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.datasource.local.apptheme - -import com.tangem.domain.apptheme.model.AppThemeMode -import kotlinx.coroutines.flow.Flow - -interface AppThemeModeStore { - - fun get(): Flow - - suspend fun store(item: AppThemeMode) - - suspend fun isEmpty(): Boolean -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt deleted file mode 100644 index 483bf4b525..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.datasource.local.apptheme - -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import com.tangem.datasource.local.datastore.core.StringKeyDataStore -import com.tangem.domain.apptheme.model.AppThemeMode - -internal class DefaultAppThemeModeStore( - dataStore: StringKeyDataStore, -) : AppThemeModeStore, KeylessDataStoreDecorator(dataStore) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardsStore.kt deleted file mode 100644 index 0d54995dbb..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardsStore.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.local.card - -import com.tangem.datasource.local.datastore.SharedPreferencesDataStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import kotlinx.coroutines.flow.Flow - -interface UsedCardsStore { - - fun get(): Flow> - - suspend fun getSyncOrNull(): List? - - suspend fun store(item: List) -} - -internal class DefaultUsedCardsStore( - store: SharedPreferencesDataStore>, -) : UsedCardsStore, KeylessDataStoreDecorator>( - wrappedDataStore = store, - key = "usedCardsInfo_v2", -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/BooleanSharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/BooleanSharedPreferencesDataStore.kt deleted file mode 100644 index 36141207c0..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/BooleanSharedPreferencesDataStore.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.datasource.local.datastore - -import android.content.Context -import androidx.core.content.edit - -internal class BooleanSharedPreferencesDataStore( - preferencesName: String, - context: Context, -) : SharedPreferencesDataStore(preferencesName, context) { - - override fun getByKey(key: String): Boolean = sharedPreferences.getBoolean(key, false) - - override fun storeByKey(key: String, value: Boolean) { - sharedPreferences.edit { putBoolean(key, value) } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/IntSharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/IntSharedPreferencesDataStore.kt deleted file mode 100644 index 7ef0f70fa6..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/IntSharedPreferencesDataStore.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.datasource.local.datastore - -import android.content.Context -import androidx.core.content.edit - -internal class IntSharedPreferencesDataStore( - preferencesName: String, - context: Context, -) : SharedPreferencesDataStore(preferencesName, context) { - - override fun getByKey(key: String): Int = sharedPreferences.getInt(key, 0) - - override fun storeByKey(key: String, value: Int) { - sharedPreferences.edit { putInt(key, value) } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/JsonSharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/JsonSharedPreferencesDataStore.kt deleted file mode 100644 index 5e47ee8114..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/JsonSharedPreferencesDataStore.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.datasource.local.datastore - -import android.content.Context -import androidx.core.content.edit -import com.squareup.moshi.JsonAdapter - -internal class JsonSharedPreferencesDataStore( - preferencesName: String, - context: Context, - private val adapter: JsonAdapter, -) : SharedPreferencesDataStore(preferencesName, context) { - - override fun getByKey(key: String): Value? { - val json = sharedPreferences.getString(key, null) ?: return null - - return adapter.fromJson(json) - } - - override fun storeByKey(key: String, value: Value) { - val json = adapter.toJson(value) - - sharedPreferences.edit { putString(key, json) } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/LongSharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/LongSharedPreferencesDataStore.kt deleted file mode 100644 index b1bb00e4e1..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/LongSharedPreferencesDataStore.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.datasource.local.datastore - -import android.content.Context -import androidx.core.content.edit - -internal class LongSharedPreferencesDataStore( - preferencesName: String, - context: Context, -) : SharedPreferencesDataStore(preferencesName, context) { - - override fun getByKey(key: String): Long = sharedPreferences.getLong(key, 0) - - override fun storeByKey(key: String, value: Long) { - sharedPreferences.edit { putLong(key, value) } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt deleted file mode 100644 index 417b4e9ac4..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.tangem.datasource.local.datastore - -import android.content.Context -import android.content.Context.MODE_PRIVATE -import android.content.SharedPreferences -import androidx.core.content.edit -import com.tangem.datasource.local.datastore.core.StringKeyDataStore -import com.tangem.datasource.local.datastore.utils.Trigger -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.mapNotNull -import timber.log.Timber - -internal abstract class SharedPreferencesDataStore( - preferencesName: String, - context: Context, -) : StringKeyDataStore { - - protected val sharedPreferences: SharedPreferences by lazy { - context.getSharedPreferences(preferencesName, MODE_PRIVATE) - } - - private val writeTrigger = Trigger() - - abstract fun getByKey(key: String): Value? - - abstract fun storeByKey(key: String, value: Value) - - override suspend fun isEmpty(): Boolean { - return sharedPreferences.all.isEmpty() - } - - override suspend fun contains(key: String): Boolean { - return sharedPreferences.contains(key) - } - - override fun get(key: String): Flow { - return writeTrigger - .mapNotNull { getInternal(key) } - .distinctUntilChanged() - } - - override fun getAll(): Flow> { - throw UnsupportedOperationException("Unknown key") - } - - override suspend fun getSyncOrNull(key: String): Value? = getInternal(key) - - override suspend fun getAllSyncOrNull(): List { - throw UnsupportedOperationException("Unknown key") - } - - override suspend fun store(key: String, value: Value) { - try { - storeByKey(key, value) - writeTrigger.trigger() - } catch (e: Throwable) { - Timber.e(e, "Unable to edit preferences: $key") - } - } - - override suspend fun store(values: Map) { - values.forEach { (key, item) -> - store(key, item) - } - } - - override suspend fun remove(key: String) { - sharedPreferences.edit { remove(key) } - writeTrigger.trigger() - } - - override suspend fun remove(keys: Collection) { - sharedPreferences.edit { - keys.forEach { key -> - remove(key) - } - } - - writeTrigger.trigger() - } - - override suspend fun clear() { - sharedPreferences.edit { clear() } - writeTrigger.trigger() - } - - private fun getInternal(key: String): Value? { - if (!sharedPreferences.contains(key)) return null - - return try { - getByKey(key) - } catch (e: Throwable) { - Timber.e(e, "Unable to get value from preferences: $key") - null - } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt index 7d0d6ba94a..9e9b278b7e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt @@ -4,11 +4,13 @@ import android.content.Context import androidx.datastore.core.DataMigration import androidx.datastore.core.DataStore import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.SharedPreferencesMigration import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStoreFile import com.tangem.datasource.local.preferences.PreferencesDataStore.INSTANCE +import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import timber.log.Timber @@ -23,6 +25,8 @@ import kotlin.coroutines.CoroutineContext internal object PreferencesDataStore { private const val PREFERENCES_FILE_NAME = "TAP_PREFS" + private const val LEGACY_TAP_PREFS_FILE_NAME = "tapPrefs" + private const val LEGACY_DEFAULT_KEY_NAME = "key" private var INSTANCE: DataStore? = null @@ -33,7 +37,7 @@ internal object PreferencesDataStore { private fun create(context: Context, dispatcher: CoroutineContext): DataStore { return PreferenceDataStoreFactory.create( corruptionHandler = createCorruptionHandler(), - migrations = createMigrations(), + migrations = createMigrations(context = context), scope = CoroutineScope(context = dispatcher + SupervisorJob()), produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) }, ) @@ -48,7 +52,31 @@ internal object PreferencesDataStore { ) } - private fun createMigrations(): List> { - return listOf() + private fun createMigrations(context: Context): List> { + return listOf( + SharedPreferencesMigration( + context = context, + sharedPreferencesName = LEGACY_TAP_PREFS_FILE_NAME, + keysToMigrate = getTapPrefKeysToMigrate(), + ), + SharedPreferencesKeyMigration( + context = context, + legacyPrefsName = "app_theme", + legacyKeyName = LEGACY_DEFAULT_KEY_NAME, + keyName = PreferencesKeys.APP_THEME_MODE_KEY.name, + ), + SharedPreferencesKeyMigration( + context = context, + legacyPrefsName = "selected_app_currency", + legacyKeyName = LEGACY_DEFAULT_KEY_NAME, + keyName = PreferencesKeys.SELECTED_APP_CURRENCY_KEY.name, + ), + SharedPreferencesKeyMigration( + context = context, + legacyPrefsName = "balance_hiding_settings", + legacyKeyName = LEGACY_DEFAULT_KEY_NAME, + keyName = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY.name, + ), + ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 90f2b61270..d81065c147 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -1,13 +1,49 @@ package com.tangem.datasource.local.preferences +import androidx.datastore.preferences.core.* +import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY + /** * All preferences keys that DataStore is stored. * [REDACTED_AUTHOR] */ -object PreferencesKeys +object PreferencesKeys { + + val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } + + val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } + + val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") } + + val FUNDS_FOUND_DATE_KEY by lazy { longPreferencesKey(name = "fundsFoundDate") } + + val USER_WAS_INTERACT_WITH_RATING_KEY by lazy { booleanPreferencesKey(name = "userWasInteractWithRating") } + + val USED_CARDS_INFO_KEY by lazy { stringPreferencesKey(name = "usedCardsInfo_v2") } + + val APP_THEME_MODE_KEY by lazy { stringPreferencesKey(name = "appThemeMode") } + + val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") } + + val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") } +} /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ internal fun getTapPrefKeysToMigrate(): Set { - return setOf() + return setOf( + SAVE_USER_WALLETS_KEY, + APP_LAUNCH_COUNT_KEY, + SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY, + FUNDS_FOUND_DATE_KEY, + USER_WAS_INTERACT_WITH_RATING_KEY, + USED_CARDS_INFO_KEY, + ) + .map(Preferences.Key<*>::name) + .toSet() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/settings/AppLaunchCountStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/settings/AppLaunchCountStore.kt deleted file mode 100644 index a2ee494307..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/settings/AppLaunchCountStore.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.local.settings - -import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import kotlinx.coroutines.flow.Flow - -interface AppLaunchCountStore { - - fun get(): Flow - - suspend fun getSyncOrNull(): Int? - - suspend fun store(item: Int) -} - -internal class DefaultAppLaunchCountStore( - store: IntSharedPreferencesDataStore, -) : AppLaunchCountStore, KeylessDataStoreDecorator( - wrappedDataStore = store, - key = "launchCount", -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/settings/AppRatingShowingCountStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/settings/AppRatingShowingCountStore.kt deleted file mode 100644 index dfbb452f22..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/settings/AppRatingShowingCountStore.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.local.settings - -import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import kotlinx.coroutines.flow.Flow - -interface AppRatingShowingCountStore { - - fun get(): Flow - - suspend fun getSyncOrNull(): Int? - - suspend fun store(item: Int) -} - -internal class DefaultAppRatingShowingCountStore( - store: IntSharedPreferencesDataStore, -) : AppRatingShowingCountStore, KeylessDataStoreDecorator( - wrappedDataStore = store, - key = "showRatingDialogAtLaunchCount", -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/settings/FundsFoundDateInMillisStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/settings/FundsFoundDateInMillisStore.kt deleted file mode 100644 index 1e6a50f959..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/settings/FundsFoundDateInMillisStore.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.local.settings - -import com.tangem.datasource.local.datastore.LongSharedPreferencesDataStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import kotlinx.coroutines.flow.Flow - -interface FundsFoundDateInMillisStore { - - fun get(): Flow - - suspend fun getSyncOrNull(): Long? - - suspend fun store(item: Long) -} - -internal class DefaultFundsFoundDateInMillisStore( - store: LongSharedPreferencesDataStore, -) : FundsFoundDateInMillisStore, KeylessDataStoreDecorator( - wrappedDataStore = store, - key = "fundsFoundDate", -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/settings/UserInteractingStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/settings/UserInteractingStatusStore.kt deleted file mode 100644 index 1a1ed3bf03..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/settings/UserInteractingStatusStore.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.local.settings - -import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import kotlinx.coroutines.flow.Flow - -interface UserInteractingStatusStore { - - fun get(): Flow - - suspend fun getSyncOrNull(): Boolean? - - suspend fun store(item: Boolean) -} - -internal class DefaultUserInteractingStatusStore( - store: BooleanSharedPreferencesDataStore, -) : UserInteractingStatusStore, KeylessDataStoreDecorator( - wrappedDataStore = store, - key = "userWasInteractWithRating", -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt deleted file mode 100644 index cbc20ba792..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.datasource.local.userwallet - -import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore -import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator -import kotlinx.coroutines.flow.Flow - -/** -[REDACTED_AUTHOR] - */ -interface ShouldSaveUserWalletStore { - - fun get(): Flow - - suspend fun getSyncOrNull(): Boolean? - - suspend fun store(item: Boolean) -} - -internal class DefaultShouldSaveUserWalletStore( - store: BooleanSharedPreferencesDataStore, -) : ShouldSaveUserWalletStore, KeylessDataStoreDecorator( - wrappedDataStore = store, - key = "saveUserWallets", -) \ No newline at end of file diff --git a/data/app-currency/build.gradle.kts b/data/app-currency/build.gradle.kts index 69613e2e85..ca214f10a9 100644 --- a/data/app-currency/build.gradle.kts +++ b/data/app-currency/build.gradle.kts @@ -23,12 +23,15 @@ dependencies { /** Project - Utils */ implementation(projects.core.utils) + /** AndroidX */ + implementation(deps.androidx.datastore) + /** DI */ implementation(deps.hilt.core) kapt(deps.hilt.kapt) /** Other */ + implementation(deps.jodatime) implementation(deps.kotlin.coroutines) implementation(deps.timber) - implementation(deps.jodatime) } \ No newline at end of file diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index 520be9c24d..bce54591e8 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -6,12 +6,16 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore -import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -19,23 +23,25 @@ import org.joda.time.Duration internal class DefaultAppCurrencyRepository( private val tangemTechApi: TangemTechApi, + private val appPreferencesStore: AppPreferencesStore, private val availableAppCurrenciesStore: AvailableAppCurrenciesStore, - private val selectedAppCurrencyStore: SelectedAppCurrencyStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : AppCurrencyRepository { private val appCurrencyConverter = AppCurrencyConverter() - override fun getSelectedAppCurrency(): Flow = channelFlow { - launch(dispatchers.io) { - selectedAppCurrencyStore.get() - .map(appCurrencyConverter::convert) - .collect(::send) - } + override fun getSelectedAppCurrency(): Flow { + return channelFlow { + launch { + appPreferencesStore + .getObject(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + .filterNotNull() + .map(appCurrencyConverter::convert) + .collect(::send) + } - withContext(dispatchers.io) { - if (selectedAppCurrencyStore.isEmpty()) { + withContext(dispatchers.io) { fetchDefaultAppCurrency() } } @@ -61,7 +67,10 @@ internal class DefaultAppCurrencyRepository( "Unable to find app currency with provided code: $currencyCode" } - selectedAppCurrencyStore.store(currency) + appPreferencesStore.storeObject( + key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + value = currency, + ) } } diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt index cd80c7c52b..d9cb9be5f7 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt @@ -4,7 +4,7 @@ import com.tangem.data.appcurrency.DefaultAppCurrencyRepository import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore -import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -21,17 +21,17 @@ internal object AppCurrencyDataModule { @Singleton fun provideAppCurrencyRepository( tangemTechApi: TangemTechApi, + appPreferencesStore: AppPreferencesStore, availableAppCurrenciesStore: AvailableAppCurrenciesStore, - selectedAppCurrencyStore: SelectedAppCurrencyStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): AppCurrencyRepository { return DefaultAppCurrencyRepository( - tangemTechApi, - availableAppCurrenciesStore, - selectedAppCurrencyStore, - cacheRegistry, - dispatchers, + tangemTechApi = tangemTechApi, + appPreferencesStore = appPreferencesStore, + availableAppCurrenciesStore = availableAppCurrenciesStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/app-theme/build.gradle.kts b/data/app-theme/build.gradle.kts index 7996a21c65..92a28083d7 100644 --- a/data/app-theme/build.gradle.kts +++ b/data/app-theme/build.gradle.kts @@ -27,4 +27,7 @@ dependencies { /** Other */ implementation(deps.kotlin.coroutines) + + /** Local storages */ + implementation(deps.androidx.datastore) } \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt index 4d8ed9cf4c..8abef1097b 100644 --- a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt @@ -1,35 +1,25 @@ package com.tangem.data.apptheme -import com.tangem.datasource.local.apptheme.AppThemeModeStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.repository.AppThemeModeRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.Flow internal class DefaultAppThemeModeRepository( - private val appThemeModeStore: AppThemeModeStore, - private val dispatchers: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, ) : AppThemeModeRepository { override fun getAppThemeMode(): Flow { - return channelFlow { - launch(dispatchers.io) { - if (appThemeModeStore.isEmpty()) { - appThemeModeStore.store(AppThemeMode.DEFAULT) - } - } - - launch(dispatchers.io) { - appThemeModeStore.get().collect(::send) - } - } + return appPreferencesStore.getObject( + key = PreferencesKeys.APP_THEME_MODE_KEY, + default = AppThemeMode.DEFAULT, + ) } override suspend fun changeAppThemeMode(mode: AppThemeMode) { - withContext(dispatchers.io) { - appThemeModeStore.store(mode) - } + appPreferencesStore.storeObject(key = PreferencesKeys.APP_THEME_MODE_KEY, value = mode) } } \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt index a360ba19b6..11925c23cd 100644 --- a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt @@ -1,9 +1,8 @@ package com.tangem.data.apptheme.di import com.tangem.data.apptheme.DefaultAppThemeModeRepository -import com.tangem.datasource.local.apptheme.AppThemeModeStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.apptheme.repository.AppThemeModeRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -16,10 +15,7 @@ internal object AppThemeModeDataModule { @Provides @Singleton - fun provideAppThemeModeRepository( - appThemeModeStore: AppThemeModeStore, - dispatchers: CoroutineDispatcherProvider, - ): AppThemeModeRepository { - return DefaultAppThemeModeRepository(appThemeModeStore, dispatchers) + fun provideAppThemeModeRepository(appPreferencesStore: AppPreferencesStore): AppThemeModeRepository { + return DefaultAppThemeModeRepository(appPreferencesStore = appPreferencesStore) } } \ No newline at end of file diff --git a/data/balance-hiding/build.gradle.kts b/data/balance-hiding/build.gradle.kts index 434d3e0a34..84db19b2d7 100644 --- a/data/balance-hiding/build.gradle.kts +++ b/data/balance-hiding/build.gradle.kts @@ -11,10 +11,10 @@ android { } dependencies { + implementation(deps.androidx.datastore) /** DI */ implementation(deps.hilt.android) - kapt(deps.hilt.kapt) implementation(deps.kotlin.coroutines) diff --git a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultBalanceHidingRepository.kt b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultBalanceHidingRepository.kt index 879bef5f17..948ec336de 100644 --- a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultBalanceHidingRepository.kt +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultBalanceHidingRepository.kt @@ -1,36 +1,41 @@ package com.tangem.data.balancehiding -import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrDefault +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.withContext internal class DefaultBalanceHidingRepository( - private val balanceHidingSettingsStore: BalanceHidingSettingsStore, - private val dispatchers: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, ) : BalanceHidingRepository { override fun getBalanceHidingSettingsFlow(): Flow { - return balanceHidingSettingsStore.get() - .onStart { emit(getBalanceHidingSettings()) } - .flowOn(dispatchers.io) - .distinctUntilChanged() + return appPreferencesStore.getObject( + key = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY, + default = DEFAULT_HIDING_SETTINGS, + ) } - override suspend fun storeBalanceHidingSettings(balanceHidingSettings: BalanceHidingSettings) { - withContext(dispatchers.io) { - balanceHidingSettingsStore.store(balanceHidingSettings) - } + override suspend fun storeBalanceHidingSettings(isBalanceHidden: BalanceHidingSettings) { + appPreferencesStore.storeObject(key = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY, value = isBalanceHidden) } override suspend fun getBalanceHidingSettings(): BalanceHidingSettings { - return withContext(dispatchers.io) { - balanceHidingSettingsStore.getSyncOrDefault() - } + return appPreferencesStore.getObjectSyncOrDefault( + key = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY, + default = DEFAULT_HIDING_SETTINGS, + ) + } + + private companion object { + val DEFAULT_HIDING_SETTINGS = BalanceHidingSettings( + isHidingEnabledInSettings = false, + isBalanceHidden = false, + isBalanceHidingNotificationEnabled = true, + ) } } \ No newline at end of file diff --git a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/di/BalanceHidingModule.kt b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/di/BalanceHidingModule.kt index 86b293b9fe..f83fac4932 100644 --- a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/di/BalanceHidingModule.kt +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/di/BalanceHidingModule.kt @@ -3,16 +3,15 @@ package com.tangem.data.balancehiding.di import android.content.Context import com.tangem.data.balancehiding.DefaultBalanceHidingRepository import com.tangem.data.balancehiding.DefaultDeviceFlipDetector -import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.balancehiding.DeviceFlipDetector import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -import dagger.hilt.android.qualifiers.ApplicationContext @Module @InstallIn(SingletonComponent::class) @@ -20,14 +19,8 @@ internal object BalanceHidingModule { @Provides @Singleton - fun provideBalanceHidingRepository( - balanceHidingSettingsStore: BalanceHidingSettingsStore, - dispatchers: CoroutineDispatcherProvider, - ): BalanceHidingRepository { - return DefaultBalanceHidingRepository( - balanceHidingSettingsStore = balanceHidingSettingsStore, - dispatchers = dispatchers, - ) + fun provideBalanceHidingRepository(appPreferencesStore: AppPreferencesStore): BalanceHidingRepository { + return DefaultBalanceHidingRepository(appPreferencesStore = appPreferencesStore) } @Provides diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index c7a692bd7e..6015c1ed3a 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -11,6 +11,8 @@ android { } dependencies { + implementation(deps.androidx.datastore) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt index 6f4a1fb553..7f0a56fd9a 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -1,43 +1,37 @@ package com.tangem.data.card import com.tangem.datasource.local.card.UsedCardInfo -import com.tangem.datasource.local.card.UsedCardsStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.domain.card.repository.CardRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.map internal class DefaultCardRepository( - private val usedCardsStore: UsedCardsStore, - private val dispatchers: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, ) : CardRepository { override fun wasCardScanned(cardId: String): Flow { - return channelFlow { - launch(dispatchers.io) { - usedCardsStore.get() - .collect { savedCards -> - send(element = savedCards.any { it.cardId == cardId }) - } + return appPreferencesStore.getObject>(key = PreferencesKeys.USED_CARDS_INFO_KEY) + .map { savedCards -> + savedCards?.any { it.cardId == cardId } ?: false } - - withContext(dispatchers.io) { - if (usedCardsStore.getSyncOrNull() == null) { - send(element = false) - } - } - } } override suspend fun setCardWasScanned(cardId: String) { - withContext(dispatchers.io) { - usedCardsStore.store( - item = usedCardsStore.getSyncOrNull() - ?.updateCard(cardId) - ?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)), + appPreferencesStore.editData { mutablePreferences -> + val usedCards: List? = mutablePreferences.getObject( + key = PreferencesKeys.USED_CARDS_INFO_KEY, + ) + + val updatedUsedCards = usedCards?.updateCard(cardId) + ?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)) + + mutablePreferences.setObject( + key = PreferencesKeys.USED_CARDS_INFO_KEY, + value = updatedUsedCards, ) } } diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt index 0c2cfdecbf..a526073cd2 100644 --- a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt +++ b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt @@ -4,10 +4,9 @@ import com.tangem.data.card.DefaultCardRepository import com.tangem.data.card.DefaultCardSdkConfigRepository import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.data.source.preferences.PreferencesDataSource -import com.tangem.datasource.local.card.UsedCardsStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -32,10 +31,7 @@ internal object CardDataModule { @Provides @Singleton - fun provideCardRepository( - usedCardsStore: UsedCardsStore, - dispatchers: CoroutineDispatcherProvider, - ): CardRepository { - return DefaultCardRepository(usedCardsStore = usedCardsStore, dispatchers = dispatchers) + fun provideCardRepository(appPreferencesStore: AppPreferencesStore): CardRepository { + return DefaultCardRepository(appPreferencesStore = appPreferencesStore) } } \ No newline at end of file diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index e59302acd9..5daa9b86f0 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -12,18 +12,19 @@ android { dependencies { + implementation(projects.core.datasource) + implementation(projects.core.utils) + + implementation(projects.data.source.preferences) + + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.settings) + + implementation(deps.androidx.datastore) + /** DI */ implementation(deps.hilt.android) - kapt(deps.hilt.kapt) implementation(deps.kotlin.coroutines) - - implementation(projects.core.utils) - implementation(projects.core.datasource) - - implementation(projects.domain.settings) - implementation(projects.domain.balanceHiding.models) - - implementation(projects.data.source.preferences) } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt index e7bc353372..78e99623f1 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt @@ -1,68 +1,42 @@ package com.tangem.data.settings -import com.tangem.datasource.local.settings.AppLaunchCountStore -import com.tangem.datasource.local.settings.AppRatingShowingCountStore -import com.tangem.datasource.local.settings.FundsFoundDateInMillisStore -import com.tangem.datasource.local.settings.UserInteractingStatusStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.edit +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.get import com.tangem.domain.settings.repositories.AppRatingRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.withContext import java.util.Calendar internal class DefaultAppRatingRepository( - private val fundsFoundDateInMillisStore: FundsFoundDateInMillisStore, - private val appLaunchCountStore: AppLaunchCountStore, - private val appRatingShowingCountStore: AppRatingShowingCountStore, - private val userInteractingStatusStore: UserInteractingStatusStore, - private val dispatchers: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, ) : AppRatingRepository { - override suspend fun initialize() { - fundsFoundDateInMillisStore.getSyncOrNull() - ?: fundsFoundDateInMillisStore.store(item = FUNDS_FOUND_DATE_UNDEFINED) - - appLaunchCountStore.getSyncOrNull() - ?: appLaunchCountStore.store(item = DEFAULT_APP_LAUNCH_COUNT) - - appRatingShowingCountStore.getSyncOrNull() - ?: appRatingShowingCountStore.store(item = FIRST_SHOWING_COUNT) - - userInteractingStatusStore.getSyncOrNull() - ?: userInteractingStatusStore.store(item = false) - } - override suspend fun setWalletWithFundsFound() { - withContext(dispatchers.io) { - val foundDate = fundsFoundDateInMillisStore.getSyncOrNull() - if (foundDate != null && foundDate != FUNDS_FOUND_DATE_UNDEFINED) return@withContext + appPreferencesStore.edit { mutablePreferences -> + val foundDate = mutablePreferences[PreferencesKeys.FUNDS_FOUND_DATE_KEY] + if (foundDate != null && foundDate != FUNDS_FOUND_DATE_UNDEFINED) return@edit - fundsFoundDateInMillisStore.store(item = Calendar.getInstance().timeInMillis) + mutablePreferences[PreferencesKeys.FUNDS_FOUND_DATE_KEY] = Calendar.getInstance().timeInMillis - val appLaunchCount = appLaunchCountStore.getSyncOrNull().let { count -> - if (count == null) { - appLaunchCountStore.store(item = DEFAULT_APP_LAUNCH_COUNT) - DEFAULT_APP_LAUNCH_COUNT - } else { - count - } - } - - appRatingShowingCountStore.store(item = appLaunchCount + FIRST_SHOWING_COUNT) - - userInteractingStatusStore.getSyncOrNull() - ?: userInteractingStatusStore.store(item = false) + val appLaunchCount = mutablePreferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY] ?: DEFAULT_APP_LAUNCH_COUNT + mutablePreferences[PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY] = + appLaunchCount + FIRST_SHOWING_COUNT } } override fun isReadyToShow(): Flow { // TODO: [REDACTED_JIRA] return combine( - userInteractingStatusStore.get(), - appRatingShowingCountStore.get(), - appLaunchCountStore.get(), - fundsFoundDateInMillisStore.get(), + appPreferencesStore.get(key = PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY, default = false), + appPreferencesStore.get( + key = PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY, + default = FIRST_SHOWING_COUNT, + ), + appPreferencesStore.get(key = PreferencesKeys.APP_LAUNCH_COUNT_KEY, default = DEFAULT_APP_LAUNCH_COUNT), + appPreferencesStore.get(key = PreferencesKeys.FUNDS_FOUND_DATE_KEY, default = FUNDS_FOUND_DATE_UNDEFINED), ) { isInteracting, ratingShowingCount, appLaunchCount, fundsFoundDate -> if (!isInteracting) { val diff = Calendar.getInstance().timeInMillis - fundsFoundDate @@ -77,20 +51,22 @@ internal class DefaultAppRatingRepository( } override suspend fun remindLater() { - appLaunchCountStore.getSyncOrNull()?.let { - updateNextShowing(at = it + DEFER_SHOWING_COUNT) + appPreferencesStore.edit { mutablePreferences -> + mutablePreferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY]?.let { + mutablePreferences.updateNextShowing(it + DEFER_SHOWING_COUNT) + } } } override suspend fun setNeverToShow() { - updateNextShowing(at = Int.MAX_VALUE) + appPreferencesStore.edit { mutablePreferences -> + mutablePreferences.updateNextShowing(at = Int.MAX_VALUE) + } } - private suspend fun updateNextShowing(at: Int) { - withContext(dispatchers.io) { - appRatingShowingCountStore.store(item = at) - userInteractingStatusStore.store(item = true) - } + private fun MutablePreferences.updateNextShowing(at: Int) { + this[PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY] = at + this[PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY] = true } private companion object { diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 68355c5d61..254235cf84 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -3,10 +3,7 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.data.source.preferences.PreferencesDataSource -import com.tangem.datasource.local.settings.AppLaunchCountStore -import com.tangem.datasource.local.settings.AppRatingShowingCountStore -import com.tangem.datasource.local.settings.FundsFoundDateInMillisStore -import com.tangem.datasource.local.settings.UserInteractingStatusStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -34,19 +31,7 @@ internal object SettingsDataModule { @Provides @Singleton - fun provideAppRatingRepository( - fundsFoundDateInMillisStore: FundsFoundDateInMillisStore, - appLaunchCountStore: AppLaunchCountStore, - appRatingShowingCountStore: AppRatingShowingCountStore, - userInteractingStatusStore: UserInteractingStatusStore, - dispatchers: CoroutineDispatcherProvider, - ): AppRatingRepository { - return DefaultAppRatingRepository( - fundsFoundDateInMillisStore = fundsFoundDateInMillisStore, - appLaunchCountStore = appLaunchCountStore, - appRatingShowingCountStore = appRatingShowingCountStore, - userInteractingStatusStore = userInteractingStatusStore, - dispatchers = dispatchers, - ) + fun provideAppRatingRepository(appPreferencesStore: AppPreferencesStore): AppRatingRepository { + return DefaultAppRatingRepository(appPreferencesStore = appPreferencesStore) } } \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index d36f42d591..88d7471830 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -32,6 +32,9 @@ dependencies { implementation(deps.tangem.blockchain) implementation(deps.tangem.card.core) + /** AndroidX */ + implementation(deps.androidx.datastore) + /** DI */ implementation(deps.hilt.core) kapt(deps.hilt.kapt) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index f8e33206dd..80ceefbdc9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -6,8 +6,8 @@ import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository import com.tangem.data.tokens.repository.DefaultNetworksRepository import com.tangem.data.tokens.repository.DefaultQuotesRepository import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore import com.tangem.datasource.local.network.NetworksStatusesStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore @@ -52,15 +52,15 @@ internal object TokensDataModule { @Singleton fun provideQuotesRepository( tangemTechApi: TangemTechApi, + appPreferencesStore: AppPreferencesStore, quotesStore: QuotesStore, - selectedAppCurrencyStore: SelectedAppCurrencyStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): QuotesRepository { return DefaultQuotesRepository( tangemTechApi = tangemTechApi, + appPreferencesStore = appPreferencesStore, quotesStore = quotesStore, - selectedAppCurrencyStore = selectedAppCurrencyStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 37a89fafd0..825f937b65 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -5,7 +5,12 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.QuotesConverter import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.local.* +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.quote.QuotesStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote @@ -17,8 +22,8 @@ import kotlinx.coroutines.withContext internal class DefaultQuotesRepository( private val tangemTechApi: TangemTechApi, + private val appPreferencesStore: AppPreferencesStore, private val quotesStore: QuotesStore, - private val selectedAppCurrencyStore: SelectedAppCurrencyStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : QuotesRepository { @@ -31,8 +36,11 @@ internal class DefaultQuotesRepository( @OptIn(ExperimentalCoroutinesApi::class) override fun getQuotesUpdates(currenciesIds: Set): Flow> { - return selectedAppCurrencyStore.get() + return appPreferencesStore.getObject( + key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ) .distinctUntilChanged() + .filterNotNull() .flatMapLatest { appCurrency -> fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false) @@ -44,9 +52,12 @@ internal class DefaultQuotesRepository( override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { return withContext(dispatchers.io) { - val selectedAppCurrency = requireNotNull(selectedAppCurrencyStore.getSyncOrNull()) { - "Unable to get selected application currency to update quotes" - } + val selectedAppCurrency = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ), + lazyMessage = { "Unable to get selected application currency to update quotes" }, + ) fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh) diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index f39f9b3c1e..c1a86f55ca 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -13,10 +13,12 @@ android { dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.data.source.preferences) implementation(projects.domain.wallets) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Local storages */ + implementation(deps.androidx.datastore) } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index af326a1a45..2cc72cd098 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -1,29 +1,26 @@ package com.tangem.data.wallets -import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.withContext internal class DefaultWalletsRepository( - private val shouldSaveUserWalletStore: ShouldSaveUserWalletStore, - private val dispatchers: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, ) : WalletsRepository { - override suspend fun initialize() { - withContext(dispatchers.io) { - shouldSaveUserWalletStore.getSyncOrNull() ?: shouldSaveUserWalletStore.store(item = false) - } - } - override suspend fun shouldSaveUserWalletsSync(): Boolean { - return withContext(dispatchers.io) { shouldSaveUserWalletStore.getSyncOrNull() ?: false } + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } - override fun shouldSaveUserWallets(): Flow = shouldSaveUserWalletStore.get() + override fun shouldSaveUserWallets(): Flow { + return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) + } override suspend fun saveShouldSaveUserWallets(item: Boolean) { - withContext(dispatchers.io) { shouldSaveUserWalletStore.store(item = item) } + appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item) } } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 80ff2c2567..29cd5fb5c0 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -1,9 +1,8 @@ package com.tangem.data.wallets.di import com.tangem.data.wallets.DefaultWalletsRepository -import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -12,17 +11,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object WalletsDataModule { +internal object WalletsDataModule { @Provides @Singleton - fun providesWalletsRepository( - shouldSaveUserWalletStore: ShouldSaveUserWalletStore, - coroutineDispatcherProvider: CoroutineDispatcherProvider, - ): WalletsRepository { - return DefaultWalletsRepository( - shouldSaveUserWalletStore = shouldSaveUserWalletStore, - dispatchers = coroutineDispatcherProvider, - ) + fun providesWalletsRepository(appPreferencesStore: AppPreferencesStore): WalletsRepository { + return DefaultWalletsRepository(appPreferencesStore = appPreferencesStore) } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/AppRatingRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/AppRatingRepository.kt index e10dc94046..33772c3cf6 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/AppRatingRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/AppRatingRepository.kt @@ -4,9 +4,6 @@ import kotlinx.coroutines.flow.Flow interface AppRatingRepository { - // FIXME: We must to initialize all stores before calling [isReadyToShow], otherwise flow will not emit data - suspend fun initialize() - suspend fun setWalletWithFundsFound() fun isReadyToShow(): Flow diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 6190b6d2e0..003f9b9ff2 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -4,8 +4,6 @@ import kotlinx.coroutines.flow.Flow interface WalletsRepository { - suspend fun initialize() - suspend fun shouldSaveUserWalletsSync(): Boolean fun shouldSaveUserWallets(): Flow From 872d52b0dda1ce19c9e547fc7dd05d08b9a9c09a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 13:13:57 +0300 Subject: [PATCH 09/11] Updated on 2026-08-14 --- .../ui/components/fields/SimpleTextField.kt | 60 ++++++ .../ui/components/inputrow/InputRowDefault.kt | 152 ++++++++++++++ .../ui/components/inputrow/InputRowEnter.kt | 167 +++++++++++++++ .../components/inputrow/InputRowEnterInfo.kt | 153 ++++++++++++++ .../ui/components/inputrow/InputRowImage.kt | 196 ++++++++++++++++++ .../components/inputrow/InputRowRecipient.kt | 168 +++++++++++++++ .../inputrow/InputRowRecipientDefault.kt | 106 ++++++++++ .../inputrow/inner/DividerContainer.kt | 31 +++ .../components/inputrow/inner/PasteButton.kt | 80 +++++++ 9 files changed, 1113 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt new file mode 100644 index 0000000000..7e0e1643c6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -0,0 +1,60 @@ +package com.tangem.core.ui.components.fields + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.input.VisualTransformation +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Simple text field with placeholder + */ +@Composable +fun SimpleTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: TextReference? = null, + singleLine: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + color: Color = TangemTheme.colors.text.primary1, + readOnly: Boolean = false, +) { + val focusRequester = remember { FocusRequester() } + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = TangemTheme.typography.body2.copy(color = color), + cursorBrush = SolidColor(TangemTheme.colors.text.primary1), + singleLine = singleLine, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + decorationBox = { textValue -> + Box { + if (value.isBlank() && placeholder != null) { + Text( + text = placeholder.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.disabled, + modifier = Modifier, + ) + } + textValue() + } + }, + modifier = modifier + .focusRequester(focusRequester), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt new file mode 100644 index 0000000000..e41ac02d17 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -0,0 +1,152 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +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.R +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) + * + * @param title title reference + * @param text primary text reference + * @param modifier modifier + * @param titleColor title color + * @param textColor text color + * @param iconRes action icon + * @param iconTint action icon tint + * @param onIconClick click on action icon + * @param showDivider show divider + * @see [InputRowEnter] for editable version + */ +@Composable +fun InputRowDefault( + title: TextReference, + text: TextReference, + modifier: Modifier = Modifier, + titleColor: Color = TangemTheme.colors.text.secondary, + textColor: Color = TangemTheme.colors.text.primary1, + iconRes: Int? = null, + iconTint: Color = TangemTheme.colors.icon.informative, + onIconClick: (() -> Unit)? = null, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Column( + modifier = Modifier + .weight(1f), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = textColor, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } + iconRes?.let { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onIconClick?.invoke() }, + ) + } + } + } +} + +//region preview +@Preview +@Composable +private fun InputRowDefaultPreview_Light( + @PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData, +) { + TangemTheme { + InputRowDefault( + title = TextReference.Str(data.title), + text = TextReference.Str(data.text), + iconRes = data.iconRes, + showDivider = data.showDivider, + modifier = Modifier.background(TangemTheme.colors.background.action), + ) + } +} + +@Preview +@Composable +private fun InputRowDefaultPreview_Dark( + @PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData, +) { + TangemTheme(isDark = true) { + InputRowDefault( + title = TextReference.Str(data.title), + text = TextReference.Str(data.text), + iconRes = data.iconRes, + showDivider = data.showDivider, + modifier = Modifier.background(TangemTheme.colors.background.action), + ) + } +} + +private data class InputRowDefaultPreviewData( + val title: String, + val text: String, + val iconRes: Int?, + val showDivider: Boolean, +) + +private class InputRowDefaultPreviewDataProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowDefaultPreviewData( + title = "title", + text = "text", + iconRes = null, + showDivider = true, + ), + InputRowDefaultPreviewData( + title = "title", + text = "text", + iconRes = R.drawable.ic_chevron_right_24, + showDivider = false, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt new file mode 100644 index 0000000000..f81372c3da --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt @@ -0,0 +1,167 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.VisualTransformation +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.R +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4) + * + * @param title title reference + * @param text primary text reference + * @param onValueChange text change callback + * @param modifier modifier + * @param titleColor title color + * @param textColor text color + * @param isSingleLine text + * @param visualTransformation applied transformation to text + * @param keyboardOptions keyboard options for field + * @param iconRes action icon + * @param iconTint action icon tint + * @param onIconClick click on action icon + * @param showDivider show divider + * @see [InputRowDefault] for read only version + */ +@Composable +fun InputRowEnter( + title: TextReference, + text: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + titleColor: Color = TangemTheme.colors.text.secondary, + textColor: Color = TangemTheme.colors.text.primary1, + isSingleLine: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + iconRes: Int? = null, + iconTint: Color = TangemTheme.colors.icon.informative, + onIconClick: (() -> Unit)? = null, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + SimpleTextField( + value = text, + onValueChange = onValueChange, + color = textColor, + singleLine = isSingleLine, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8), + ) + } + iconRes?.let { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onIconClick?.invoke() }, + ) + } + } + } +} + +//region preview +@Preview +@Composable +private fun InputRowEnterPreview_Light( + @PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData, +) { + TangemTheme { + InputRowEnter( + title = TextReference.Str(data.title), + text = data.text, + iconRes = data.iconRes, + showDivider = data.showDivider, + onValueChange = {}, + modifier = Modifier.background(TangemTheme.colors.background.action), + ) + } +} + +@Preview +@Composable +private fun InputRowEnterPreview_Dark( + @PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData, +) { + TangemTheme(isDark = true) { + InputRowEnter( + title = TextReference.Str(data.title), + text = data.text, + iconRes = data.iconRes, + showDivider = data.showDivider, + onValueChange = {}, + modifier = Modifier.background(TangemTheme.colors.background.action), + ) + } +} + +private data class InputRowEnterPreviewData( + val title: String, + val text: String, + val iconRes: Int?, + val showDivider: Boolean, +) + +private class InputRowEnterPreviewDataProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowEnterPreviewData( + title = "title", + text = "text", + iconRes = null, + showDivider = true, + ), + InputRowEnterPreviewData( + title = "title", + text = "text", + iconRes = R.drawable.ic_chevron_right_24, + showDivider = false, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt new file mode 100644 index 0000000000..25debb16dc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt @@ -0,0 +1,153 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.VisualTransformation +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.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Input Row Enter with Info variation. + * [Input Row Enter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4) + * [Input Row Enter Info](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7854-33577&mode=design&t=6o23sqF8fDQdn4C5-4) + * + * @param title title reference + * @param text primary text reference + * @param onValueChange text change callback + * @param modifier modifier + * @param titleColor title color + * @param textColor text color + * @param isSingleLine text + * @param visualTransformation applied transformation to text + * @param keyboardOptions keyboard options for field + * @param showDivider show divider + */ +@Composable +fun InputRowEnterInfo( + title: TextReference, + text: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + info: TextReference? = null, + titleColor: Color = TangemTheme.colors.text.secondary, + textColor: Color = TangemTheme.colors.text.primary1, + infoColor: Color = TangemTheme.colors.text.tertiary, + isSingleLine: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + Row { + SimpleTextField( + value = text, + onValueChange = onValueChange, + singleLine = isSingleLine, + color = textColor, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .weight(1f), + ) + info?.let { + Text( + text = it.resolveReference(), + style = TangemTheme.typography.body2, + color = infoColor, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .align(Alignment.Bottom), + ) + } + } + } + } +} + +//region preview +@Preview +@Composable +private fun InputRowEnterInfoPreview_Light( + @PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData, +) { + TangemTheme { + InputRowEnterInfo( + title = data.title, + text = data.text, + info = data.info, + showDivider = data.showDivider, + onValueChange = {}, + modifier = Modifier.background(TangemTheme.colors.background.action), + ) + } +} + +@Preview +@Composable +private fun InputRowEnterInfoPreview_Dark( + @PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData, +) { + TangemTheme(isDark = true) { + InputRowEnterInfo( + title = data.title, + text = data.text, + info = data.info, + showDivider = data.showDivider, + onValueChange = {}, + modifier = Modifier.background(TangemTheme.colors.background.action), + ) + } +} + +private data class InputRowEnterInfoPreviewData( + val title: TextReference, + val text: String, + val showDivider: Boolean, + val info: TextReference?, +) + +private class InputRowEnterInfoPreviewDataProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowEnterInfoPreviewData( + title = TextReference.Str("title"), + text = "text", + showDivider = true, + info = TextReference.Str("info"), + ), + InputRowEnterInfoPreviewData( + title = TextReference.Str("title"), + text = "text", + showDivider = false, + info = null, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt new file mode 100644 index 0000000000..1754c3b8bc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -0,0 +1,196 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +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.R +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * [Input Row Image](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-813&mode=design&t=IQ5lBJEkFGU4WSvi-4) + * + * @param title title reference + * @param subtitle subtitle reference + * @param caption caption reference + * @param tokenIconState token icon state [TokenIconState] + * @param modifier modifier + * @param titleColor title color + * @param subtitleColor subtitle color + * @param captionColor caption color + * @param iconRes action icon + * @param iconTint action icon tint + * @param onIconClick click on action icon + * @param showNetworkIcon show token network icon + * @param showDivider show divider + */ +@Composable +fun InputRowImage( + title: TextReference, + subtitle: TextReference, + caption: TextReference, + tokenIconState: TokenIconState, + modifier: Modifier = Modifier, + titleColor: Color = TangemTheme.colors.text.secondary, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, + iconRes: Int? = null, + iconTint: Color = TangemTheme.colors.icon.informative, + onIconClick: (() -> Unit)? = null, + showNetworkIcon: Boolean = false, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + Row( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing6, + ), + ) { + TokenIcon( + state = tokenIconState, + shouldDisplayNetwork = showNetworkIcon, + modifier = Modifier + .size(TangemTheme.dimens.size36), + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = TangemTheme.dimens.spacing12), + ) { + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = subtitleColor, + ) + Text( + text = caption.resolveReference(), + style = TangemTheme.typography.caption2, + color = captionColor, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing2), + ) + } + iconRes?.let { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier + .align(CenterVertically) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onIconClick?.invoke() }, + ) + } + } + } + } +} + +//region preview +@Preview +@Composable +private fun InputRowInputEnterInfoPreview_Light( + @PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData, +) { + TangemTheme { + InputRowImage( + title = data.title, + modifier = Modifier.background(TangemTheme.colors.background.action), + subtitle = data.subtitle, + caption = data.caption, + tokenIconState = data.iconState, + iconRes = data.actionIconRes, + onIconClick = {}, + showNetworkIcon = false, + showDivider = data.showDivider, + ) + } +} + +@Preview +@Composable +private fun InputRowImagePreview_Dark( + @PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData, +) { + TangemTheme(isDark = true) { + InputRowImage( + title = data.title, + modifier = Modifier.background(TangemTheme.colors.background.action), + subtitle = data.subtitle, + caption = data.caption, + tokenIconState = data.iconState, + iconRes = data.actionIconRes, + onIconClick = {}, + showNetworkIcon = false, + showDivider = data.showDivider, + ) + } +} + +private data class InputRowImagePreviewData( + val title: TextReference, + val subtitle: TextReference, + val caption: TextReference, + val iconState: TokenIconState, + val showDivider: Boolean, + val actionIconRes: Int?, + val showNetworkIcon: Boolean = false, +) + +private class InputRowImagePreviewDataProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowImagePreviewData( + title = TextReference.Str("title"), + subtitle = TextReference.Str("subtitle"), + caption = TextReference.Str("caption"), + iconState = TokenIconState.Locked, + actionIconRes = null, + showDivider = false, + showNetworkIcon = false, + ), + InputRowImagePreviewData( + title = TextReference.Str("title"), + subtitle = TextReference.Str("subtitle"), + caption = TextReference.Str("caption"), + iconState = TokenIconState.Locked, + actionIconRes = R.drawable.ic_chevron_right_24, + showDivider = true, + showNetworkIcon = true, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt new file mode 100644 index 0000000000..312b0c0db1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -0,0 +1,168 @@ +package com.tangem.core.ui.components.inputrow + +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.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.R +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.inputrow.inner.PasteButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4) + * + * @param title title reference + * @param value recipient address + * @param placeholder placeholder + * @param onValueChange callback for value change + * @param onPasteClick callback for paste + * @param modifier composable modifier + * @param singleLine is single line text + * @param error error text + * @param isError is error flag + * @param showDivider show divider + * + * @see InputRowRecipientDefault for readonly version + */ +@Composable +fun InputRowRecipient( + title: TextReference, + value: String, + placeholder: TextReference, + onValueChange: (String) -> Unit, + onPasteClick: (String) -> Unit, + modifier: Modifier = Modifier, + singleLine: Boolean = false, + error: TextReference? = null, + isError: Boolean = false, + showDivider: Boolean = false, +) { + val (titleText, color) = if (isError && error != null) { + error to TangemTheme.colors.text.warning + } else { + title to TangemTheme.colors.text.secondary + } + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = titleText.resolveReference(), + style = TangemTheme.typography.caption2, + color = color, + ) + Row( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8), + ) { + IdentIcon( + address = value, + modifier = Modifier + .align(CenterVertically) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) + .size(TangemTheme.dimens.size36) + .background(TangemTheme.colors.background.tertiary), + ) + SimpleTextField( + value = value, + placeholder = placeholder, + onValueChange = onValueChange, + singleLine = singleLine, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .weight(1f) + .align(CenterVertically), + ) + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + modifier = Modifier + .align(CenterVertically) + .padding(start = TangemTheme.dimens.spacing8), + ) + } + } + } +} + +//region preview +@Preview +@Composable +private fun InputRowRecipientPreview_Light( + @PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData, +) { + TangemTheme { + InputRowRecipient( + value = value.value, + title = TextReference.Res(R.string.send_recipient), + placeholder = TextReference.Res(R.string.send_optional_field), + error = TextReference.Str("Error"), + isError = value.isError, + showDivider = true, + onValueChange = {}, + onPasteClick = {}, + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} + +@Preview +@Composable +private fun InputRowRecipientPreview_Dark( + @PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData, +) { + TangemTheme(isDark = true) { + InputRowRecipient( + value = value.value, + title = TextReference.Res(R.string.send_recipient), + placeholder = TextReference.Res(R.string.send_optional_field), + error = TextReference.Str("Error"), + isError = value.isError, + showDivider = true, + onValueChange = {}, + onPasteClick = {}, + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} + +private data class InputRowRecipientPreviewData( + val value: String, + val isError: Boolean, +) + +private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowRecipientPreviewData( + value = "", + isError = false, + ), + InputRowRecipientPreviewData( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + isError = false, + ), + InputRowRecipientPreviewData( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + isError = true, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt new file mode 100644 index 0000000000..8ae6b1b386 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt @@ -0,0 +1,106 @@ +package com.tangem.core.ui.components.inputrow + +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.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Read only version of [InputRowRecipient]. + * [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode + * =design&t=IQ5lBJEkFGU4WSvi-4) + * + * @param title title reference + * @param value recipient address + * @param modifier composable modifier + * @param titleColor title color + * @param showDivider show divider + * @see InputRowRecipient for editable version + */ +@Composable +fun InputRowRecipientDefault( + title: TextReference, + value: String, + modifier: Modifier = Modifier, + titleColor: Color = TangemTheme.colors.text.secondary, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + Row( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8), + ) { + IdentIcon( + address = value, + modifier = Modifier + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) + .size(TangemTheme.dimens.size36) + .background(TangemTheme.colors.background.tertiary), + ) + Text( + text = value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .weight(1f) + .align(Alignment.CenterVertically), + ) + } + } + } +} + +//region preview +@Preview +@Composable +private fun InputRowRecipientPreview_Light() { + TangemTheme { + InputRowRecipientDefault( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + title = TextReference.Res(R.string.send_recipient), + showDivider = true, + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} + +@Preview +@Composable +private fun InputRowRecipientPreview_Dark() { + TangemTheme(isDark = true) { + InputRowRecipientDefault( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + title = TextReference.Res(R.string.send_recipient), + showDivider = false, + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt new file mode 100644 index 0000000000..a5510a378d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt @@ -0,0 +1,31 @@ +package com.tangem.core.ui.components.inputrow.inner + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun DividerContainer( + modifier: Modifier = Modifier, + showDivider: Boolean = false, + paddingValues: PaddingValues = PaddingValues(start = TangemTheme.dimens.spacing12), + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + content() + if (showDivider) { + Divider( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(paddingValues), + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt new file mode 100644 index 0000000000..3e4642c061 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -0,0 +1,80 @@ +package com.tangem.core.ui.components.inputrow.inner + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +/** + * Paste button with cross icon. Retrieves text from clipboard. + * [Paste button](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7853-33535&mode=design&t=6o23sqF8fDQdn4C5-4) + * + * @param isPasteButtonVisible is paste button visible + * @param onClick action callback + * @param modifier composable modifier + */ +@Composable +fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { + val clipboardManager = LocalClipboardManager.current + val hapticFeedback = LocalHapticFeedback.current + + if (isPasteButtonVisible) { + Box(modifier = modifier) { + Text( + text = "Paste", + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary2, + modifier = Modifier + .background( + color = TangemTheme.colors.button.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding( + horizontal = TangemTheme.dimens.spacing10, + vertical = TangemTheme.dimens.spacing2, + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(radius = TangemTheme.dimens.radius8), + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onClick( + clipboardManager + .getText() + ?.toString() + .orEmpty(), + ) + }, + ), + ) + } + } else { + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = stringResource(R.string.common_close), + modifier = modifier + .size(TangemTheme.dimens.size20) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(radius = TangemTheme.dimens.radius10), + onClick = { onClick("") }, + ), + ) + } +} \ No newline at end of file From 5a277755c415cc0ced047944eac387cbcbddb73c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 10:53:09 +0300 Subject: [PATCH 10/11] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../tangem/datasource/local/quote/DefaultQuotesStore.kt | 8 ++++++++ .../java/com/tangem/datasource/local/quote/QuotesStore.kt | 2 ++ .../data/tokens/repository/DefaultQuotesRepository.kt | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index ade38b689e..f5a90466d2 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit ade38b689e5e390cf4105f9dfa641b4f6912dcd0 +Subproject commit f5a90466d2245ca5cfde59b49dc7fa82a6b7cbc2 diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index 2b8de0afd9..fc0d1e0e42 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -30,6 +30,14 @@ internal class DefaultQuotesStore( } } + override suspend fun getSync(currenciesIds: Set): Set { + return currenciesIds.mapNotNull { currencyId -> + currencyId.rawCurrencyId?.let { + dataStore.getSyncOrNull(it) + } + }.toSet() + } + override suspend fun store(response: QuotesResponse) { val quotes = response.quotes.mapValues { (id, quote) -> StoredQuote(id, quote) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt index 64b3bf2c9c..dd3f577782 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt @@ -9,5 +9,7 @@ interface QuotesStore { fun get(currenciesIds: Set): Flow> + suspend fun getSync(currenciesIds: Set): Set + suspend fun store(response: QuotesResponse) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 825f937b65..cdd04c62ce 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -61,7 +61,7 @@ internal class DefaultQuotesRepository( fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh) - val quotes = quotesStore.get(currenciesIds).first() + val quotes = quotesStore.getSync(currenciesIds) quotesConverter.convertSet(quotes) } From bcd686fb41368f773c1ad3a648af249bc6e0fb91 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 18:46:14 +0500 Subject: [PATCH 11/11] Updated on 2026-08-14 --- .../tangem/domain/walletmanager/DefaultWalletManagersFacade.kt | 1 + gradle/dependencies.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index a581d0d2fa..15010e8b52 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -201,6 +201,7 @@ class DefaultWalletManagersFacade( val itemsResult = walletManager.getTransactionsHistory( request = TransactionHistoryRequest( address = walletManager.wallet.address, + decimals = currency.decimals, page = TransactionHistoryRequest.Page(number = page, size = pageSize), filterType = when (currency) { is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index eed86ea134..90e018f42d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-387" +tangemBlockchainSdk = "develop-393" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-310" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds